Skip to content

feat(spec-specs, tests): initial implementation of EIP-8038 state-access gas cost update#2972

Draft
danceratopz wants to merge 9 commits into
ethereum:eips/amsterdam/eip-8038from
danceratopz:8038
Draft

feat(spec-specs, tests): initial implementation of EIP-8038 state-access gas cost update#2972
danceratopz wants to merge 9 commits into
ethereum:eips/amsterdam/eip-8038from
danceratopz:8038

Conversation

@danceratopz

@danceratopz danceratopz commented Jun 10, 2026

Copy link
Copy Markdown
Member

🗒️ Description

WIP: many failing tests for --fork Amsterdam outside of ./tests/amsterdam, probably worth getting #2969 in first

Implements EIP-8038: State-access gas cost update in forks/amsterdam, with provisional values (flat 3x of the legacy schedule) pending final benchmark results:

Parameter Legacy Provisional
COLD_ACCOUNT_ACCESS 2,600 7,800
ACCOUNT_WRITE 6,700 20,100
COLD_STORAGE_ACCESS 2,100 6,300
STORAGE_WRITE 2,800 8,400
WARM_ACCESS 100 300
STORAGE_CLEAR_REFUND 4,800 14,400
CREATE_ACCESS 7,000 21,000
ACCESS_LIST_STORAGE_KEY_COST 1,900 5,700
ACCESS_LIST_ADDRESS_COST 2,400 7,200

The commits are organized to be reviewed in order:

  1. refactor(specs): give TLOAD/TSTORE dedicated gas constants (unchanged at 100) so the WARM_ACCESS bump does not implicitly reprice transient storage, which is in-memory only and not covered by EIP-8038. Forward-compatible with EIP-7971.
  2. feat(specs): the repricing itself, including the new SSTORE formula (access cost always charged; STORAGE_WRITE on first change, refunded on restore), the EXT* second-read surcharge, the SELFDESTRUCT ACCOUNT_WRITE charge, CALL_VALUE = ACCOUNT_WRITE + CALL_STIPEND, and the EIP-7702 per-authorization intrinsic cost restructuring with the ACCOUNT_WRITE refund for existing authorities.
  3. refactor(test-forks): the same transient-storage decoupling in the EEST fork gas model.
  4. feat(test-forks): the repricing in the EEST fork gas model (folded into the EIP8037 mixin, which shares the gas schedule), including the EXT*/SELFDESTRUCT formula updates and a new GasCosts.ACCOUNT_WRITE field.
  5. Five chore(tests) commits updating test expectations, grouped by fix type so each can be reviewed against one rule: the EIP-8037 test spec constants; the split EIP-7702 refund channels (uncapped state refill vs capped ACCOUNT_WRITE regular refund); the new SELFDESTRUCT account-write charge; the cold no-op SSTORE cost (access only, no warm surcharge) in a hand-rolled formula; and mechanical gas budget/boundary re-tunes that the repricing pushed into OOG. Each commit body documents the rule and the re-derived values.

Where the provisional flat-3x values deviate from the EIP's own derivation formulas (CREATE_ACCESS 21,000 vs 26,400; STORAGE_CLEAR_REFUND 14,400 vs 14,112; access list costs vs "equal to cold cost"), the deviation is flagged with a comment at the constant definition; the values follow the provisional table and need confirmation before this ships.

