Skip to content

state: Replace State::find() with the nonexistent account flag - #1712

Open
chfast wants to merge 3 commits into
masterfrom
state/nonexistent-account-flag
Open

chfast wants to merge 3 commits into
masterfrom
state/nonexistent-account-flag

Conversation

@chfast

@chfast chfast commented Sep 14, 2026

Copy link
Copy Markdown
Member

Stacked on #1709 — review the top two commits.

State::find() returned nullptr for a missing account, so every caller split into two cases and nothing could be recorded about an account which does not exist. get() now always returns Account& and the nonexistent flag is the answer to "does this account exist?".

What falls out of that:

  • The miss is cached, so the initial state is queried once per address rather than once per access — the old find() TODO.
  • A cold access warms the account in place, without creating it. The address no longer reaches StateDiff at all, which removes the leftover deleted_accounts entry that state: Restore account nonexistence when a cold access is reverted #1709 only removes for the reverted case.
  • journal_new_account is gone. It hardcoded access_status = COLD, which is wrong once a nonexistent account can already be warm; journal_account_flags snapshots the real flags and reverts nonexistent like any other flag.
  • insert() materializes in place, carrying over access_status, storage and transient_storage. Replacing the node instead discards the storage slots the tx access list warmed on it (state.cpp warms slots right after access_account) — measured as +2100 gas in create2_rollback_preserves_access_list_slot_warmth.
  • Rolling back to nonexistent clears the value fields a reverted CREATE used to leave behind. They were unreachable through find(), but are readable through the returned reference, so without this EXTCODEHASH/EXTCODESIZE/BALANCE on a reverted-create address would return the deployed contract's data.

Expectation::diff_excludes

The second commit lets state_transition tests inspect the diff. An account which has never existed can appear in StateDiff as modified or deleted without changing the state the diff produces, so the post state alone cannot tell — the fixture applies the diff and compares the result, making a no-op delete invisible.

diff_excludes lists addresses which must appear in neither list. This supersedes the standalone state_diff_test.cpp added in #1709, which is deleted here in favour of a declarative test:

TEST_F(state_transition, access_nonexistent_revert)
{
    static constexpr auto ABSENT = 0xab5e17_address;
    tx.to = To;
    pre[*tx.to] = {.code = push(ABSENT) + OP_BALANCE + OP_POP + revert(0, 0)};

    expect.status = EVMC_REVERT;
    expect.post[*tx.to].exists = true;
    expect.post[ABSENT].exists = false;
    expect.diff_excludes = {ABSENT};
}

A blanket "every deleted account existed in pre" fixture invariant is not available instead: an account created and emptied within the transaction legitimately violates it (the massdestruct tests).

Verification

1292 unit tests with -DASSERTIONS=ON, Release build clean, EEST tests-2001 8172 state and 8612 blockchain. The declarative test was mutation-tested: restoring materialize-before-journal makes it fail with 0x…ab5e17: deleted in the state diff.

Host::access_account warms up an account which does not exist by inserting
a temporary empty one, but journaled its flags after the insertion, recording
the account as already existing. A revert therefore left it in the state and
build_diff() reported it in StateDiff::deleted_accounts, deleting an account
which has never existed. Applying such a diff is a no-op, so the produced
state has always been correct.

Journal the new account instead, as create() and execute_message() do.
find() returned nullptr for a missing account, so callers handled absence
separately and nothing could be recorded about an account which does not
exist. Return a reference always and let the nonexistent flag answer "does
this account exist?". The miss is then cached, a cold access warms the account
without creating it, and journal_account_flags reverts the flag like any
other, replacing journal_new_account. Reverting to nonexistent also clears the
value fields left by a reverted CREATE, now reachable through the reference.
An account which has never existed can appear in StateDiff as modified or
deleted without changing the state the diff produces, so the post state alone
cannot tell. Add Expectation::diff_excludes listing addresses which must not
appear in the diff at all, and use it for the reverted cold access.
@codspeed

codspeed Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 129 untouched benchmarks


Comparing state/nonexistent-account-flag (b10da1b) with state/access-account-journal-order (e0c04e5)

Open in CodSpeed

@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.03%. Comparing base (e0c04e5) to head (b10da1b).

Additional details and impacted files
@@                         Coverage Diff                         @@
##           state/access-account-journal-order    #1712   +/-   ##
===================================================================
  Coverage                               98.02%   98.03%           
===================================================================
  Files                                     180      179    -1     
  Lines                                   16379    16368   -11     
  Branches                                 3769     3760    -9     
===================================================================
- Hits                                    16056    16046   -10     
  Misses                                    243      243           
+ Partials                                   80       79    -1     
Flag Coverage Δ
eest-develop 82.79% <100.00%> (-0.02%) ⬇️
eest-develop-gmp 26.17% <81.48%> (-0.02%) ⬇️
eest-legacy 17.15% <77.77%> (-0.07%) ⬇️
eest-libsecp256k1 28.44% <81.48%> (-0.02%) ⬇️
eest-stable 82.76% <100.00%> (-0.02%) ⬇️
evmone-unittests 94.35% <98.76%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
core 96.21% <100.00%> (+0.01%) ⬆️
tooling 94.06% <ø> (ø)
tests 99.81% <100.00%> (-0.01%) ⬇️
Files with missing lines Coverage Δ
test/state/host.cpp 100.00% <100.00%> (+0.45%) ⬆️
test/state/state.cpp 99.69% <100.00%> (+<0.01%) ⬆️
test/state/state.hpp 100.00% <ø> (ø)
test/unittests/state_transition.cpp 98.92% <100.00%> (+0.10%) ⬆️
test/unittests/state_transition_touch_test.cpp 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Base automatically changed from state/access-account-journal-order to master September 15, 2026 07:32

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant