Skip to content

feat(spec-specs, tests): merge EIP-8037 to forks/amsterdam#2901

Open
spencer-tb wants to merge 135 commits into
ethereum:forks/amsterdamfrom
spencer-tb:eips/amsterdam/eip-8037
Open

feat(spec-specs, tests): merge EIP-8037 to forks/amsterdam#2901
spencer-tb wants to merge 135 commits into
ethereum:forks/amsterdamfrom
spencer-tb:eips/amsterdam/eip-8037

Conversation

@spencer-tb
Copy link
Copy Markdown
Contributor

@spencer-tb spencer-tb commented May 22, 2026

🗒️ Description

This PR merges EIP-8037 into forks/amsterdam including framework, specs and all test changes.

Review Plan

🔗 Related Issues or PRs

N/A.

✅ 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).

spencer-tb and others added 30 commits May 21, 2026 12:59
…se (ethereum#2363)

* feat(spec-specs): update EIP-8037 to match latest spec revision

* feat(tests): add EIP-8037 state creation gas cost increase tests
Co-authored-by: Ben Adams <thundercat@illyriad.co.uk>
Add sstore_state_gas(), code_deposit_state_gas(), and create_state_gas()
calculator methods to Fork. Replace Spec constants and manual gas
arithmetic across all EIP-8037 tests with dynamic fork method calls.
Remove redundant Op.STOP, hardcoded numbers from docstrings, and add
fork: Fork parameter to all test functions that use fork methods.
Test CREATE with max initcode size using proper regular/state gas
split via the reservoir. Verifies gas boundary behavior with EIP-8037
two-dimensional metering.
Align EIP-8037 gas constant references with upstream renames:
- GAS_STORAGE_UPDATE → GAS_COLD_STORAGE_WRITE
- GAS_COLD_SLOAD → GAS_COLD_STORAGE_ACCESS
- GAS_WARM_ACCOUNT_ACCESS → GAS_WARM_ACCESS

Bump gas_limit on tests added to upstream after EIP-8037 branched,
which now need extra gas for EIP-8037 state gas costs.
… format runs in withdrawal request contract tests (ethereum#2532)

* fix(tests): prevent tx_gas_limit double-accumulation across fixture format runs in withdrawal request contract tests

* fix: mypy

* fix: ruff

* fix: ruff

* chore(tests): refactor fix to fork-aware transactions to prevent mutation

* chore(test): add a warning to all tests that could mutate vars; address in later PR

Add a lightweight repr-based snapshot hook to the filler plugin that warns whenever
any ``pytest.param`` value is mutated during a test run.

A subsequent PR could address this by returning values instead of mutating, then
flipping the hook to a hard failure.

---------

Co-authored-by: fselmo <fselmo2@gmail.com>
… gas validity test (ethereum#2583)

Co-authored-by: Stefan <22667037+qu0b@users.noreply.github.com>
Conditionally increase tx gas_limit (and env gas_limit where needed)
when fork >= Amsterdam to account for EIP-8037 state creation gas costs.

137 files, 9 with env gas_limit bumps. Headroom: 2,000,000.
Move MAX_CODE_SIZE check before gas charges, charge keccak hash
cost (regular gas) before code deposit state gas, and add tests
for over-max code size and reservoir preservation after OOG.
On CREATE/CREATE2 address collision the 63/64 gas allocation is
burned but was not added to regular_gas_used, leaving it invisible
to 2D block gas accounting. Per EIP-684 collision behaves as an
immediate exceptional halt, so the burned gas belongs in the regular
dimension.
…and subcall pattern

Co-authored-by: Mario Vega <marioevz@gmail.com>
The static test skip list and conftest were a temporary workaround for
EIP-8037 gas failures. The ported static tests in tests/ported_static/
replace this approach; failures are tracked in ethereum#2601.
…thereum#2603)

* fix(execute): use --sender-fund-refund-gas-limit for all funding txs

On EIP-8037 networks, simple value transfers to new accounts require
more than 21000 gas due to GAS_NEW_ACCOUNT state gas (112 * cpsb).
The default Transaction gas_limit of 21000 causes 'intrinsic gas too
low' errors during test setup.

Changes:
- contracts.py: Use 200000 gas for deterministic factory deployer funding
- pre_alloc.py: Pass sender_fund_refund_gas_limit to Alloc and use it
  for simple EOA funding transactions

Usage: --sender-fund-refund-gas-limit 200000

* chore: additional fixes for execute remote funds w/ higher gas limits

* fix: bump all gas limits to 200k for EIP-8037 state creation costs

- sender.py: bump --sender-fund-refund-gas-limit default 21000 → 200000
- pre_alloc.py: bump funding_gas_limit default 21000 → 200000
- pre_alloc.py: use configurable gas limit for per-test refund txs
- execute_recover.py: bump recovery refund gas limit 21000 → 200000

EIP-8037 charges GAS_NEW_ACCOUNT (112 × cost_per_state_byte = 131488)
for transfers to new accounts, making 21000 gas insufficient for all
funding, refund, and recovery transactions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Felipe Selmo <fselmo2@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… gas (ethereum#2595)

Co-authored-by: spencer-tb <spencer.tb@ethereum.org>
…ode size validation (ethereum#2608)

* fix(spec): charge CREATE state gas after initcode size validation

Move charge_state_gas(STATE_BYTES_PER_NEW_ACCOUNT) from create()/create2()
into generic_create(), after the MAX_INIT_CODE_SIZE check.

Previously, state gas was charged before the initcode size check, so a
CREATE with oversized initcode would persist state_gas_used equal to the
account creation state gas cost (STATE_BYTES_PER_NEW_ACCOUNT *
cost_per_state_byte) even though no account was ever created and no state
was touched.

Reported by @AskDragan (reth): ethereum#2578 (comment)

* fix(spec): check static context before gas in CREATE/CREATE2

Move the is_static check from generic_create() into create() and
create2(), before stack pops and charge_gas(). This is consistent
with SSTORE, CALL, and SELFDESTRUCT which all check static context
before any gas charging.
…evel failure (ethereum#2689)

Co-authored-by: spencer-tb <spencer.tb@ethereum.org>
@spencer-tb
Copy link
Copy Markdown
Contributor Author

Got through Osaka, Berlin, and Byzantium today. I saw a few things that need to be updated, but nothing major!

@kclowes, fixed your comments in: 1732df1, 2c7fe05 & 88055cf

…-by-design tests (ethereum#2843)

* feat(forks): add Fork.oog_budget_lift helper for EIP-8037 OoG tests

OoG-by-design ported_static tests are calibrated to land just below
a cost boundary; on Amsterdam each fresh SSTORE-set and CREATE
spills its state-gas portion back into regular gas when the per-tx
reservoir is empty. Tests that expect "N SSTOREs and M CREATEs
complete before OoG" need their gas budget lifted by
N * sstore_state_gas + M * create_state_gas to land at the same
intermediate state.

`Fork.oog_budget_lift(sstores_before_oog=N, creates_before_oog=M)`
composes the two existing state-gas helpers and returns 0 on
pre-EIP-8037 forks (where both helpers are 0), so callers can apply
it unconditionally without a fork guard.

Unit-tested on Cancun (zero) and Amsterdam (cumulative spill).

* fix(ported_static): Approach-1 fork-conditional OoG lift for stCreate*/stRevert*/stWallet* tests

Eight tests share the OoG-by-design shape `tx_gas = [oog_path, success_path]`
where the success_path budget is tuned to barely complete a single
CREATE, a CREATE plus a few SSTOREs, or a deploy chain. On Amsterdam
EIP-8037 splits NEW_ACCOUNT, fresh SSTORE-set, and code-deposit cost
into a regular portion plus a state-gas portion; with an empty
reservoir, the state-gas spills back into regular gas and breaks the
success_path budget.

Apply `Fork.oog_budget_lift` with the right (creates, sstores,
deploy_code_size) counts to lift the budget on Amsterdam only. Pre-
EIP-8037 forks return 0 from the helper, so the original budget is
preserved.

Files (skip-list entries cleared):
- stCreate2/test_create2_oo_gafter_init_code.py (-g1)
- stCreate2/test_create2_oo_gafter_init_code_returndata2.py (-g1)
- stCreateTest/test_create_oo_gafter_init_code.py (-g1)
- stCreateTest/test_create_oo_gafter_init_code_returndata2.py (-g1)
- stRevertTest/test_revert_sub_call_storage_oog.py (-g1-v0)
- stRevertTest/test_revert_sub_call_storage_oog2.py (-g1-v0)
- stWalletTest/test_wallet_construction_oog.py (-g1)
- stWalletTest/test_multi_owned_construction_not_enough_gas_partial.py (-g1)

Removes 8 entries from amsterdam_skip_list.txt.

* chore(ported_static): remove stale Amsterdam skip entries for stSStoreTest 16-pair family

22 stSStoreTest files (`sstore_0to*`, `sstore_xto*` excluding `gas`,
`gas_left`, `change_from_external_call_in_init_code`) had 3 skip
entries each (`d{0,1,2}-g1`). Re-running these on Amsterdam with the
skip list disabled shows all 60 fixture variants per file already
pass without any code changes.

The post-state expectation at `g=1` is `contract_2: storage={1: 0}`
(only slot 1, asserted to be zero). On Cancun, ~14 of the 16
SSTORE pairs complete before OoG; on Amsterdam EIP-8037 the state-
gas spill cuts that to ~5 pairs. In both cases slot 1 ends at 0 and
the final `SSTORE(1, 1)` does not run, so the assertion holds on
both forks unchanged.

These were defensive skip entries from an earlier snapshot. Remove
all 66 entries; no test-file changes needed.

* fix(ported_static): lift tx_gas[0] on Amsterdam for the 2 base oo_gafter_init_code tests

Address kclowes's review on ethereum#2843: with only tx_gas[1] lifted, g=0
OoG'd at CREATE/CREATE2 dispatch on Amsterdam (NEW_ACCOUNT state-gas
spill) before init code ever ran — the assertion still held
(`NONEXISTENT` either way) but the failure mode shifted from "OoG
after init code" (the test's named scenario) to "dispatch-time OoG".

A clean closed form using `fork.oog_budget_lift(creates_before_oog=1)`
(183600) overshoots and pushes g=0 past the deploy threshold. The
Cancun 1000-gas gap between g=0 and g=1 collapses on Amsterdam:
once dispatch is cleared, the 5-byte init code is cheap enough to
always complete. Empirical binary-search on both files puts the safe
range at (166499, 167000); 166_750 sits in the middle, keeping g=0
OoG'ing at dispatch and g=1 just clearing the deploy threshold.

The two `_returndata2` variants are left unchanged — g=0's post-state
happens to land identically on both forks at the existing budget, and
adding any lift breaks them.

* fix(spec-specs, test-types): resolve lint errors from forks/amsterdam → devnets/bal/7 merge

The May-18 merge `dffc4cfea` ("Merge remote-tracking branch
'upstream/forks/amsterdam' into devnets/bal/7") had two conflict
resolutions that left `bal/7` in a state where `just static` fails:

1. `src/ethereum/forks/amsterdam/blocks.py` ended up with the EIP-7843
   `slot_number: U64` field declared twice (lines 260 and 268).
   `mypy` rejects it with `[no-redef]`; `ethereum-spec-lint` crashes
   with `ValueError: duplicate path Header.slot_number`. Remove the
   second copy.

2. `BuiltBlock.derive_engine_payload_modifier` was dropped from
   `packages/testing/src/execution_testing/specs/blockchain.py` while
   its 5 call sites in `specs/tests/test_types.py` were kept. `mypy`
   reports 5 `attr-defined` errors. Restore the staticmethod (and the
   `FixtureExecutionPayloadModifier` import it needs) from
   `forks/amsterdam`.

The instance-level wiring on `forks/amsterdam` (`BuiltBlock.rlp_modifier`
field, constructor pass-through, and `get_fixture_engine_new_payload`
call) is **not** restored — neither the linter nor the test file
references it, and `bal/7`'s current `get_fixture_engine_new_payload`
already runs without it.

This unblocks CI on every open PR against `devnets/bal/7` (including
this one).
@leolara
Copy link
Copy Markdown
Member

leolara commented Jun 2, 2026

One observation on the Amsterdam gas bumps — non-blocking, more of a
consistency note for a follow-up than anything for this PR.

The fork-conditional bumps across tests/ported_static/ are currently written
in three different idioms. All three are correct; they've just accumulated over
time and I think it's worth converging them.

1. Flat fork >= Amsterdam → hardcoded 2_100_000 — one budget applied
broadly (stRandom, stRandom2, recursive call tests):

gas_limit=2100000 if fork >= Amsterdam else 100000,

gas_limit=2600000 if fork >= Amsterdam else 600000,

2. fork.is_eip_enabled(8037) with per-test hardcoded constants — guarded by
the more precise predicate, but the headroom values are still hand-chosen:

inner_call_gas = 0x3D090
outer_call_gas = 0x55730
if fork.is_eip_enabled(8037):
inner_call_gas = 0xF4240

3. Programmatic, derived from the cost model — the bump is computed from the
framework's own EIP-8037 numbers instead of a magic literal:

tx_gas[0] = 200000
if fork.is_eip_enabled(8037):
    tx_gas[0] += 4 * Op.SSTORE(new_value=1).state_cost(fork)

if fork.is_eip_enabled(8037):
tx_gas[0] += 4 * Op.SSTORE(new_value=1).state_cost(fork)

# into a smaller regular portion plus per-storage state-gas. When
# the state-gas reservoir is empty for these tests, the full state
# gas spills into regular gas, so Op.GAS sees +20 468 per fresh
# SSTORE-set compared to Cancun (=37 568 state-gas - 17 100 base
# regular drop). Apply that delta to the 10 measurements that
# trigger a fresh-set spill; the SLOAD-only and no-op SSTORE
# entries below are unchanged.
sstore_set_delta = (
(Op.SSTORE(new_value=1).state_cost(fork) - 17100)

tx_gas = [16777216 + 14 * Op.SSTORE(new_value=1).state_cost(fork)]

state_cost(fork) returns just the EIP-8037 state-gas portion of a bytecode (0
on pre-Amsterdam forks); it sits alongside gas_cost, regular_cost, and
refund:

def state_cost(self, fork: Type[ForkOpcodeInterface]) -> int:
"""
Use a fork object to calculate the state gas used by this
bytecode.
"""
if self._state_cost_ is None or self._state_cost_fork_ != fork:
self._state_cost_fork_ = fork
opcode_state_calculator = fork.opcode_state_calculator()
self._state_cost_ = 0
for opcode in self.opcode_list:
self._state_cost_ += opcode_state_calculator(opcode)
return self._state_cost_

I'd lean toward form 3 as the target. The hardcoded numbers in forms 1 and 2
were sized against a specific per-storage state-gas value, and that constant has
already moved once during review (CPSB 1174 → 1530 in #2827) — which silently
invalidates any hardcoded headroom but leaves the state_cost(fork) form
correct. It also documents why a bump is a given size (N fresh SSTORE sets
of headroom) rather than leaving an opaque literal.

This doesn't need to hold up the PR — form 3 already exists in the tree, so the
migration is partly done.

leolara
leolara previously approved these changes Jun 2, 2026
Copy link
Copy Markdown
Member

@marioevz marioevz left a comment

Choose a reason for hiding this comment

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

Some partial comments that need to be addressed for tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py

Comment on lines +2663 to +2664
# Delegation resolved before balance check fails.
account_expectations[delegation_target] = BalAccountExpectation.empty()
Copy link
Copy Markdown
Member

@marioevz marioevz Jun 2, 2026

Choose a reason for hiding this comment

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

Suggested change
# Delegation resolved before balance check fails.
account_expectations[delegation_target] = BalAccountExpectation.empty()
# Delegation target must NOT appear in the BAL — get_account
# for code_address only runs inside generic_call, which is
# never invoked when the balance check fails, except when
# EIP-8037 is enabled since it adds a balance check before
# to charge the state gas.
if fork.is_eip_enabled(8037):
account_expectations[delegation_target] = BalAccountExpectation.empty()
else:
account_expectations[delegation_target] = None

This is indeed a spec change: https://github.com/spencer-tb/execution-specs/blob/b481c9bcb1fac2e5fdd9c700e1da7b1b7f867abe/src/ethereum/forks/amsterdam/vm/instructions/system.py#L447-L448

EIP-8037 adds a balance check to be able to charge state-gas. IMO the spec could be reworked to avoid this, but for now we can add this test change and address the spec change later, perhaps for glam-devnet-5.

Copy link
Copy Markdown
Contributor Author

@spencer-tb spencer-tb Jun 3, 2026

Choose a reason for hiding this comment

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

I went with a different fix here, as I believe this is a regression of #2473 rather than a new EIP-8037 behaviour. Commit c77d964ccf

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I reverted the #2473 change and added your change with a TODO here. For when the BAL related issues are consolidated.

spencer-tb and others added 18 commits June 3, 2026 09:55
Co-authored-by: Mario Vega <marioevz@gmail.com>
Co-authored-by: Mario Vega <marioevz@gmail.com>
… family

Co-authored-by: Mario Vega <marioevz@gmail.com>
Co-authored-by: Mario Vega <marioevz@gmail.com>
Co-authored-by: Mario Vega <marioevz@gmail.com>
Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com>
Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com>
Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com>
Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com>
Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com>
Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com>
Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com>
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.

10 participants