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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 50 additions & 15 deletions test/state/host.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ evmc_storage_status Host::set_storage(
status = EVMC_STORAGE_MODIFIED_RESTORED; // X → Y → X
}

// In Berlin this is handled in access_storage().
if (m_rev < EVMC_BERLIN)
m_state.journal_storage_change(addr, key, storage_slot);
storage_slot.current = value; // Update current value.
return status;
}
Expand Down Expand Up @@ -100,10 +103,15 @@ 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_create(beneficiary, false);
auto& acc = m_state.get(addr);
const auto balance = acc.balance;
auto& beneficiary_acc = m_state.touch(beneficiary);

m_state.journal_balance_change(beneficiary, beneficiary_acc.balance);
m_state.journal_balance_change(addr, balance);

if (m_rev >= EVMC_CANCUN && !acc.just_created)
{
// EIP-6780:
Expand All @@ -123,7 +131,13 @@ bool Host::selfdestruct(const address& addr, const address& beneficiary) noexcep
acc.balance = 0; // Zero balance if acc is the beneficiary.

// Mark the destruction if not done already.
return !std::exchange(acc.destructed, true);
if (!acc.destructed)
{
m_state.journal_destruct(addr);
acc.destructed = true;
return true;
}
return false;
}

address compute_new_account_address(const address& sender, uint64_t sender_nonce,
Expand Down Expand Up @@ -164,6 +178,8 @@ std::optional<evmc_message> Host::prepare_message(evmc_message msg)
if (sender_nonce == Account::NonceMax)
return {}; // Light early exception.

if (msg.depth != 0)
m_state.journal_bump_nonce(msg.sender);
++sender_acc.nonce; // Bump sender nonce.

if (msg.kind == EVMC_CREATE || msg.kind == EVMC_CREATE2)
Expand Down Expand Up @@ -194,21 +210,28 @@ evmc::Result Host::create(const evmc_message& msg) noexcept
collision_acc != nullptr && (collision_acc->nonce != 0 || !collision_acc->code.empty()))
return evmc::Result{EVMC_FAILURE};

const bool exists = m_state.find(msg.recipient) != nullptr;
auto& new_acc = m_state.get_or_insert(msg.recipient);
m_state.journal_create(msg.recipient, exists);
assert(new_acc.nonce == 0);
if (m_rev >= EVMC_SPURIOUS_DRAGON)
new_acc.nonce = 1;
new_acc.nonce = 1; // No need to journal: create revert will 0 the nonce.

new_acc.just_created = true;

// Clear the new account storage, but keep the access status (from tx access list).
// This is only needed for tests and cannot happen in real networks.
for (auto& [_, v] : new_acc.storage) [[unlikely]]
for (auto& [k, v] : new_acc.storage) [[unlikely]]
{
m_state.journal_storage_change(msg.recipient, k, v);
v = StorageValue{.access_status = v.access_status};
}

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);
sender_acc.balance -= value;
new_acc.balance += value; // The new account may be prefunded.

Expand Down Expand Up @@ -266,6 +289,13 @@ evmc::Result Host::execute_message(const evmc_message& msg) noexcept
if (msg.kind == EVMC_CREATE || msg.kind == EVMC_CREATE2)
return create(msg);

if (msg.kind == EVMC_CALL)
{
const auto exists = m_state.find(msg.recipient) != nullptr;
if (!exists)
m_state.journal_create(msg.recipient, exists);
}

assert(msg.kind != EVMC_CALL || evmc::address{msg.recipient} == msg.code_address);
auto* const dst_acc =
(msg.kind == EVMC_CALL) ? &m_state.touch(msg.recipient) : m_state.find(msg.code_address);
Expand All @@ -276,15 +306,16 @@ evmc::Result Host::execute_message(const evmc_message& msg) noexcept
// The sender's balance is already checked therefore the sender account must exist.
const auto value = intx::be::load<intx::uint256>(msg.value);
assert(m_state.get(msg.sender).balance >= value);
m_state.journal_balance_change(msg.sender, m_state.get(msg.sender).balance);
m_state.journal_balance_change(msg.recipient, dst_acc->balance);
m_state.get(msg.sender).balance -= value;
dst_acc->balance += value;
}

if (is_precompile(m_rev, msg.code_address))
return call_precompile(m_rev, msg);