Deliberately out of scope (tracked as EIP-8037 follow-up work, since the EIP-8037 markdown spec was updated in ethereum/EIPs@a3749180 after the Python implementation merged in #2901):

  • State refill and ACCOUNT_WRITE refund for invalid authorizations (new rule 1).
  • pre_delegated/cur_delegated delegation-indicator refill semantics, including the 0 -> a -> 0 double refill (new rule 3).
  • The account-leaf existence condition (EIP-161 emptiness vs trie membership, rule 2).

Validation: tests/amsterdam/ fills green at Amsterdam, and the EEST unit suite passes.

🔗 Related Issues or PRs

✅ Checklist

  • All: Ran fast static checks to avoid unnecessary CI fails, see also Code Standards and Enabling Pre-commit Checks:
    just static
  • All: PR title adheres to the repo standard - it will be used as the squash commit message and should start type(scope):.
  • All: Considered updating the online docs in the ./docs/ directory.
  • All: Set appropriate labels for the changes (only maintainers can apply labels).

Cute Animal Picture

image

`TLOAD` and `TSTORE` (EIP-1153) were charged via `WARM_ACCESS` because
the two costs happened to coincide at 100 gas. Give them dedicated
`OPCODE_TLOAD` and `OPCODE_TSTORE` constants so that state-access
repricing does not implicitly reprice transient storage, which is
in-memory only. Values are unchanged; EIP-7971 proposes new values for
these operations and can update the constants in place when it lands.
Reprice state-access operations per EIP-8038 with provisional values
(flat 3x of the legacy schedule, pending final benchmark results):

- `WARM_ACCESS` 100 -> 300, `COLD_ACCOUNT_ACCESS` 2600 -> 7800,
  `COLD_STORAGE_ACCESS` 2100 -> 6300.
- New `STORAGE_WRITE` (8400) replaces `COLD_STORAGE_WRITE`: `SSTORE`
  now always charges the access cost (cold or warm) and additionally
  charges `STORAGE_WRITE` on the first change to a slot; restoring the
  original value refunds `STORAGE_WRITE`. `REFUND_STORAGE_CLEAR`
  4800 -> 14400.
- New `ACCOUNT_WRITE` (20100): `CALL_VALUE` is redefined as
  `ACCOUNT_WRITE` + `CALL_STIPEND` (22400), and `SELFDESTRUCT` charges
  `ACCOUNT_WRITE` when a positive balance is sent to an empty account.
- New `CREATE_ACCESS` (21000) replaces `REGULAR_GAS_CREATE` for
  `CREATE`/`CREATE2` and contract-creation transactions.
- `EXTCODESIZE`/`EXTCODECOPY` charge an additional `WARM_ACCESS` for
  the second database read (the code).
- Access list costs: `TX_ACCESS_LIST_ADDRESS` 2400 -> 7200,
  `TX_ACCESS_LIST_STORAGE_KEY` 1900 -> 5700.
- EIP-7702 authorizations: the per-authorization intrinsic regular gas
  is `ACCOUNT_WRITE` + `REGULAR_PER_AUTH_BASE_COST` (replacing
  `PER_AUTH_BASE_COST`), and `ACCOUNT_WRITE` is refunded to the refund
  counter when the authority's account leaf already exists.

Deviations of provisional values from the EIP's derivation formulas
(`CREATE_ACCESS`, `REFUND_STORAGE_CLEAR`, access list costs) are
flagged with comments at the constant definitions.
…load

Mirror the spec-side change in the EEST fork gas model: `TLOAD` and
`TSTORE` were priced via `WARM_SLOAD` because the two costs happened to
coincide at 100 gas. Add dedicated `OPCODE_TLOAD` and `OPCODE_TSTORE`
gas cost fields, set by the EIP-1153 mixin, so that state-access
repricing does not implicitly reprice transient storage. Values are
unchanged for all forks.
…as model

Fold the EIP-8038 repricing into the EEST EIP-8037 mixin: the two EIPs
ship together in Amsterdam and share one gas schedule, and the EIP-8037
compound constants (`AUTH_PER_EMPTY_ACCOUNT`, `TX_CREATE`,
`STORAGE_SET`) are derived from the EIP-8038 parameters. Values are
provisional (flat 3x), matching the spec.

- Reprice the access, storage, call value, refund, and access list
  constants; model the new `SSTORE` formula (access cost always
  charged, `STORAGE_WRITE` on first change, refunded on restore).
- Expose `ACCOUNT_WRITE` as a new `GasCosts` field (0 before the
  repricing): tests need it to model the capped regular-gas refund for
  EIP-7702 authorities with an existing account leaf, separately from
  the uncapped state refill (`REFUND_AUTH_PER_EXISTING_ACCOUNT`).
- `EXTCODESIZE`/`EXTCODECOPY` charge an extra `WARM_ACCESS` for the
  second database read (the code).
- `SELFDESTRUCT` charges `ACCOUNT_WRITE` when a positive balance is
  sent to an empty account.
Mirror the provisional EIP-8038 values in the test-side constants:

- `REGULAR_GAS_CREATE` 9000 -> 21000: the spec's new `CREATE_ACCESS`.
- `PER_AUTH_BASE_COST` 7500 -> 33116: total regular intrinsic per
  EIP-7702 authorization, `ACCOUNT_WRITE` (20100) +
  `REGULAR_PER_AUTH_BASE_COST` (13016 = 1616 calldata + 3000 ecRecover
  + 7800 cold account read + 2 x 300 warm writes).
- `GAS_COLD_STORAGE_WRITE` 5000 -> 14700: `COLD_STORAGE_ACCESS` (6300)
  + `STORAGE_WRITE` (8400).
EIP-8038 splits the existing-authority adjustment for EIP-7702
authorizations into two channels with different cap semantics:

- The new-account *state* portion is refilled directly to the state
  gas reservoir (`REFUND_AUTH_PER_EXISTING_ACCOUNT`, uncapped), as
  before.
- The worst-case `ACCOUNT_WRITE` (20100) charged at intrinsic time is
  refunded via the regular refund counter, subject to the standard
  1/5 refund cap. The cap typically binds: per-auth gas used before
  refund is well below 5 x 20100.

Update the tests that model existing-authority refunds accordingly:

- `test_auth_refund_block_gas_accounting`: receipt
  `cumulative_gas_used` subtracts `min(gas_used_before_refund // 5,
  ACCOUNT_WRITE)` for the `existing_leaf`/`existing_delegation`
  variants. Header `gas_used` is unchanged: EIP-7778 excludes
  refund-counter refunds from block accounting.
- `test_auth_sender_billing_after_failure`: same capped term; the
  refund survives the top-level REVERT because delegations (and their
  refunds) are applied before execution.
- EIP-7976 `max_refund` fixture: existing-authority refunds now
  contribute `ACCOUNT_WRITE` to the capped refund counter under
  EIP-8037/8038 (previously nothing; pre-8037 the full
  `REFUND_AUTH_PER_EXISTING_ACCOUNT`).
- EIP-7778 `build_refund_tx`: add `ACCOUNT_WRITE` per authorization to
  the refund counter, outside the revert guard for the same reason as
  the sender-billing test.
EIP-8038 charges `ACCOUNT_WRITE` (20100) in regular gas when
SELFDESTRUCT sends a positive balance to an empty account, alongside
the EIP-8037 account-creation state gas.

- Rework `test_selfdestruct_new_beneficiary_no_regular_account_creation_cost`
  into `test_selfdestruct_new_beneficiary_account_write_cost`: the old
  premise (no regular charge, asserted via a tight budget with 20000
  slack) inverted, since `ACCOUNT_WRITE` exceeds the slack and the
  transaction ran out of gas. The reworked test tags the opcode with
  `account_new=True` so the framework folds `ACCOUNT_WRITE` plus the
  state gas into the budget, and tightens the slack to 4000, below the
  legacy 25000 minus `ACCOUNT_WRITE`: any regular draw beyond
  `ACCOUNT_WRITE` still runs out of gas, proving the charge is exactly
  the new parameter and not the legacy combined cost.
- `test_selfdestruct_in_create_tx_initcode`: the budget left only 1000
  slack above the account-creation state gas, so the new charge made
  the creation transaction halt. Tag `account_new=True` and drop the
  now redundant explicit `NEW_ACCOUNT` term.
Under the EIP-8038 SSTORE formula the access cost is always charged
and the write cost is added only on the first change to a slot. A cold
no-op store therefore costs `COLD_STORAGE_ACCESS` flat; the legacy
formula charged cold plus warm (warm was the "else" branch of the
write cost).

The halt-path gas simulation in `test_parent_state_gas_after_child_failure`
kept the legacy `+ WARM_ACCESS` for the factory's cold no-op SSTORE,
overstating the expected receipt by exactly 300. The revert variants
were unaffected: they compute the same bytecode via
`bytecode.gas_cost(fork)`, which uses the framework formula.
Adjust hardcoded gas budgets and boundary parameters that the EIP-8038
repricing pushed over their tuning point. No test semantics change;
each value is re-derived for the provisional costs:

- `test_delegatecall_child_spill_not_double_charged`: 700k -> 1M; six
  cold zero-to-nonzero SSTOREs now cost 6 x (14700 regular + 97920
  state) = 675720 before intrinsic and call overhead. Still far below
  the transaction gas cap, preserving the no-reservoir premise.
- `test_code_deposit_oog_preserves_parent_reservoir`: forwarded
  `child_gas` 1M -> 1.5M; the factory's retained 1/64 (~15k) no longer
  covered its repriced post-CREATE SSTOREs (~21k); now ~23k. The
  code-deposit OOG premise is unaffected (deposit state gas 6.27M).
- `test_failed_create_with_value_no_log` (EIP-7708): 500k -> 800k; the
  INVALID initcode burns all forwarded gas and the retained 1/64
  (~4.3k) no longer covered the cold no-op SSTORE (6300), so the
  frame halted and the expected transaction-level transfer log
  disappeared.
- `test_bal_create_oog_code_deposit` (EIP-7928): regular headroom
  500k -> 1.1M for the same retained-1/64 reason; the parent's halt
  rolled back the nonce change the block access list expects.
- EIP-7976 `intrinsic_gas_data_floor_minimum_delta`: 250 -> 11000; the
  SSTORE-clear prefix now costs 14706, and the floor must clear the
  post-refund execution cost (the fixture's own assert diagnosed
  this). The floor grows 64/byte vs 16/byte intrinsic, so ~9.7k of
  delta closes the gap; 11000 leaves margin.
- EIP-7981 floor-boundary test: minimum calldata size 1000 -> 1700
  nonzero bytes; the tripled access-list intrinsic cost (7200 + 10 x
  5700) exceeded the floor midpoint the test pins `gas_limit` to.
  Each nonzero byte closes the gap by 48 gas (floor +64, intrinsic
  +16), so the premise needs at least 1565 bytes.
@danceratopz danceratopz added A-spec-specs Area: Specification—The Ethereum specification itself (eg. `src/ethereum/*`) A-tests Area: Consensus tests. C-feat Category: an improvement or new feature labels Jun 10, 2026
@danceratopz danceratopz marked this pull request as draft June 10, 2026 14:48
@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.00%. Comparing base (84e39e1) to head (2caf201).
⚠️ Report is 2 commits behind head on eips/amsterdam/eip-8038.

Files with missing lines Patch % Lines
src/ethereum/forks/amsterdam/transactions.py 0.00% 2 Missing ⚠️
Additional details and impacted files
@@                     Coverage Diff                     @@
##           eips/amsterdam/eip-8038    #2972      +/-   ##
===========================================================
- Coverage                    90.53%   89.00%   -1.53%     
===========================================================
  Files                          535      496      -39     
  Lines                        32897    30098    -2799     
  Branches                      3021     2725     -296     
===========================================================
- Hits                         29782    26788    -2994     
- Misses                        2596     2835     +239     
+ Partials                       519      475      -44     
Flag Coverage Δ
unittests 89.00% <0.00%> (-1.53%) ⬇️

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

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@danceratopz danceratopz changed the base branch from eips/amsterdam/eip-8038 to forks/amsterdam June 10, 2026 14:54
@danceratopz danceratopz changed the base branch from forks/amsterdam to eips/amsterdam/eip-8038 June 10, 2026 14:54

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The changes here are because we split regular and state-access gas costs in
#2972 changes/3cadf8299ac440e535c1a61a2eb9b17de8b2dc77 so that we don't incorrectly price transient storage at the new values.

Comment on lines +152 to +153
OPCODE_TLOAD: int = 0
OPCODE_TSTORE: int = 0

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

As above, see 3cadf82.

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

Labels

A-spec-specs Area: Specification—The Ethereum specification itself (eg. `src/ethereum/*`) A-tests Area: Consensus tests. C-feat Category: an improvement or new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant