diff --git a/test/state/CMakeLists.txt b/test/state/CMakeLists.txt index cd35817f17..585f59ad5a 100644 --- a/test/state/CMakeLists.txt +++ b/test/state/CMakeLists.txt @@ -32,6 +32,8 @@ target_sources( rlp.hpp state.hpp state.cpp + state_diff.hpp + state_view.hpp system_contracts.hpp system_contracts.cpp test_state.hpp diff --git a/test/state/account.hpp b/test/state/account.hpp index d836525ddc..d08c009559 100644 --- a/test/state/account.hpp +++ b/test/state/account.hpp @@ -12,6 +12,7 @@ namespace evmone::state using evmc::address; using evmc::bytes; using evmc::bytes32; +using namespace evmc::literals; /// The representation of the account storage value. struct StorageValue @@ -31,25 +32,38 @@ struct Account /// The maximum allowed nonce value. static constexpr auto NonceMax = std::numeric_limits::max(); + /// The keccak256 hash of the empty input. Used to identify empty account's code. + static constexpr auto EMPTY_CODE_HASH = + 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470_bytes32; + /// The account nonce. uint64_t nonce = 0; /// The account balance. intx::uint256 balance; - /// The account storage map. + bytes32 code_hash = EMPTY_CODE_HASH; + + /// If the account has non-empty initial storage (when accessing the cold account). + bool has_initial_storage = false; + + /// The cached and modified account storage entries. std::unordered_map storage; + /// The EIP-1153 transient (transaction-level lifetime) storage. std::unordered_map transient_storage; - /// The account code. + /// The cache of the account code. + /// + /// Check code_hash to know if an account code is empty. + /// Empty here only means it has not been loaded from the initial storage. bytes code; - /// The account has been destructed and should be erased at the end of of a transaction. + /// The account has been destructed and should be erased at the end of a transaction. bool destructed = false; /// The account should be erased if it is empty at the end of a transaction. - /// This flag means the account has been "touched" as defined in EIP-161 + /// This flag means the account has been "touched" as defined in EIP-161, /// or it is a newly created temporary account. /// /// Yellow Paper uses term "delete" but it is a keyword in C++ while @@ -63,7 +77,7 @@ struct Account [[nodiscard]] bool is_empty() const noexcept { - return code.empty() && nonce == 0 && balance == 0; + return nonce == 0 && balance == 0 && code_hash == EMPTY_CODE_HASH; } }; } // namespace evmone::state diff --git a/test/state/host.cpp b/test/state/host.cpp index 258aba2eba..a227d09142 100644 --- a/test/state/host.cpp +++ b/test/state/host.cpp @@ -18,10 +18,7 @@ bool Host::account_exists(const address& addr) const noexcept bytes32 Host::get_storage(const address& addr, const bytes32& key) const noexcept { - const auto& acc = m_state.get(addr); - if (const auto it = acc.storage.find(key); it != acc.storage.end()) - return it->second.current; - return {}; + return m_state.get_storage(addr, key).current; } evmc_storage_status Host::set_storage( @@ -30,7 +27,7 @@ evmc_storage_status Host::set_storage( // Follow EVMC documentation https://evmc.ethereum.org/storagestatus.html#autotoc_md3 // and EIP-2200 specification https://eips.ethereum.org/EIPS/eip-2200. - auto& storage_slot = m_state.get(addr).storage[key]; + auto& storage_slot = m_state.get_storage(addr, key); const auto& [current, original, _] = storage_slot; const auto dirty = original != current; @@ -92,23 +89,28 @@ bytes_view extcode(bytes_view code) noexcept /// as defined in the [EIP-7610](https://eips.ethereum.org/EIPS/eip-7610). [[nodiscard]] bool is_create_collision(const Account& acc) noexcept { - if (acc.nonce != 0 || !acc.code.empty()) + // TODO: This requires much more testing: + // - what if an account had storage but is destructed? + // - what if an account had cold storage but it was emptied? + // - what if an account without cold storage gain one? + if (acc.nonce != 0) return true; - - // acc.storage may have entries from access list, even if account storage is empty. - // Check for non-zero current values. - if (std::ranges::any_of( - acc.storage, [](auto& e) noexcept { return !is_zero(e.second.current); })) + if (acc.code_hash != Account::EMPTY_CODE_HASH) + return true; + if (acc.has_initial_storage) return true; + // The hot storage is ignored because it can contain elements from access list. + // TODO: Is this correct for destructed accounts? + assert(!acc.destructed && "untested"); return false; } } // namespace size_t Host::get_code_size(const address& addr) const noexcept { - const auto* const acc = m_state.find(addr); - return (acc != nullptr) ? extcode(acc->code).size() : 0; + const auto raw_code = m_state.get_code(addr); + return extcode(raw_code).size(); } bytes32 Host::get_code_hash(const address& addr) const noexcept @@ -116,17 +118,20 @@ bytes32 Host::get_code_hash(const address& addr) const noexcept const auto* const acc = m_state.find(addr); if (acc == nullptr || acc->is_empty()) return {}; - if (is_eof_container(acc->code)) + + // Load code and check if not EOF. + // TODO: Optimize the second account lookup here. + if (is_eof_container(m_state.get_code(addr))) return EOF_CODE_HASH_SENTINEL; - // TODO: Cache code hash. It will be needed also to compute the MPT hash. - return keccak256(acc->code); + + return acc->code_hash; } size_t Host::copy_code(const address& addr, size_t code_offset, uint8_t* buffer_data, size_t buffer_size) const noexcept { - const auto* const acc = m_state.find(addr); - const auto code = (acc != nullptr) ? extcode(acc->code) : bytes_view{}; + const auto raw_code = m_state.get_code(addr); + const auto code = extcode(raw_code); const auto code_slice = code.substr(std::min(code_offset, code.size())); const auto num_bytes = std::min(buffer_size, code_slice.size()); std::copy_n(code_slice.begin(), num_bytes, buffer_data); @@ -367,6 +372,7 @@ evmc::Result Host::create(const evmc_message& msg) noexcept } } + new_acc->code_hash = keccak256(code); new_acc->code = code; return evmc::Result{result.status_code, gas_left, result.gas_refund, msg.recipient}; @@ -409,9 +415,8 @@ evmc::Result Host::execute_message(const evmc_message& msg) noexcept if (is_precompile(m_rev, msg.code_address)) return call_precompile(m_rev, msg); - // In case msg.recipient == msg.code_address, this is the second lookup of the same address. - const auto* const code_acc = m_state.find(msg.code_address); - const auto code = code_acc != nullptr ? bytes_view{code_acc->code} : bytes_view{}; + // TODO: get_code() performs the account lookup. Add a way to get an account with code? + const auto code = m_state.get_code(msg.code_address); return m_vm.execute(*this, m_rev, msg, code.data(), code.size()); } @@ -503,7 +508,7 @@ evmc_access_status Host::access_account(const address& addr) noexcept evmc_access_status Host::access_storage(const address& addr, const bytes32& key) noexcept { - auto& storage_slot = m_state.get(addr).storage[key]; + auto& storage_slot = m_state.get_storage(addr, key); m_state.journal_storage_change(addr, key, storage_slot); return std::exchange(storage_slot.access_status, EVMC_ACCESS_WARM); } diff --git a/test/state/state.cpp b/test/state/state.cpp index 0569fe7480..a756d29e93 100644 --- a/test/state/state.cpp +++ b/test/state/state.cpp @@ -5,6 +5,7 @@ #include "state.hpp" #include "../utils/stdx/utility.hpp" #include "host.hpp" +#include "state_view.hpp" #include #include #include @@ -78,18 +79,64 @@ evmc_message build_message( } } // namespace +StateDiff State::build_diff(evmc_revision rev) const +{ + StateDiff diff; + for (const auto& [addr, m] : m_modified) + { + if (m.destructed) + { + // TODO: This must be done even for just_created + // because destructed may pre-date just_created. Add test to evmone (EEST has it). + diff.deleted_accounts.emplace_back(addr); + continue; + } + if (m.erase_if_empty && rev >= EVMC_SPURIOUS_DRAGON && m.is_empty()) + { + if (!m.just_created) // Don't report just created accounts + diff.deleted_accounts.emplace_back(addr); + continue; + } + + // Unconditionally report nonce and balance as modified. + // TODO: We don't have information if the balance/nonce has actually changed. + // One option is to just keep the original values. This may be handy for RPC. + // TODO(clang): In old Clang emplace_back without Account doesn't compile. + // NOLINTNEXTLINE(modernize-use-emplace) + auto& a = diff.modified_accounts.emplace_back(StateDiff::Entry{addr, m.nonce, m.balance}); + + // Output only the new code. + // TODO: Output also the code hash. It will be needed for DB update and MPT hash. + if (m.just_created && !m.code.empty()) + a.code = m.code; + + for (const auto& [k, v] : m.storage) + { + if (v.current != v.original) + a.modified_storage.emplace_back(k, v.current); + } + } + return diff; +} + Account& State::insert(const address& addr, Account account) { - const auto r = m_accounts.insert({addr, std::move(account)}); + const auto r = m_modified.insert({addr, std::move(account)}); assert(r.second); return r.first->second; } Account* State::find(const address& addr) noexcept { - const auto it = m_accounts.find(addr); - if (it != m_accounts.end()) + // TODO: Avoid double lookup (find+insert) and not cached initial state lookup for non-existent + // accounts. If we want to cache non-existent account we need a proper flag for it. + if (const auto it = m_modified.find(addr); it != m_modified.end()) return &it->second; + if (const auto cacc = m_initial.get_account(addr); cacc) + return &insert(addr, {.nonce = cacc->nonce, + .balance = cacc->balance, + .code_hash = cacc->code_hash, + .has_initial_storage = cacc->has_storage}); return nullptr; } @@ -107,6 +154,18 @@ Account& State::get_or_insert(const address& addr, Account account) return insert(addr, std::move(account)); } +bytes_view State::get_code(const address& addr) +{ + auto* a = find(addr); + if (a == nullptr) + return {}; + if (a->code_hash == Account::EMPTY_CODE_HASH) + return {}; + if (a->code.empty()) + a->code = m_initial.get_account_code(addr); + return a->code; +} + Account& State::touch(const address& addr) { auto& acc = get_or_insert(addr, {.erase_if_empty = true}); @@ -118,6 +177,19 @@ Account& State::touch(const address& addr) return acc; } +StorageValue& State::get_storage(const address& addr, const bytes32& key) +{ + // TODO: Avoid account lookup by giving the reference to the account's storage to Host. + auto& acc = get(addr); + const auto [it, missing] = acc.storage.try_emplace(key); + if (missing) + { + const auto initial_value = m_initial.get_storage(addr, key); + it->second = {initial_value, initial_value}; + } + return it->second; +} + void State::journal_balance_change(const address& addr, const intx::uint256& prev_balance) { m_journal.emplace_back(JournalBalanceChange{{addr}, prev_balance}); @@ -185,6 +257,7 @@ void State::rollback(size_t checkpoint) // This account is not always "touched". TODO: Why? auto& a = get(e.addr); a.nonce = 0; + a.code_hash = Account::EMPTY_CODE_HASH; a.code.clear(); } else @@ -193,7 +266,7 @@ void State::rollback(size_t checkpoint) // so we need to delete them here explicitly. // This should be changed by tuning "erasable" flag // and clear in all revisions. - m_accounts.erase(e.addr); + m_modified.erase(e.addr); } } else if constexpr (std::is_same_v) @@ -277,7 +350,7 @@ std::variant validate_transaction(const Account& sende if (tx.max_gas_price < block.base_fee) return make_error_code(FEE_CAP_LESS_THEN_BLOCKS); - if (!sender_acc.code.empty()) + if (sender_acc.code_hash != Account::EMPTY_CODE_HASH) return make_error_code(SENDER_NOT_EOA); // Origin must not be a contract (EIP-3607). if (sender_acc.nonce == Account::NonceMax) // Nonce value limit (EIP-2681). @@ -315,23 +388,11 @@ std::variant validate_transaction(const Account& sende return execution_gas_limit; } -namespace -{ -/// Deletes "touched" (marked as erasable) empty accounts in the state. -void delete_empty_accounts(State& state) -{ - std::erase_if(state.get_accounts(), [](const std::pair& p) noexcept { - const auto& acc = p.second; - return acc.erase_if_empty && acc.is_empty(); - }); -} -} // namespace - - -void finalize(State& state, evmc_revision rev, const address& coinbase, +StateDiff finalize(const StateView& state_view, evmc_revision rev, const address& coinbase, std::optional block_reward, std::span ommers, std::span withdrawals) { + State state{state_view}; // TODO: The block reward can be represented as a withdrawal. if (block_reward.has_value()) { @@ -351,15 +412,14 @@ void finalize(State& state, evmc_revision rev, const address& coinbase, for (const auto& withdrawal : withdrawals) state.touch(withdrawal.recipient).balance += withdrawal.get_amount(); - // Delete potentially empty block reward recipients. - if (rev >= EVMC_SPURIOUS_DRAGON) - delete_empty_accounts(state); + return state.build_diff(rev); } -std::variant transition(State& state, const BlockInfo& block, - const Transaction& tx, evmc_revision rev, evmc::VM& vm, int64_t block_gas_left, - int64_t blob_gas_left) +std::variant transition(const StateView& state_view, + const BlockInfo& block, const Transaction& tx, evmc_revision rev, evmc::VM& vm, + int64_t block_gas_left, int64_t blob_gas_left) { + State state{state_view}; auto* sender_ptr = state.find(tx.sender); // Validate transaction. The validation needs the sender account, so in case @@ -407,10 +467,9 @@ std::variant transition(State& state, const host.access_account(*tx.to); for (const auto& [a, storage_keys] : tx.access_list) { - host.access_account(a); // TODO: Return account ref. - auto& storage = state.get(a).storage; + host.access_account(a); for (const auto& key : storage_keys) - storage[key].access_status = EVMC_ACCESS_WARM; + state.get_storage(a, key).access_status = EVMC_ACCESS_WARM; } // EIP-3651: Warm COINBASE. // This may create an empty coinbase account. The account cannot be created unconditionally @@ -431,37 +490,13 @@ std::variant transition(State& state, const sender_acc.balance += tx_max_cost - gas_used * effective_gas_price; state.touch(block.coinbase).balance += gas_used * priority_gas_price; - // Apply destructs. - std::erase_if(state.get_accounts(), - [](const std::pair& p) noexcept { return p.second.destructed; }); - // Cumulative gas used is unknown in this scope. - TransactionReceipt receipt{tx.type, result.status_code, gas_used, {}, host.take_logs(), {}, {}}; + TransactionReceipt receipt{ + tx.type, result.status_code, gas_used, {}, host.take_logs(), {}, state.build_diff(rev)}; // Cannot put it into constructor call because logs are std::moved from host instance. receipt.logs_bloom_filter = compute_bloom_filter(receipt.logs); - // Delete empty accounts after every transaction. This is strictly required until Byzantium - // where intermediate state root hashes are part of the transaction receipt. - // TODO: Consider limiting this only to Spurious Dragon. - if (rev >= EVMC_SPURIOUS_DRAGON) - delete_empty_accounts(state); - - // Post-transaction clean-up. - // - Set accounts and their storage access status to cold. - // - Clear the "just created" account flag. - for (auto& [addr, acc] : state.get_accounts()) - { - acc.transient_storage.clear(); - acc.access_status = EVMC_ACCESS_COLD; - acc.just_created = false; - for (auto& [key, val] : acc.storage) - { - val.access_status = EVMC_ACCESS_COLD; - val.original = val.current; - } - } - return receipt; } } // namespace evmone::state diff --git a/test/state/state.hpp b/test/state/state.hpp index 16d31b299b..edeef64583 100644 --- a/test/state/state.hpp +++ b/test/state/state.hpp @@ -9,11 +9,14 @@ #include "bloom_filter.hpp" #include "errors.hpp" #include "hash_utils.hpp" +#include "state_diff.hpp" #include "transaction.hpp" #include namespace evmone::state { +class StateView; + /// The Ethereum State: the collection of accounts mapped by their addresses. class State { @@ -61,17 +64,21 @@ class State std::variant; - std::unordered_map m_accounts; + /// The read-only view of the initial (cold) state. + const StateView& m_initial; + + /// The accounts loaded from the initial state and potentially modified. + std::unordered_map m_modified; - /// The state journal: the list of changes made in the state + /// The state journal: the list of changes made to the state /// with information how to revert them. std::vector m_journal; public: - State() = default; + explicit State(const StateView& state_view) noexcept : m_initial{state_view} {} State(const State&) = delete; - State(State&&) = default; - State& operator=(State&&) = default; + State(State&&) = delete; + State& operator=(State&&) = delete; /// Inserts the new account at the address. /// There must not exist any account under this address before. @@ -86,9 +93,11 @@ class State /// Gets an existing account or inserts new account. Account& get_or_insert(const address& addr, Account account = {}); - [[nodiscard]] auto& get_accounts() noexcept { return m_accounts; } + bytes_view get_code(const address& addr); + + StorageValue& get_storage(const address& addr, const bytes32& key); - [[nodiscard]] const auto& get_accounts() const noexcept { return m_accounts; } + StateDiff build_diff(evmc_revision rev) const; /// Returns the state journal checkpoint. It can be later used to in rollback() /// to revert changes newer than the checkpoint. @@ -125,11 +134,11 @@ class State /// /// Applies block reward to coinbase, withdrawals (post Shanghai) and deletes empty touched accounts /// (post Spurious Dragon). -void finalize(State& state, evmc_revision rev, const address& coinbase, - std::optional block_reward, std::span ommers, +[[nodiscard]] StateDiff finalize(const StateView& state_view, evmc_revision rev, + const address& coinbase, std::optional block_reward, std::span ommers, std::span withdrawals); -[[nodiscard]] std::variant transition(State& state, +[[nodiscard]] std::variant transition(const StateView& state, const BlockInfo& block, const Transaction& tx, evmc_revision rev, evmc::VM& vm, int64_t block_gas_left, int64_t blob_gas_left); diff --git a/test/state/state_diff.hpp b/test/state/state_diff.hpp new file mode 100644 index 0000000000..81fba225fc --- /dev/null +++ b/test/state/state_diff.hpp @@ -0,0 +1,51 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2024 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include + +namespace evmone::state +{ +using evmc::address; +using evmc::bytes; +using evmc::bytes32; +using intx::uint256; + +/// Collection of changes to the State +struct StateDiff +{ + struct Entry + { + /// Address of the modified account. + address addr; + + /// New nonce value. + /// TODO: Currently it is not guaranteed the value is different from the initial one. + uint64_t nonce; + + /// New balance value. + /// TODO: Currently it is not guaranteed the value is different from the initial one. + uint256 balance; + + /// New account code. If empty, it means the code has not changed. + bytes code; + + /// The list of the account's storage modifications: key => new value. + /// The value 0 means the storage entry is deleted. + std::vector> modified_storage; + }; + + /// List of modified or created accounts. + std::vector modified_accounts; + + /// List of deleted accounts. + /// + /// This list doesn't have common addresses with modified_accounts. + /// Note that from the Cancun revision (because of the modification to the SELFDESTRUCT) + /// accounts cannot be deleted and this list is always empty. + std::vector
deleted_accounts; +}; +} // namespace evmone::state diff --git a/test/state/state_view.hpp b/test/state/state_view.hpp new file mode 100644 index 0000000000..1a34dc0d3c --- /dev/null +++ b/test/state/state_view.hpp @@ -0,0 +1,33 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2024 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include + +namespace evmone::state +{ +using evmc::address; +using evmc::bytes; +using evmc::bytes32; +using intx::uint256; + +class StateView +{ +public: + struct Account + { + uint64_t nonce = 0; + uint256 balance; + bytes32 code_hash; + bool has_storage = false; + }; + + virtual ~StateView() = default; + virtual std::optional get_account(const address& addr) const noexcept = 0; + virtual bytes get_account_code(const address& addr) const noexcept = 0; + virtual bytes32 get_storage(const address& addr, const bytes32& key) const noexcept = 0; +}; +} // namespace evmone::state diff --git a/test/state/system_contracts.cpp b/test/state/system_contracts.cpp index f988e81abf..5c19675202 100644 --- a/test/state/system_contracts.cpp +++ b/test/state/system_contracts.cpp @@ -4,6 +4,7 @@ #include "system_contracts.hpp" #include "host.hpp" +#include "state_view.hpp" namespace evmone::state { @@ -35,17 +36,19 @@ static_assert(std::ranges::is_sorted(SYSTEM_CONTRACTS, } // namespace -void system_call(State& state, const BlockInfo& block, evmc_revision rev, evmc::VM& vm) +StateDiff system_call( + const StateView& state_view, const BlockInfo& block, evmc_revision rev, evmc::VM& vm) { + State state{state_view}; for (const auto& [since, addr, get_input] : SYSTEM_CONTRACTS) { if (rev < since) - return; // Because entries are ordered, there are no other contracts for this revision. + break; // Because entries are ordered, there are no other contracts for this revision. // Skip the call if the target account doesn't exist. This is by EIP-4788 spec. // > if no code exists at [address], the call must fail silently. - const auto acc = state.find(addr); - if (acc == nullptr) + const auto code = state_view.get_account_code(addr); + if (code.empty()) continue; const auto input = get_input(block); @@ -61,17 +64,10 @@ void system_call(State& state, const BlockInfo& block, evmc_revision rev, evmc:: const Transaction empty_tx{}; Host host{rev, vm, state, block, empty_tx}; - const auto& code = acc->code; [[maybe_unused]] const auto res = vm.execute(host, rev, msg, code.data(), code.size()); assert(res.status_code == EVMC_SUCCESS); - assert(acc->access_status == EVMC_ACCESS_COLD); - - // Reset storage status. - for (auto& [_, val] : acc->storage) - { - val.access_status = EVMC_ACCESS_COLD; - val.original = val.current; - } } + // TODO: Should we return empty diff if no system contracts? + return state.build_diff(rev); } } // namespace evmone::state diff --git a/test/state/system_contracts.hpp b/test/state/system_contracts.hpp index b3e6114add..6b4def136c 100644 --- a/test/state/system_contracts.hpp +++ b/test/state/system_contracts.hpp @@ -19,11 +19,13 @@ constexpr auto BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02 constexpr auto HISTORY_STORAGE_ADDRESS = 0x0aae40965e6800cd9b1f4b05ff21581047e3f91e_address; struct BlockInfo; -class State; +struct StateDiff; +class StateView; /// Performs the system call: invokes system contracts. /// /// Executes code of pre-defined accounts via pseudo-transaction from the system sender (0xff...fe). /// The sender's nonce is not increased. -void system_call(State& state, const BlockInfo& block, evmc_revision rev, evmc::VM& vm); +[[nodiscard]] StateDiff system_call( + const StateView& state_view, const BlockInfo& block, evmc_revision rev, evmc::VM& vm); } // namespace evmone::state diff --git a/test/state/test_state.cpp b/test/state/test_state.cpp index 50bac31d0e..8d84ee2277 100644 --- a/test/state/test_state.cpp +++ b/test/state/test_state.cpp @@ -7,55 +7,80 @@ namespace evmone::test { -TestState::TestState(const state::State& intra_state) +std::optional TestState::get_account(const address& addr) const noexcept { - for (const auto& [addr, acc] : intra_state.get_accounts()) - { - auto& test_acc = - (*this)[addr] = {.nonce = acc.nonce, .balance = acc.balance, .code = acc.code}; - auto& test_storage = test_acc.storage; - for (const auto& [key, value] : acc.storage) - test_storage[key] = value.current; - } + const auto it = find(addr); + if (it == end()) + return std::nullopt; + + const auto& acc = it->second; + // TODO: Cache code hash for MTP root hash calculation? + return Account{acc.nonce, acc.balance, keccak256(acc.code), !acc.storage.empty()}; } -state::State TestState::to_intra_state() const +bytes TestState::get_account_code(const address& addr) const noexcept { - state::State intra_state; - for (const auto& [addr, acc] : *this) + const auto it = find(addr); + if (it == end()) + return {}; + + return it->second.code; +} + +void TestState::apply(const state::StateDiff& diff) +{ + for (const auto& m : diff.modified_accounts) { - auto& intra_acc = intra_state.insert( - addr, {.nonce = acc.nonce, .balance = acc.balance, .code = acc.code}); - auto& storage = intra_acc.storage; - for (const auto& [key, value] : acc.storage) - storage[key] = {.current = value, .original = value}; + auto& a = (*this)[m.addr]; + a.nonce = m.nonce; + a.balance = m.balance; + if (!m.code.empty()) + a.code = m.code; // TODO: Consider taking rvalue ref to avoid code copy. + for (const auto& [k, v] : m.modified_storage) + { + if (v) + a.storage.insert_or_assign(k, v); + else + a.storage.erase(k); + } } - return intra_state; + + for (const auto& addr : diff.deleted_accounts) + erase(addr); +} + +bytes32 TestState::get_storage(const address& addr, const bytes32& key) const noexcept +{ + const auto ait = find(addr); + if (ait == end()) // TODO: When? + return bytes32{}; + const auto& storage = ait->second.storage; + const auto it = storage.find(key); + return (it != storage.end()) ? it->second : bytes32{}; } [[nodiscard]] std::variant transition(TestState& state, const state::BlockInfo& block, const state::Transaction& tx, evmc_revision rev, evmc::VM& vm, int64_t block_gas_left, int64_t blob_gas_left) { - auto intra_state = state.to_intra_state(); - auto res = state::transition(intra_state, block, tx, rev, vm, block_gas_left, blob_gas_left); - state = TestState{intra_state}; - return res; + const auto result_or_error = + state::transition(state, block, tx, rev, vm, block_gas_left, blob_gas_left); + if (const auto result = get_if(&result_or_error)) + state.apply(result->state_diff); + return result_or_error; } void finalize(TestState& state, evmc_revision rev, const address& coinbase, std::optional block_reward, std::span ommers, std::span withdrawals) { - auto intra_state = state.to_intra_state(); - state::finalize(intra_state, rev, coinbase, block_reward, ommers, withdrawals); - state = TestState{intra_state}; + const auto diff = state::finalize(state, rev, coinbase, block_reward, ommers, withdrawals); + state.apply(diff); } void system_call(TestState& state, const state::BlockInfo& block, evmc_revision rev, evmc::VM& vm) { - auto intra_state = state.to_intra_state(); - state::system_call(intra_state, block, rev, vm); - state = TestState{intra_state}; + const auto diff = state::system_call(state, block, rev, vm); + state.apply(diff); } } // namespace evmone::test diff --git a/test/state/test_state.hpp b/test/state/test_state.hpp index 3bac733c57..0fbb17f6bb 100644 --- a/test/state/test_state.hpp +++ b/test/state/test_state.hpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#include "state_view.hpp" #include #include #include @@ -15,10 +16,10 @@ namespace state { struct BlockInfo; struct Ommer; +struct StateDiff; struct Transaction; struct TransactionReceipt; struct Withdrawal; -class State; } // namespace state namespace test @@ -44,11 +45,15 @@ struct TestAccount /// This is a simplified variant of state::State: /// it hides some details related to transaction execution (e.g. original storage values) /// and is also easier to work with in tests. -class TestState : public std::map +class TestState : public state::StateView, public std::map { public: using map::map; + std::optional get_account(const address& addr) const noexcept override; + bytes get_account_code(const address& addr) const noexcept override; + bytes32 get_storage(const address& addr, const bytes32& key) const noexcept override; + /// Inserts new account to the state. /// /// This method is for compatibility with state::State::insert(). @@ -63,11 +68,8 @@ class TestState : public std::map /// TODO: deprecate this method. TestAccount& get(const address& addr) { return (*this)[addr]; } - /// Converts the intra state to TestState. - explicit TestState(const state::State& intra_state); - - /// Converts the TestState to intra state for transaction execution. - [[nodiscard]] state::State to_intra_state() const; + /// Apply the state changes. + void apply(const state::StateDiff& diff); }; /// Wrapping of state::transition() which operates on TestState. diff --git a/test/state/transaction.hpp b/test/state/transaction.hpp index 592981139e..30b352aa7b 100644 --- a/test/state/transaction.hpp +++ b/test/state/transaction.hpp @@ -5,6 +5,7 @@ #pragma once #include "bloom_filter.hpp" +#include "state_diff.hpp" #include #include #include @@ -90,6 +91,7 @@ struct TransactionReceipt int64_t cumulative_gas_used = 0; std::vector logs; BloomFilter logs_bloom_filter; + StateDiff state_diff; /// Root hash of the state after this transaction. Used only in old pre-Byzantium transactions. std::optional post_state;