Skip to content

fix(tests): measure on-chain SSTORE regular gas and warm the warm rows#7

Merged
marioevz merged 3 commits into
CPerezz:eip-8038-testsfrom
danceratopz:8038-fixup-test_sstore_regular_gas
Jun 23, 2026
Merged

fix(tests): measure on-chain SSTORE regular gas and warm the warm rows#7
marioevz merged 3 commits into
CPerezz:eip-8038-testsfrom
danceratopz:8038-fixup-test_sstore_regular_gas

Conversation

@danceratopz

Copy link
Copy Markdown

Stacked on ethereum#3033. This PR targets its eip-8038-tests branch and only changes tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_gas.py, fixing the test_sstore_regular_gas case that PR adds.

Why

test_sstore_regular_gas asserts the EIP-8038 regular SSTORE schedule, but it never measured the executed cost. It only cross-checked the framework gas model against a hand-derived formula (a closed tautology, since both come from the same gas_costs) and asserted post-state {slot: new}, which is independent of the gas charged. Separately, slot warmth came only as a side effect of a prep SSTORE emitted when current != original, so the four key_warm=True rows with current == original (00x_warm, xx0, xxy_warm, xxx) ran cold on-chain and produced fixtures identical to their cold siblings. The warm storage-access price was never exercised in execution.

What changed

  • Wrap the measured SSTORE in CodeGasMeasure and assert its executed regular cost equals expected_regular. The model cross-check is kept as a secondary guard.
  • Warm the key_warm rows via the transaction access list instead of relying on an incidental prep write.
  • Leave gas_limit unset so the EIP-8037 state-gas reservoir lands above the EIP-7825 cap and Op.GAS measures regular gas only. The prior gas_limit=1_000_000 was below the cap, which zeroes the reservoir and spills the zero-to-nonzero rows' state gas (97920) into the measurement; with it unset the reservoir absorbs the state gas, matching test_create_regular_gas.
  • Clarify the row ids. Every row now carries an explicit _cold / _warm / _dirty suffix plus a legend. _dirty (current != original) reuses the clean/dirty vocabulary already documented in tests/benchmark/stateful/bloatnet/test_single_opcode.py.

After the fix the warm and cold rows genuinely diverge. 00x_warm and xxy_warm measure 10100 against 13000 for their cold siblings, xxx_warm measures 100 against 3000, and xx0_warm is a real warm clear at 10100.

Verification

  • uv run fill --fork Amsterdam tests/amsterdam/eip8038_state_access_gas_cost_increase/test_sstore_gas.py passes (30/30).
  • just static is clean.

Per-row facts from a trace dump (--traces --evm-dump-dir). Across all rows measured reg equals stored@0 equals expected. The 97920 measured state on the 00x rows is charged on the state dimension and excluded from measured reg (no spill), and the _dirty rows show their unmeasured prep write.

id prep measured reg measured state stored@0 expected
00x_cold none 13000 97920 13000 13000
00x_warm none 10100 97920 10100 10100
0x0_dirty 10100 100 0 100 100
xx0_warm none 10100 0 10100 10100
xxy_cold none 13000 0 13000 13000
xxy_warm none 10100 0 10100 10100
xyz_dirty 10100 100 0 100 100
xyx_dirty 10100 100 0 100 100
xxx_warm none 100 0 100 100
xxx_cold none 3000 0 3000 3000

🔗 Related Issues or PRs

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

`test_sstore_regular_gas` never measured the executed `SSTORE` cost. It
only cross-checked the gas model against a hand-derived formula and
asserted post-state `{slot: new}`. Warmth came solely from the prep
`SSTORE` (emitted when `current != original`), so the four `key_warm`
rows with `current == original` ran cold on-chain, producing fixtures
identical to their cold siblings.

Wrap the measured write in `CodeGasMeasure` and assert its stored cost
equals `expected_regular`, and warm the `key_warm` rows via the access
list. Leave `gas_limit` unset so the state-gas reservoir lands above the
EIP-7825 cap and `Op.GAS` measures regular gas only. Correct the false
comment and the function docstring.
Every `test_sstore_regular_gas` row now carries an explicit suffix
instead of hiding warmth in the parameter tuple. Clean rows
(current == original) are `_cold` or `_warm`; dirty rows
(current != original) are `_dirty`, since a prior in-frame SSTORE
necessarily warms the slot. Add a legend defining the scheme.

The clean/dirty terms are existing repo vocabulary;
`tests/benchmark/stateful/bloatnet/test_single_opcode.py` documents the
same EIP-2200 distinction, "clean (original==current) vs dirty
(original!=current)".

@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.

Two comments on these changes, thanks!

measured = measured_bare(data_slot, new)

# Cross-check the oracle agrees with the hand-derived formula.
assert measured.regular_cost(fork) - 2 * very_low == expected_regular

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This kinda defeats the purpose of gas_cost(fork)/regular_cost(fork)/state_cost(fork), and forces us to have to update this particular test if the gas price formula changes in the future.

We should simply:

expected_regular = measured_bare.regular_cost(fork)

and remove other gas calculations from this function.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

My comment should really be in the expected_regular = access_cost + write_cost line, but it also applies here that we should not assert the cost of the push opcodes via 2 * very_low either.

# Warm the slot for ``key_warm`` rows that have no prep to warm it;
# harmless for prep rows (warmth is set membership). Built after
# ``deploy_contract`` so the address exists.
access_list = (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Great idea, nice.

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