// Copy of the code. Revert will invalidate the account.
const auto code = dst_acc != nullptr ? dst_acc->code : bytes{};
const auto code = dst_acc != nullptr ? bytes_view{dst_acc->code} : bytes_view{};
return m_vm.execute(*this, m_rev, msg, code.data(), code.size());
}

Expand All @@ -294,8 +325,8 @@ evmc::Result Host::call(const evmc_message& orig_msg) noexcept
if (!msg.has_value())
return evmc::Result{EVMC_FAILURE, orig_msg.gas}; // Light exception.

auto state_snapshot = m_state;
const auto logs_snapshot = m_logs.size();
const auto logs_checkpoint = m_logs.size();
const auto state_checkpoint = m_state.checkpoint();

auto result = execute_message(*msg);

Expand All @@ -306,8 +337,8 @@ evmc::Result Host::call(const evmc_message& orig_msg) noexcept
const auto is_03_touched = acc_03 != nullptr && acc_03->erasable;

// Revert.
m_state = std::move(state_snapshot);
m_logs.resize(logs_snapshot);
m_state.rollback(state_checkpoint);
m_logs.resize(logs_checkpoint);

// The 0x03 quirk: the touch on this address is never reverted.
if (is_03_touched && m_rev >= EVMC_SPURIOUS_DRAGON)
Expand Down Expand Up @@ -365,18 +396,20 @@ evmc_access_status Host::access_account(const address& addr) noexcept
return EVMC_ACCESS_COLD; // Ignore before Berlin.

auto& acc = m_state.get_or_insert(addr, {.erasable = true});
const auto status = std::exchange(acc.access_status, EVMC_ACCESS_WARM);

// Overwrite status for precompiled contracts: they are always warm.
if (status == EVMC_ACCESS_COLD && is_precompile(m_rev, addr))
if (acc.access_status == EVMC_ACCESS_WARM || is_precompile(m_rev, addr))
return EVMC_ACCESS_WARM;

return status;
m_state.journal_access_account(addr);
acc.access_status = EVMC_ACCESS_WARM;
return EVMC_ACCESS_COLD;
}

evmc_access_status Host::access_storage(const address& addr, const bytes32& key) noexcept
{
return std::exchange(m_state.get(addr).storage[key].access_status, EVMC_ACCESS_WARM);
auto& storage_slot = m_state.get(addr).storage[key];
m_state.journal_storage_change(addr, key, storage_slot);
return std::exchange(storage_slot.access_status, EVMC_ACCESS_WARM);
}


Expand All @@ -390,6 +423,8 @@ evmc::bytes32 Host::get_transient_storage(const address& addr, const bytes32& ke
void Host::set_transient_storage(
const address& addr, const bytes32& key, const bytes32& value) noexcept
{
m_state.get(addr).transient_storage[key] = value;
auto& slot = m_state.get(addr).transient_storage[key];
m_state.journal_transient_storage_change(addr, key, slot);
slot = value;
}
} // namespace evmone::state
69 changes: 68 additions & 1 deletion test/state/state.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,73 @@ evmc_message build_message(const Transaction& tx, int64_t execution_gas_limit) n
}
} // namespace

void State::rollback(size_t checkpoint)
{
while (m_journal.size() != checkpoint)
{
std::visit(
[this](const auto& e) {
using T = std::decay_t<decltype(e)>;
if constexpr (std::is_same_v<T, JournalNonceBump>)
{
get(e.addr).nonce -= 1;
}
else if constexpr (std::is_same_v<T, JournalTouched>)
{
get(e.addr).erasable = false;
}
else if constexpr (std::is_same_v<T, JournalDestruct>)
{
get(e.addr).destructed = false;
}
else if constexpr (std::is_same_v<T, JournalAccessAccount>)
{
get(e.addr).access_status = EVMC_ACCESS_COLD;
}
else if constexpr (std::is_same_v<T, JournalCreate>)
{
if (e.existed)
{
// This account is not always "touched". TODO: Why?
auto& a = get(e.addr);
a.nonce = 0;
a.code.clear();
}
else
{
// TODO: Before Spurious Dragon we don't clear empty accounts ("erasable")
// 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);
}
}
else if constexpr (std::is_same_v<T, JournalStorageChange>)
{
auto& s = get(e.addr).storage.find(e.key)->second;
s.current = e.prev_value;
s.access_status = e.prev_access_status;
}
else if constexpr (std::is_same_v<T, JournalTransientStorageChange>)
{
auto& s = get(e.addr).transient_storage.find(e.key)->second;
s = e.prev_value;
}
else if constexpr (std::is_same_v<T, JournalBalanceChange>)
{
get(e.addr).balance = e.prev_balance;
}
else
{
// TODO(C++23): Change condition to `false` once CWG2518 is in.
static_assert(std::is_void_v<T>, "unhandled journal entry type");
}
},
m_journal.back());
m_journal.pop_back();
}
}

