Skip to content

fix(tests): execute the empty-account EXTCODEHASH hash assertion#4

Merged
marioevz merged 1 commit into
CPerezz:eip-8038-testsfrom
danceratopz:eip-8038-tests-fix-test-extcodehash-empty-account
Jun 23, 2026
Merged

fix(tests): execute the empty-account EXTCODEHASH hash assertion#4
marioevz merged 1 commit into
CPerezz:eip-8038-testsfrom
danceratopz:eip-8038-tests-fix-test-extcodehash-empty-account

Conversation

@danceratopz

Copy link
Copy Markdown

🗒️ Description

test_extcodehash_empty_account asserts two things about EXTCODEHASH on an empty account: the access gas cost (no EIP-8038 code-read surcharge), and that the returned hash is 0. The gas assertion (storage slot 0) was always exercised, but the hash assertion (storage slot 1) was dead code and never ran.

The test concatenates the hash store after the gas measurement:

code = CodeGasMeasure(
    ...,
    sstore_key=gas_slot,
) + Op.SSTORE(hash_slot, Op.EXTCODEHASH(empty_addr))

CodeGasMeasure appends Op.STOP by default (stop=True), so the measure block halts before the trailing SSTORE. Slot 1 was therefore never written and kept the EVM default of 0. Because the expected value registered for slot 1 is also 0, the post-state check passed trivially: the test never observed the actual EXTCODEHASH return value. This is well hidden, since 0 is simultaneously the expected empty-account hash and the never-written storage default.

Fix

This PR passes stop=False to CodeGasMeasure so execution flows from the gas SSTORE (slot 0) into the trailing hash SSTORE (slot 1), then halts at the end of the code. Slot 1 now genuinely holds the opcode's return value, so the hash assertion has teeth: a client returning a non-zero hash for an empty account would now fail the test.

The hash store must run after the measured access, not before: in the cold case a pre-measure EXTCODEHASH(empty_addr) would warm 0xDEAD and turn the measured access warm, breaking the gas assertion. stop=False keeps the store after the measured region, where the account is already warm and only the return value matters.

This PR also adds a defensive sentinel so the assertion cannot silently regress. Before the measured region, slot 1 is poisoned with a non-zero value that the real store must overwrite back to 0. Because the empty-account hash 0 is indistinguishable from an unwritten slot, this is what makes the guard durable: if the trailing hash store is ever stranded again (for example by a future revert of stop=False), slot 1 keeps the sentinel and the test fails instead of passing vacuously. The poison is written before the first GAS reading, so it does not affect the measured gas.

Expected post-state values are unchanged. Slot 0 (gas) stays 0xbb8 (cold) / 0x64 (warm) and slot 1 stays 0; the only behavioral change is that slot 1 is now actually written. Execution traces confirm this: before the fix the trace shows a single SSTORE (slot 0) followed immediately by STOP. After the fix the trace shows the sentinel store to slot 1, then the unchanged gas store to slot 0, then the real hash store overwriting slot 1 back to 0. Reverting stop=False with the sentinel in place makes both the cold and warm cases fail, confirming the guard.

🔗 Related Issues or PRs

Fix-up to ethereum#3033 (EIP-8038 state-access gas-cost test suite), which introduced this test.

`test_extcodehash_empty_account` asserts both the surcharge-free access
gas (slot 0) and the empty-account hash (slot 1). The slot 1 assertion
was dead code: `CodeGasMeasure` appends a `STOP` by default, which halts
the frame before the trailing `Op.SSTORE(hash_slot, EXTCODEHASH(...))`,
so slot 1 was never written and kept the EVM default of `0`. Because the
expected value is also `0`, the post-state check passed without ever
observing the opcode's return value.

Pass `stop=False` so execution flows from the gas `SSTORE` into the hash
`SSTORE`, then halts at the end of the code. The hash store deliberately
stays after the measured access, where the account is already warm, so
it cannot warm `0xDEAD` and perturb the cold-case gas measurement.

Also add a defensive sentinel: slot 1 is poisoned with a non-zero value
before the measured region, which the real store must overwrite back to
`0`. The empty-account hash `0` is indistinguishable from an unwritten
slot, so this is what keeps the assertion regression-proof: if the hash
store is ever stranded again, slot 1 keeps the sentinel and the test
fails instead of passing vacuously. The poison precedes the measured
region, so it does not perturb the gas measurement.

Expected post-state is unchanged: slot 0 stays `0xbb8` (cold) / `0x64`
(warm) and slot 1 stays `0`. Verified with `--traces` (the sentinel
store runs first, the gas store is unchanged, the real hash store
overwrites slot 1 back to `0`); reverting `stop=False` with the sentinel
in place now fails both cases.

@marioevz marioevz left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@marioevz
marioevz merged commit cfd03ee into CPerezz:eip-8038-tests Jun 23, 2026
6 of 15 checks passed
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.

2 participants