diff --git a/test/vm/unit/CMakeLists.txt b/test/vm/unit/CMakeLists.txt index 55befcf956..a9e308847b 100644 --- a/test/vm/unit/CMakeLists.txt +++ b/test/vm/unit/CMakeLists.txt @@ -24,6 +24,7 @@ target_sources(vm-unit-tests PRIVATE evm-as_tests.cpp monad_vm_interface_tests.cpp monad_vm_memory_pool_tests.cpp + mocked_host_tests.cpp utils_tests.cpp uint256_tests.cpp uint128_tests.cpp @@ -69,7 +70,6 @@ target_link_libraries(vm-unit-tests PRIVATE monad-vm::monad-vm-interpreter PRIVATE monad-vm::monad-vm-utils PRIVATE GTest::gtest - PRIVATE evmc::mocked_host PRIVATE monad_execution PRIVATE monad_core PRIVATE quill::quill diff --git a/test/vm/unit/evm_fixture.hpp b/test/vm/unit/evm_fixture.hpp index 1fa85f8970..3a7ba27c45 100644 --- a/test/vm/unit/evm_fixture.hpp +++ b/test/vm/unit/evm_fixture.hpp @@ -26,12 +26,12 @@ #include #include #include +#include #include #include #include -#include #include @@ -96,7 +96,7 @@ namespace monad::vm::compiler::test monad::vm::test::TestMessage test_msg_; evmc_message &msg_{*test_msg_}; - evmc::MockedHost host_; + vm::test::MockedHost host_; evmc::Result result_; diff --git a/test/vm/unit/mocked_host_tests.cpp b/test/vm/unit/mocked_host_tests.cpp new file mode 100644 index 0000000000..73a53f161b --- /dev/null +++ b/test/vm/unit/mocked_host_tests.cpp @@ -0,0 +1,71 @@ +// Copyright (C) 2025-26 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 + +using namespace monad; +using namespace monad::vm::test; + +TEST(MockedHost, StorageStatus) +{ + struct Transition + { + bytes32_t original; + bytes32_t current; + bytes32_t value; + evmc_storage_status status; + }; + + auto const O = bytes32_t{}; + auto const X = bytes32_t{1}; + auto const Y = bytes32_t{2}; + auto const Z = bytes32_t{3}; + + Transition const transitions[] = { + {O, O, O, EVMC_STORAGE_ASSIGNED}, + {X, O, O, EVMC_STORAGE_ASSIGNED}, + {O, Y, Y, EVMC_STORAGE_ASSIGNED}, + {X, Y, Y, EVMC_STORAGE_ASSIGNED}, + {Y, Y, Y, EVMC_STORAGE_ASSIGNED}, + {O, Y, Z, EVMC_STORAGE_ASSIGNED}, + {X, Y, Z, EVMC_STORAGE_ASSIGNED}, + {O, O, Z, EVMC_STORAGE_ADDED}, + {X, X, O, EVMC_STORAGE_DELETED}, + {X, X, Z, EVMC_STORAGE_MODIFIED}, + {X, O, Z, EVMC_STORAGE_DELETED_ADDED}, + {X, Y, O, EVMC_STORAGE_MODIFIED_DELETED}, + {X, O, X, EVMC_STORAGE_DELETED_RESTORED}, + {O, Y, O, EVMC_STORAGE_ADDED_DELETED}, + {X, Y, X, EVMC_STORAGE_MODIFIED_RESTORED}, + }; + + auto const addr = 0x00000000000000000000000000000000000000AA_address; + auto const key = bytes32_t{7}; + + for (auto const &t : transitions) { + MockedHost host; + host.accounts[addr].storage[key] = { + .current = t.current, .original = t.original}; + EXPECT_EQ(host.set_storage(addr, key, t.value), t.status); + EXPECT_EQ(host.get_storage(addr, key), evmc::bytes32{t.value}); + } +} diff --git a/test/vm/unit/monad_vm_interface_tests.cpp b/test/vm/unit/monad_vm_interface_tests.cpp index 637b54cae2..1f18ec9aaf 100644 --- a/test/vm/unit/monad_vm_interface_tests.cpp +++ b/test/vm/unit/monad_vm_interface_tests.cpp @@ -23,13 +23,13 @@ #include #include +#include #include #include #include #include -#include #include @@ -416,7 +416,7 @@ TEST(MonadVmInterface, try_insert_varcode) TEST(MonadVmInterface, execute_bytecode_raw) { VM vm; - evmc::MockedHost host; + test::MockedHost host; auto [bytecode0, hash0] = make_bytecode(0); @@ -438,7 +438,7 @@ TEST(MonadVmInterface, execute_bytecode_raw) TEST(MonadVmInterface, execute_intercode_raw) { VM vm; - evmc::MockedHost host; + test::MockedHost host; auto [bytecode0, hash0] = make_bytecode(0); auto icode0 = make_shared_intercode(bytecode0); @@ -460,7 +460,7 @@ TEST(MonadVmInterface, execute_intercode_raw) TEST(MonadVmInterface, execute_native_entrypoint_raw) { VM vm; - evmc::MockedHost host; + test::MockedHost host; auto [bytecode0, hash0] = make_bytecode(0); auto icode0 = make_shared_intercode(bytecode0); @@ -485,7 +485,7 @@ TEST(MonadVmInterface, execute_native_entrypoint_raw) static void test_execute_raw(VM::Mode const mode) { VM vm{mode}; - evmc::MockedHost host; + test::MockedHost host; test::TestMessage msg{}; msg->gas = 100'000'000; diff --git a/test/vm/unit/runtime/fixture.cpp b/test/vm/unit/runtime/fixture.cpp index aab39e8920..956a3e96ba 100644 --- a/test/vm/unit/runtime/fixture.cpp +++ b/test/vm/unit/runtime/fixture.cpp @@ -22,10 +22,10 @@ #include #include #include +#include #include #include -#include #include #include @@ -172,9 +172,8 @@ namespace monad::vm::compiler::test auto const codehash = keccak256({code.data(), code.size()}); bytes32_t codehash_bytes; std::copy(codehash.bytes, codehash.bytes + 32, codehash_bytes.bytes); - auto const account = evmc::MockedAccount{ - .nonce = 0, - .code = evmc::bytes(code.data(), code.size()), + auto const account = vm::test::MockedAccount{ + .code = byte_string(code.data(), code.size()), .codehash = codehash_bytes, .balance = {}, .storage = {}, diff --git a/test/vm/unit/runtime/fixture.hpp b/test/vm/unit/runtime/fixture.hpp index 6539792de3..7369a42df1 100644 --- a/test/vm/unit/runtime/fixture.hpp +++ b/test/vm/unit/runtime/fixture.hpp @@ -20,7 +20,7 @@ #include #include #include -#include +#include #include #include diff --git a/test/vm/unit/runtime/mocked_host.hpp b/test/vm/unit/runtime/mocked_host.hpp deleted file mode 100644 index 9b90421106..0000000000 --- a/test/vm/unit/runtime/mocked_host.hpp +++ /dev/null @@ -1,211 +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 . - -#pragma once - -#include -#include - -#include -#include - -#include -#include -#include -#include -#include - -namespace monad::vm::test -{ - class MockedHost : public Host - { - evmc::MockedHost inner_; - - static constexpr size_t PAGE_KEY_SHIFT = 7; - - struct PageGrowth - { - int current_state_growth{0}; - int net_state_growth{0}; - }; - - using PageKey = std::pair; - - std::set read_accessed_pages_; - std::set write_accessed_pages_; - std::map growth_; - evmc_page_storage_status last_write_page_status_{}; - - static evmc::bytes32 - compute_page_key(evmc::bytes32 const &slot_key) noexcept - { - return store_be_as( - load_be(slot_key) >> PAGE_KEY_SHIFT); - } - - public: - decltype(inner_.accounts) &accounts = inner_.accounts; - decltype(inner_.recorded_calls) &recorded_calls = inner_.recorded_calls; - decltype(inner_.recorded_logs) &recorded_logs = inner_.recorded_logs; - decltype(inner_.recorded_selfdestructs) &recorded_selfdestructs = - inner_.recorded_selfdestructs; - decltype(inner_.recorded_account_accesses) &recorded_account_accesses = - inner_.recorded_account_accesses; - decltype(inner_.recorded_blockhashes) &recorded_blockhashes = - inner_.recorded_blockhashes; - decltype(inner_.tx_context) &tx_context = inner_.tx_context; - decltype(inner_.block_hash) &block_hash = inner_.block_hash; - decltype(inner_.call_result) &call_result = inner_.call_result; - - bool account_exists(evmc::address const &addr) const noexcept override - { - return inner_.account_exists(addr); - } - - evmc::bytes32 get_storage( - evmc::address const &addr, - evmc::bytes32 const &key) const noexcept override - { - return inner_.get_storage(addr, key); - } - - evmc_storage_status set_storage( - evmc::address const &addr, evmc::bytes32 const &key, - evmc::bytes32 const &v_new) noexcept override - { - auto const v_current = inner_.get_storage(addr, key); - auto const p = PageKey{addr, compute_page_key(key)}; - - bool first_page_write = false; - if (v_current != v_new) { - auto const [it, inserted] = write_accessed_pages_.insert(p); - if (inserted) { - first_page_write = true; - growth_[p] = PageGrowth{}; - } - } - - auto const zero = evmc::bytes32{}; - if (v_current == zero && v_new != zero) { - growth_[p].current_state_growth += 1; - } - else if (v_current != zero && v_new == zero) { - growth_[p].current_state_growth -= 1; - } - - bool grew_state = false; - if (growth_[p].current_state_growth > growth_[p].net_state_growth) { - growth_[p].net_state_growth = growth_[p].current_state_growth; - grew_state = true; - } - - last_write_page_status_ = {first_page_write, grew_state}; - - return inner_.set_storage(addr, key, v_new); - } - - evmc::uint256be - get_balance(evmc::address const &addr) const noexcept override - { - return inner_.get_balance(addr); - } - - size_t get_code_size(evmc::address const &addr) const noexcept override - { - return inner_.get_code_size(addr); - } - - evmc::bytes32 - get_code_hash(evmc::address const &addr) const noexcept override - { - return inner_.get_code_hash(addr); - } - - size_t copy_code( - evmc::address const &addr, size_t code_offset, uint8_t *buffer_data, - size_t buffer_size) const noexcept override - { - return inner_.copy_code( - addr, code_offset, buffer_data, buffer_size); - } - - bool selfdestruct( - evmc::address const &addr, - evmc::address const &beneficiary) noexcept override - { - return inner_.selfdestruct(addr, beneficiary); - } - - evmc::Result call(evmc_message const &msg) noexcept override - { - return inner_.call(msg); - } - - evmc_tx_context const *get_tx_context() const noexcept override - { - return inner_.get_tx_context(); - } - - evmc::bytes32 - get_block_hash(int64_t block_number) const noexcept override - { - return inner_.get_block_hash(block_number); - } - - void emit_log( - evmc::address const &addr, uint8_t const *data, size_t data_size, - evmc::bytes32 const topics[], size_t topics_count) noexcept override - { - inner_.emit_log(addr, data, data_size, topics, topics_count); - } - - evmc_access_status - access_account(evmc::address const &addr) noexcept override - { - return inner_.access_account(addr); - } - - evmc_access_status access_storage( - evmc::address const &addr, - evmc::bytes32 const &key) noexcept override - { - inner_.access_storage(addr, key); - auto const p = PageKey{addr, compute_page_key(key)}; - auto const [it, inserted] = read_accessed_pages_.insert(p); - return inserted ? EVMC_ACCESS_COLD : EVMC_ACCESS_WARM; - } - - evmc::bytes32 get_transient_storage( - evmc::address const &addr, - evmc::bytes32 const &key) const noexcept override - { - return inner_.get_transient_storage(addr, key); - } - - void set_transient_storage( - evmc::address const &addr, evmc::bytes32 const &key, - evmc::bytes32 const &value) noexcept override - { - inner_.set_transient_storage(addr, key, value); - } - - evmc_page_storage_status update_page( - evmc::address const &, evmc::bytes32 const &, - evmc_storage_status) noexcept override - { - return last_write_page_status_; - } - }; -} diff --git a/test/vm/utils/CMakeLists.txt b/test/vm/utils/CMakeLists.txt index 9b017e761d..76665bd0fe 100644 --- a/test/vm/utils/CMakeLists.txt +++ b/test/vm/utils/CMakeLists.txt @@ -17,6 +17,7 @@ add_library(monad-vm-test-utils OBJECT) target_sources(monad-vm-test-utils PRIVATE "evm-as_utils.hpp" + "mocked_host.hpp" "test_context.hpp" "test_host.hpp" "test_memory.hpp" diff --git a/test/vm/utils/mocked_host.hpp b/test/vm/utils/mocked_host.hpp new file mode 100644 index 0000000000..2e4092230f --- /dev/null +++ b/test/vm/utils/mocked_host.hpp @@ -0,0 +1,335 @@ +// Copyright (C) 2025-26 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 . + +// Derived from evmc's include/evmc/mocked_host.hpp, Copyright 2019 The EVMC +// Authors, licensed under the Apache License, Version 2.0 +// (third_party/evmc/LICENSE); modified for monad's vm::Host. + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace monad::vm::test +{ + struct StorageValue + { + bytes32_t current; + bytes32_t original; + }; + + struct MockedAccount + { + byte_string code; + bytes32_t codehash; + bytes32_t balance; + std::unordered_map storage; + std::unordered_map transient_storage; + + void set_balance(uint64_t const x) noexcept + { + balance = bytes32_t{x}; + } + }; + + struct LogRecord + { + Address creator; + byte_string data; + std::vector topics; + }; + + class MockedHost : public Host + { + struct PageGrowth + { + int current_state_growth{0}; + int net_state_growth{0}; + }; + + using PageKey = std::pair; + + std::set read_accessed_pages_; + std::set write_accessed_pages_; + std::map growth_; + evmc_page_storage_status last_write_page_status_{}; + + void record_account_access(Address const &addr) const noexcept + { + recorded_account_accesses.emplace_back(addr); + } + + static evmc_storage_status storage_status( + bytes32_t const &original, bytes32_t const ¤t, + bytes32_t const &value) noexcept + { + auto const zero = bytes32_t{}; + if (current == value) { + return EVMC_STORAGE_ASSIGNED; + } + if (original == current) { + if (original == zero) { + return EVMC_STORAGE_ADDED; + } + return value == zero ? EVMC_STORAGE_DELETED + : EVMC_STORAGE_MODIFIED; + } + if (original != zero) { + if (current == zero) { + return original == value ? EVMC_STORAGE_DELETED_RESTORED + : EVMC_STORAGE_DELETED_ADDED; + } + if (value == zero) { + return EVMC_STORAGE_MODIFIED_DELETED; + } + } + if (original == value) { + return original == zero ? EVMC_STORAGE_ADDED_DELETED + : EVMC_STORAGE_MODIFIED_RESTORED; + } + return EVMC_STORAGE_ASSIGNED; + } + + public: + std::unordered_map accounts; + evmc_tx_context tx_context{}; + bytes32_t block_hash; + evmc_result call_result{}; + mutable std::vector recorded_blockhashes; + mutable std::vector
recorded_account_accesses; + // Pointer fields are nulled on record; only scalars outlive the call. + std::vector recorded_calls; + std::vector recorded_logs; + std::unordered_map> + recorded_selfdestructs; + + bool account_exists(evmc::address const &addr) const noexcept override + { + record_account_access(addr); + return accounts.contains(addr); + } + + evmc::bytes32 get_storage( + evmc::address const &addr, + evmc::bytes32 const &key) const noexcept override + { + record_account_access(addr); + auto const account = accounts.find(addr); + if (account == accounts.end()) { + return {}; + } + auto const slot = account->second.storage.find(key); + if (slot == account->second.storage.end()) { + return {}; + } + return slot->second.current; + } + + evmc_storage_status set_storage( + evmc::address const &addr, evmc::bytes32 const &key, + evmc::bytes32 const &value) noexcept override + { + record_account_access(addr); + auto &slot = accounts[addr].storage[key]; + auto const v_current = slot.current; + auto const v_new = bytes32_t{value}; + auto const p = PageKey{addr, compute_page_key(key)}; + + bool first_page_write = false; + if (v_current != v_new) { + auto const [it, inserted] = write_accessed_pages_.insert(p); + if (inserted) { + first_page_write = true; + growth_[p] = PageGrowth{}; + } + } + + auto const zero = bytes32_t{}; + if (v_current == zero && v_new != zero) { + growth_[p].current_state_growth += 1; + } + else if (v_current != zero && v_new == zero) { + growth_[p].current_state_growth -= 1; + } + + bool grew_state = false; + if (growth_[p].current_state_growth > growth_[p].net_state_growth) { + growth_[p].net_state_growth = growth_[p].current_state_growth; + grew_state = true; + } + + last_write_page_status_ = {first_page_write, grew_state}; + + auto const status = storage_status(slot.original, v_current, v_new); + slot.current = v_new; + return status; + } + + evmc::uint256be + get_balance(evmc::address const &addr) const noexcept override + { + record_account_access(addr); + auto const account = accounts.find(addr); + return account == accounts.end() + ? evmc::uint256be{} + : evmc::uint256be{account->second.balance}; + } + + size_t get_code_size(evmc::address const &addr) const noexcept override + { + record_account_access(addr); + auto const account = accounts.find(addr); + return account == accounts.end() ? 0 : account->second.code.size(); + } + + evmc::bytes32 + get_code_hash(evmc::address const &addr) const noexcept override + { + record_account_access(addr); + auto const account = accounts.find(addr); + return account == accounts.end() + ? evmc::bytes32{} + : evmc::bytes32{account->second.codehash}; + } + + size_t copy_code( + evmc::address const &addr, size_t const code_offset, + uint8_t *const buffer_data, + size_t const buffer_size) const noexcept override + { + record_account_access(addr); + auto const account = accounts.find(addr); + if (account == accounts.end()) { + return 0; + } + auto const &code = account->second.code; + if (code_offset >= code.size()) { + return 0; + } + auto const n = std::min(buffer_size, code.size() - code_offset); + std::copy_n(code.data() + code_offset, n, buffer_data); + return n; + } + + bool selfdestruct( + evmc::address const &addr, + evmc::address const &beneficiary) noexcept override + { + record_account_access(addr); + auto &beneficiaries = recorded_selfdestructs[addr]; + beneficiaries.emplace_back(beneficiary); + return beneficiaries.size() == 1; + } + + evmc::Result call(evmc_message const &msg) noexcept override + { + record_account_access(msg.recipient); + auto &rec = recorded_calls.emplace_back(msg); + rec.input_data = nullptr; + rec.input_size = 0; + rec.memory_handle = nullptr; + rec.memory = nullptr; + rec.memory_capacity = 0; + return evmc::Result{call_result}; + } + + evmc_tx_context const *get_tx_context() const noexcept override + { + return &tx_context; + } + + evmc::bytes32 + get_block_hash(int64_t const block_number) const noexcept override + { + recorded_blockhashes.emplace_back(block_number); + return block_hash; + } + + void emit_log( + evmc::address const &addr, uint8_t const *const data, + size_t const data_size, evmc::bytes32 const topics[], + size_t const topics_count) noexcept override + { + recorded_logs.push_back( + {addr, {data, data_size}, {topics, topics + topics_count}}); + } + + evmc_access_status + access_account(evmc::address const &addr) noexcept override + { + auto const a = Address{addr}; + auto const already_accessed = + std::ranges::find(recorded_account_accesses, a) != + recorded_account_accesses.end(); + record_account_access(a); + return already_accessed ? EVMC_ACCESS_WARM : EVMC_ACCESS_COLD; + } + + evmc_access_status access_storage( + evmc::address const &addr, + evmc::bytes32 const &key) noexcept override + { + auto const p = PageKey{addr, compute_page_key(key)}; + auto const [it, inserted] = read_accessed_pages_.insert(p); + return inserted ? EVMC_ACCESS_COLD : EVMC_ACCESS_WARM; + } + + evmc::bytes32 get_transient_storage( + evmc::address const &addr, + evmc::bytes32 const &key) const noexcept override + { + record_account_access(addr); + auto const account = accounts.find(addr); + if (account == accounts.end()) { + return {}; + } + auto const slot = account->second.transient_storage.find(key); + if (slot == account->second.transient_storage.end()) { + return {}; + } + return slot->second; + } + + void set_transient_storage( + evmc::address const &addr, evmc::bytes32 const &key, + evmc::bytes32 const &value) noexcept override + { + record_account_access(addr); + accounts[addr].transient_storage[key] = value; + } + + evmc_page_storage_status update_page( + evmc::address const &, evmc::bytes32 const &, + evmc_storage_status) noexcept override + { + return last_write_page_status_; + } + }; +}