intx::uint256 compute_blob_gas_price(uint64_t excess_blob_gas) noexcept
{
/// A helper function approximating `factor * e ** (numerator / denominator)`.
Expand Down Expand Up @@ -326,7 +393,7 @@ std::variant<TransactionReceipt, std::error_code> transition(State& state, const
gas_used -= refund;
assert(gas_used > 0);

state.get(tx.sender).balance += tx_max_cost - gas_used * effective_gas_price;
sender_acc.balance += tx_max_cost - gas_used * effective_gas_price;
state.touch(block.coinbase).balance += gas_used * priority_gas_price;

// Apply destructs.
Expand Down
95 changes: 94 additions & 1 deletion test/state/state.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,61 @@

namespace evmone::state
{
struct JournalBase
{
address addr;
};

struct JournalBalanceChange : JournalBase
{
intx::uint256 prev_balance;
};

struct JournalTouched : JournalBase
{};

struct JournalStorageChange : JournalBase
{
bytes32 key;
bytes32 prev_value;
evmc_access_status prev_access_status;
};

struct JournalTransientStorageChange : JournalBase
{
bytes32 key;
bytes32 prev_value;
};

struct JournalNonceBump : JournalBase
{};

struct JournalCreate : JournalBase
{
bool existed;
};

struct JournalDestruct : JournalBase
{};

struct JournalAccessAccount : JournalBase
{};

using JournalEntry =
std::variant<JournalBalanceChange, JournalTouched, JournalStorageChange, JournalNonceBump,
JournalCreate, JournalTransientStorageChange, JournalDestruct, JournalAccessAccount>;

/// The Ethereum State: the collection of accounts mapped by their addresses.
///
/// TODO: This class is copyable for testing. Consider making it non-copyable.
class State
{
std::unordered_map<address, Account> m_accounts;

/// The state journal: the list of changes made in the state
/// with information how to revert them.
std::vector<JournalEntry> m_journal;

public:
/// Inserts the new account at the address.
/// There must not exist any account under this address before.
Expand Down Expand Up @@ -57,13 +108,55 @@ class State
Account& touch(const address& addr)
{
auto& acc = get_or_insert(addr);
acc.erasable = true;
if (!acc.erasable)
{
acc.erasable = true;
m_journal.emplace_back(JournalTouched{addr});
}
return acc;
}

[[nodiscard]] auto& get_accounts() noexcept { return m_accounts; }

[[nodiscard]] const auto& get_accounts() const noexcept { return m_accounts; }

void journal_balance_change(const address& addr, const intx::uint256& prev_balance)
{
m_journal.emplace_back(JournalBalanceChange{{addr}, prev_balance});
}

void journal_storage_change(const address& addr, const bytes32& key, const StorageValue& value)
{
m_journal.emplace_back(
JournalStorageChange{{addr}, key, value.current, value.access_status});
}

void journal_transient_storage_change(
const address& addr, const bytes32& key, const bytes32& value)
{
m_journal.emplace_back(JournalTransientStorageChange{{addr}, key, value});
}

void journal_bump_nonce(const address& addr) { m_journal.emplace_back(JournalNonceBump{addr}); }

void journal_create(const address& addr, bool existed)
{
m_journal.emplace_back(JournalCreate{{addr}, existed});
}

void journal_destruct(const address& addr) { m_journal.emplace_back(JournalDestruct{addr}); }

void journal_access_account(const address& addr)
{
m_journal.emplace_back(JournalAccessAccount{addr});
}

/// Returns the state journal checkpoint. It can be later used to in rollback()
/// to revert changes newer than the checkpoint.
size_t checkpoint() const noexcept { return m_journal.size(); }

/// Reverts state changes made after the checkpoint.
void rollback(size_t checkpoint);
};

struct Ommer
Expand Down