Skip to content

Commit

Permalink
state: Revert state changes with journal
Browse files Browse the repository at this point in the history
Implement State Journal: a list of changes made to the state
and implement state reverting as undoing the changes until the given
journal checkpoint.
  • Loading branch information
chfast committed Dec 13, 2023
1 parent d13bf04 commit c5e7dd2
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 12 deletions.
58 changes: 47 additions & 11 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,6 +306,8 @@ 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;
}
Expand All @@ -294,8 +326,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 journal_checkpoint = m_state.get_journal_checkpoint();

auto result = execute_message(*msg);

Expand All @@ -306,7 +338,7 @@ 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_state.journal_rollback(journal_checkpoint);
m_logs.resize(logs_snapshot);

// The 0x03 quirk: the touch on this address is never reverted.
Expand Down Expand Up @@ -365,18 +397,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 +424,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
67 changes: 67 additions & 0 deletions 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::journal_rollback(size_t checkpoint) noexcept
{
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
90 changes: 89 additions & 1 deletion test/state/state.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,59 @@

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;

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 +106,52 @@ 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});
}

size_t get_journal_checkpoint() const noexcept { return m_journal.size(); }

void journal_rollback(size_t checkpoint) noexcept;
};

struct Ommer
Expand Down

0 comments on commit c5e7dd2

Please sign in to comment.