Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 36 additions & 46 deletions test/state/host.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ namespace evmone::state
{
bool Host::account_exists(const address& addr) const noexcept
{
const auto* const acc = m_state.find(addr);
return acc != nullptr && (m_rev < EVMC_SPURIOUS_DRAGON || !acc->is_empty());
const auto& acc = m_state.get(addr);
return !acc.nonexistent && (m_rev < EVMC_SPURIOUS_DRAGON || !acc.is_empty());
}

bytes32 Host::get_storage(const address& addr, const bytes32& key) const noexcept
Expand Down Expand Up @@ -69,14 +69,12 @@ evmc_storage_status Host::set_storage(

uint256be Host::get_balance(const address& addr) const noexcept
{
const auto* const acc = m_state.find(addr);
return (acc != nullptr) ? intx::be::store<uint256be>(acc->balance) : uint256be{};
return intx::be::store<uint256be>(m_state.get(addr).balance);
}

uint64_t Host::get_nonce(const address& addr) const noexcept
{
const auto* const acc = m_state.find(addr);
return (acc != nullptr) ? acc->nonce : 0;
return m_state.get(addr).nonce;
}

namespace
Expand Down Expand Up @@ -111,11 +109,11 @@ size_t Host::get_code_size(const address& addr) const noexcept

bytes32 Host::get_code_hash(const address& addr) const noexcept
{
const auto* const acc = m_state.find(addr);
if (acc == nullptr || acc->is_empty())
const auto& acc = m_state.get(addr);
if (acc.is_empty())
return {};

return acc->code_hash;
return acc.code_hash;
}

size_t Host::copy_code(const address& addr, size_t code_offset, uint8_t* buffer_data,
Expand All @@ -130,8 +128,8 @@ size_t Host::copy_code(const address& addr, size_t code_offset, uint8_t* buffer_

bool Host::selfdestruct(const address& addr, const address& beneficiary) noexcept
{
if (m_state.find(beneficiary) == nullptr)
m_state.journal_new_account(beneficiary);
if (auto& b = m_state.get(beneficiary); b.nonexistent)
m_state.journal_account_flags(beneficiary, b);
auto& acc = m_state.get(addr);
const auto balance = acc.balance;
auto& beneficiary_acc = m_state.touch(beneficiary);
Expand Down Expand Up @@ -181,35 +179,33 @@ evmc::Result Host::create(const evmc_message& msg) noexcept
assert(msg.kind == EVMC_CREATE || msg.kind == EVMC_CREATE2);
assert(msg.recipient != address{}); // Must be computed already.

// TODO: find()+insert() probes m_modified twice for a new recipient.
auto* new_acc = m_state.find(msg.recipient);
if (new_acc == nullptr)
auto& new_acc = m_state.get(msg.recipient);
if (new_acc.nonexistent)
{
new_acc = &m_state.insert(msg.recipient);
m_state.journal_new_account(msg.recipient);
m_state.journal_account_flags(msg.recipient, new_acc);
m_state.insert(msg.recipient);
}
else
{
if (is_create_collision(*new_acc))
if (is_create_collision(new_acc))
return evmc::Result{EVMC_FAILURE}; // TODO: Add EVMC errors for creation failures.
m_state.journal_create(msg.recipient);
}

assert(new_acc != nullptr);
assert(new_acc->nonce == 0);
assert(new_acc.nonce == 0);

if (m_rev >= EVMC_SPURIOUS_DRAGON)
new_acc->nonce = 1; // No need to journal: create revert will 0 the nonce.
new_acc.nonce = 1; // No need to journal: create revert will 0 the nonce.

new_acc->just_created = true;
new_acc.just_created = true;

auto& sender_acc = m_state.get(msg.sender); // TODO: Duplicated account lookup.
const auto value = intx::be::load<intx::uint256>(msg.value);
assert(sender_acc.balance >= value && "EVM must guarantee balance");
m_state.journal_balance_change(msg.sender, sender_acc.balance);
m_state.journal_balance_change(msg.recipient, new_acc->balance);
m_state.journal_balance_change(msg.recipient, new_acc.balance);
sender_acc.balance -= value;
new_acc->balance += value; // The new account may be prefunded.
new_acc.balance += value; // The new account may be prefunded.

if (m_rev >= EVMC_AMSTERDAM)
emit_transfer_log(m_logs, msg.sender, msg.recipient, value);
Expand Down Expand Up @@ -247,9 +243,9 @@ evmc::Result Host::create(const evmc_message& msg) noexcept

if (!code.empty())
{
new_acc->code_hash = keccak256(code);
new_acc->code = code;
new_acc->code_changed = true;
new_acc.code_hash = keccak256(code);
new_acc.code = code;
new_acc.code_changed = true;
}

return evmc::Result{result.status_code, gas_left, result.gas_refund};
Expand All @@ -262,9 +258,9 @@ evmc::Result Host::execute_message(const evmc_message& msg) noexcept

if (msg.kind == EVMC_CALL)
{
auto* recipient_acc = m_state.find(msg.recipient);
if (recipient_acc == nullptr)
m_state.journal_new_account(msg.recipient);
auto& recipient_acc = m_state.get(msg.recipient);
if (recipient_acc.nonexistent)
m_state.journal_account_flags(msg.recipient, recipient_acc);
// TODO: Both branches will insert new account so better to do it in common path.

if (evmc::is_zero(msg.value))
Expand All @@ -276,18 +272,18 @@ evmc::Result Host::execute_message(const evmc_message& msg) noexcept
// We skip touching if we send value, because account cannot end up empty.
// It will either have value, or code that transfers this value out, or will be
// selfdestructed anyway.
if (recipient_acc == nullptr)
recipient_acc = &m_state.insert(msg.recipient);
if (recipient_acc.nonexistent)
m_state.insert(msg.recipient);

// Transfer value: sender → recipient.
// The sender's balance is already checked therefore the sender account must exist.
const auto value = intx::be::load<intx::uint256>(msg.value);
auto& sender_acc = m_state.get(msg.sender);
assert(sender_acc.balance >= value);
m_state.journal_balance_change(msg.sender, sender_acc.balance);
m_state.journal_balance_change(msg.recipient, recipient_acc->balance);
m_state.journal_balance_change(msg.recipient, recipient_acc.balance);
sender_acc.balance -= value;
recipient_acc->balance += value;
recipient_acc.balance += value;

if (m_rev >= EVMC_AMSTERDAM)
emit_transfer_log(m_logs, msg.sender, msg.recipient, value);
Expand Down Expand Up @@ -330,8 +326,7 @@ evmc::Result Host::call(const evmc_message& msg) noexcept
bool is_03_touched = false;
if (m_rev < EVMC_PARIS && m_rev >= EVMC_SPURIOUS_DRAGON) [[unlikely]]
{
const auto* const acc_03 = m_state.find(ADDR_03);
is_03_touched = acc_03 != nullptr && acc_03->erase_if_empty;
is_03_touched = m_state.get(ADDR_03).erase_if_empty;
}

// Revert.
Expand Down Expand Up @@ -386,21 +381,16 @@ evmc_access_status Host::access_account(const address& addr) noexcept
if (m_rev < EVMC_BERLIN)
return EVMC_ACCESS_COLD; // Ignore before Berlin.

auto* acc = m_state.find(addr);

if (acc != nullptr && acc->access_status == EVMC_ACCESS_WARM)
return EVMC_ACCESS_WARM;

if (is_precompile(m_rev, addr)) // Precompiles are always warm. Don't insert to state.
return EVMC_ACCESS_WARM;

// TODO: On a modified-set miss the account is looked up twice. This can be improved with
// a try_emplace-like API, but the miss happens only in ~39% of the calls on Mainnet.
if (acc == nullptr)
acc = &m_state.insert(addr, {.erase_if_empty = true});
// A nonexistent account is warmed up as is: the flag keeps it out of the state diff.
auto& acc = m_state.get(addr);
if (acc.access_status == EVMC_ACCESS_WARM)
return EVMC_ACCESS_WARM;

m_state.journal_account_flags(addr, *acc);
acc->access_status = EVMC_ACCESS_WARM;
m_state.journal_account_flags(addr, acc);
acc.access_status = EVMC_ACCESS_WARM;
return EVMC_ACCESS_COLD;
}

Expand Down
97 changes: 52 additions & 45 deletions test/state/state.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -268,53 +268,57 @@ StateDiff State::build_diff(evmc_revision rev) const
Account& State::insert(const address& addr, Account account)
{
assert(!account.nonexistent); // No need to insert nonexistent accounts.
const auto [it, inserted] = m_modified.try_emplace(addr, std::move(account));
if (!inserted)
{
assert(it->second.nonexistent); // Overwrite only nonexistent accounts.
it->second = std::move(account);
}
return it->second;
auto& acc = get(addr);
assert(acc.nonexistent); // Overwrite only nonexistent accounts.

// The account is materialized in place, because a nonexistent account may have been accessed
// already and its warm status and warm storage slots must survive (EIP-2929).
account.access_status = acc.access_status;
account.storage = std::move(acc.storage);
account.transient_storage = std::move(acc.transient_storage);
acc = std::move(account);
return acc;
}

Account* State::find(const address& addr) noexcept
Account& State::get(const address& addr) noexcept
{
// TODO: Avoid the double lookup (find+insert). Nonexistent accounts are still re-queried from
// the initial state on every call; they could be cached as nonexistent nodes.
if (const auto it = m_modified.find(addr); it != m_modified.end())
return it->second.nonexistent ? nullptr : &it->second;
{
// A nonexistent account is readable: its zero fields are what a missing account holds.
assert(!it->second.nonexistent || it->second.is_empty());
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;
}

Account& State::get(const address& addr) noexcept
{
auto acc = find(addr);
assert(acc != nullptr);
return *acc;
{
return m_modified
.try_emplace(addr, Account{.nonce = cacc->nonce,
.balance = cacc->balance,
.code_hash = cacc->code_hash,
.has_initial_storage = cacc->has_storage})
.first->second;
}
// The miss is cached, so the initial state is queried once per address rather than once
// per access.
return m_modified.try_emplace(addr, Account{.nonexistent = true}).first->second;
}

Account& State::get_or_insert(const address& addr, Account account)
{
if (const auto acc = find(addr); acc != nullptr)
return *acc;
return insert(addr, std::move(account));
auto& acc = get(addr);
if (acc.nonexistent)
return insert(addr, std::move(account));
return acc;
}

bytes_view State::get_code(const address& addr)
{
auto* a = find(addr);
if (a == nullptr)
// A nonexistent account has the empty code hash, so it needs no separate check.
auto& a = get(addr);
if (a.code_hash == Account::EMPTY_CODE_HASH)
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;
if (a.code.empty())
a.code = m_initial.get_account_code(addr);
return a.code;
}

Account& State::touch(const address& addr)
Expand Down Expand Up @@ -366,18 +370,24 @@ void State::journal_create(const address& addr)
m_journal.emplace_back(JournalCreate{{addr}});
}

void State::journal_new_account(const address& addr)
{
// Revert restores the account to "nonexistent". The other flags are irrelevant/default.
m_journal.emplace_back(JournalAccountFlags{{addr}, EVMC_ACCESS_COLD, true, false, false});
}

void State::journal_account_flags(const address& addr, const Account& acc)
{
m_journal.emplace_back(JournalAccountFlags{
{addr}, acc.access_status, acc.nonexistent, acc.destructed, acc.erase_if_empty});
}

namespace
{
/// Resets the account value fields set by a create. The balance and the storage are restored
/// by their own journal entries, which are always replayed first.
void clear_value(Account& a) noexcept
{
a.nonce = 0;
a.code_hash = Account::EMPTY_CODE_HASH;
a.code.clear();
}
} // namespace

void State::rollback(size_t checkpoint)
{
while (m_journal.size() != checkpoint)
Expand All @@ -396,17 +406,14 @@ void State::rollback(size_t checkpoint)
a.nonexistent = e.nonexistent;
a.destructed = e.destructed;
a.erase_if_empty = e.erase_if_empty;
// TODO: On restoring nonexistent (un-created create) the node keeps its
// code/storage/transient buffers until tx end; could clear them here.
if (e.nonexistent)
clear_value(a); // An account which did not exist holds nothing.
}
else if constexpr (std::is_same_v<T, JournalCreate>)
{
// Revert a create over a pre-existing account.
// TODO: Why this account is not always "touched"?
auto& a = get(e.addr);
a.nonce = 0;
a.code_hash = Account::EMPTY_CODE_HASH;
a.code.clear();
clear_value(get(e.addr));
}
else if constexpr (std::is_same_v<T, JournalStorageChange>)
{
Expand Down
10 changes: 3 additions & 7 deletions test/state/state.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,9 @@ class State
/// There must not exist any account under this address before.
Account& insert(const address& addr, Account account = {});

/// Returns the pointer to the account at the address if the account exists. Null otherwise.
Account* find(const address& addr) noexcept;

/// Gets the account at the address (the account must exist).
/// Returns the account at the address. An account absent from the state is returned as a
/// node flagged nonexistent, so the reference is always valid and the flag is the answer to
/// "does this account exist?".
Account& get(const address& addr) noexcept;

/// Gets an existing account or inserts new account.
Expand Down Expand Up @@ -118,9 +117,6 @@ class State
/// Journals a create over a pre-existing account; revert resets its nonce and code.
void journal_create(const address& addr);

/// Journals a new-account creation; revert un-creates it (restores "does not exist").
void journal_new_account(const address& addr);

void journal_account_flags(const address& addr, const Account& acc);

/// @}
Expand Down
12 changes: 12 additions & 0 deletions test/unittests/state_transition.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <test/utils/mpt_hash.hpp>
#include <test/utils/statetest.hpp>
#include <test/utils/utils.hpp>
#include <algorithm>
#include <filesystem>
#include <fstream>

Expand Down Expand Up @@ -101,6 +102,17 @@ void state_transition::TearDown()
<< "log " << i << " topics";
}
}
const auto& diff = receipt.state_diff;
for (const auto& addr : expect.diff_excludes)
{
EXPECT_TRUE(
std::ranges::find(diff.deleted_accounts, addr) == diff.deleted_accounts.end())
<< addr << ": deleted in the state diff";
EXPECT_TRUE(std::ranges::find(diff.modified_accounts, addr, &StateDiff::Entry::addr) ==
diff.modified_accounts.end())
<< addr << ": modified in the state diff";
}

// Update default expectations - valid transaction means coinbase exists unless explicitly
// requested otherwise
if (!expect.post.contains(Coinbase))
Expand Down
5 changes: 5 additions & 0 deletions test/unittests/state_transition.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ class state_transition : public ExportableFixture
/// The expected post-execution state.
std::unordered_map<address, ExpectedAccount> post;

/// Addresses which must not appear in the transaction's state diff at all, neither as
/// modified nor as deleted. Use for accounts which have never existed: applying a diff
/// entry for such an account is a no-op, so the post state alone cannot tell.
std::vector<address> diff_excludes;

std::optional<hash256> state_hash;

/// The expected EVM execution trace. If not empty transaction execution will be performed
Expand Down
Loading