Skip to content

Implement EIP-2780 - #59

Closed
gurukamath wants to merge 47 commits into
danceratopz:8038from
gurukamath:eips/amsterdam/eip-2780
Closed

gurukamath wants to merge 47 commits into
danceratopz:8038from
gurukamath:eips/amsterdam/eip-2780

Conversation

@gurukamath

Copy link
Copy Markdown

🗒️ Description

  1. Implement EIP-2780
  2. Update the testing framework
  3. Add EIP-2780 specific tests
  4. Fix broken tests
  5. Fix broken ported_static tests (This one is a bit of a claude oneshot which I have not reviewed very well). Would be happy to hear alternative approches here).

`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.
The EIP-8037 state-access repricing lifted the `STORAGE_CLEAR` prefix
(a single `SSTORE` clear) to 14706 gas, whose minimum post-refund
execution cost (28642) now exceeds the data floor (21384) produced by
sizing calldata to `intrinsic_gas_data_floor_minimum_delta = 250`. The
`execution_gas_used` fixture asserts the floor sits above that cost so
both the below-floor and above-floor refund scenarios stay reachable;
that precondition no longer held, and the fixture's own assert
diagnosed it.

Raise `intrinsic_gas_data_floor_minimum_delta` 250 -> 11000. The floor
grows 64/byte against 16/byte intrinsic, so ~9.7k of delta closes the
gap; 11000 leaves margin. No test semantics change.
EIP-7981 tripled the access-list intrinsic cost (address 2400 -> 7200,
key 1900 -> 5700).
`test_below_amsterdam_floor_with_access_list_exact_balance` pins
`gas_limit` to the midpoint between the data floor without and with the
access list, then asserts `intrinsic_regular <= gas_limit < floor`. At
1000 nonzero bytes the regular intrinsic cost (122960) now sits above
that midpoint (95880), so the premise is unsatisfiable.

Raise the smaller calldata parameter 1000 -> 1700 nonzero bytes. Each
nonzero byte adds 64 gas to the floor (hence to the midpoint) but only
16 to the intrinsic cost, closing the gap by 48/byte; the premise needs
at least 1565 bytes. No test semantics change.
`test_code_deposit_oog_preserves_parent_reservoir` forwards a fixed
`child_gas` to the factory via `Op.CALL`. After the factory's CREATE
takes 63/64, the retained 1/64 (~15k from 1M) no longer covers its
repriced post-CREATE SSTOREs (~21k), so the factory halted before its
`parent_sstore` and the expected storage write disappeared.

Raise the in-bytecode `child_gas` 1000000 -> 1500000; the factory then
retains ~23k. The code-deposit OOG premise (deploy size 4096, deposit
state gas 6.27M) is unaffected. PR ethereum#2969 auto-sizes the transaction gas
limit but not a `CALL` gas argument inside contract code, so this budget
is re-tuned by hand.
The repricing pushed the six cold zero-to-nonzero SSTOREs in
`test_delegatecall_child_spill_not_double_charged` to
6 x (14700 regular + 97920 state) = 675720, which the hardcoded
`gas_limit=700_000` no longer covered once intrinsic and call overhead
were added, so the delegatecall hit OOG and rolled back its writes.

The premise needs the top-level frame to start with no state gas
reservoir, which holds only while `tx.gas` stays at or below the
`TX_MAX_GAS_LIMIT` cap (16777216); leaving the limit unset would let
the filler assign the block gas limit and seed a large implicit
reservoir instead. Drop the hardcoded limit and set
`state_gas_reservoir=0`, which pins the gas limit to exactly the cap
with a zero reservoir. This preserves the premise with ~16M of
headroom, is robust to further repricing, and matches the reservoir
idiom used by the sibling tests in the same module.
Replace the provisional flat-3x placeholders with the final
benchmark-derived values from the updated EIP-8038 spec, across the
spec, the EEST fork gas model, and the Amsterdam tests:

- `WARM_ACCESS` stays 100, `COLD_ACCOUNT_ACCESS` 3000,
  `COLD_STORAGE_ACCESS` 3000, `STORAGE_WRITE` 10000, `ACCOUNT_WRITE`
  8000, `CALL_VALUE` 10300.
- `CREATE_ACCESS` 11000, `REFUND_STORAGE_CLEAR` 12480,
  `TX_ACCESS_LIST_ADDRESS` 3000, `TX_ACCESS_LIST_STORAGE_KEY` 3000.
- Test constants follow suit: `REGULAR_GAS_CREATE` 11000,
  `PER_AUTH_BASE_COST` 15816, `GAS_COLD_STORAGE_WRITE` 13000.

The derived parameters now match the EIP derivation formulas exactly,
so the provisional deviation comments are dropped.

Ref: ethereum/EIPs#11802
The EIP-7702 account-warming gas tests hardcoded the EIP-2929 cold and
warm account access costs (`2600` and `100`) when computing the expected
per-account access gas they assert through `SSTORE`. The EIP-8038
state-access repricing raises `COLD_ACCOUNT_ACCESS` `2600 -> 3000` at
Amsterdam while `WARM_ACCESS` stays `100`, so the measured access cost no
longer matched the baked-in expectation: a cold access followed by a warm
delegated read measured `3100` against an expected `2700`. These tests
are `valid_from` Prague, so they must keep the pre-repricing values at
Prague and Osaka while picking up the repriced values at Amsterdam.

Derive `cold_account_cost` and `warm_account_cost` from
`fork.gas_costs().COLD_ACCOUNT_ACCESS` and `.WARM_ACCESS` in:

- `test_gas.py::test_account_warming`: the cold/warm constants that feed
  `addresses_to_check`.
- `test_gas.py::test_self_set_code_cost`: the pre-authorized
  self-delegation cost, now `cold + warm` (`2700`), and the
  in-transaction case `2 x warm` (`200`).
- `test_set_code_txs.py::test_set_code_address_and_authority_warm_state`:
  the set-code-address and authority access costs, now `cold` / `warm`,
  `2 x warm` (`200`), and `warm + cold` (`2700`).

The derived values equal the previous literals at Prague and Osaka
(`cold = 2600`, `warm = 100`) and the repriced values at Amsterdam
(`cold = 3000`). No test semantics change.
`test_selfdestruct_after_create2_collision` forwards a gas budget to a
deployer contract whose colliding `CREATE2` returns `0`. A colliding
create consumes 63/64 of the forwarded gas (the account-creation state
gas is charged then refunded on collision under EIP-8037), leaving the
deployer only 1/64 to record the result with an `SSTORE`. The EIP-8038
state-access repricing raised that cold `SSTORE` to
`COLD_STORAGE_WRITE` = 13,000 at Amsterdam (5,000 before), which now
exceeds the surviving 1/64 of the old budget
(`500_000 + NEW_ACCOUNT + SSTORE state cost`), so the deployer `CALL`
hit `OutOfGasError` and `create2_call_success` read `0` instead of `1`.

Size the forwarded budget as `500_000 + 64 x COLD_STORAGE_WRITE` so the
1/64 that survives the collision burn always covers the deployer's cold
`SSTORE`. The value is fork-derived: it tracks `COLD_STORAGE_WRITE`
(5,000 at Cancun/Osaka, 13,000 at Amsterdam) and the budget is forwarded,
so the extra headroom is returned unused on the cheaper forks. No test
semantics change: the `CREATE2` still collides and returns `0`.
`test_tstore_rollback_on_failed_create` recorded each failed create's
result with `SSTORE(slot, result)` over a pre-stored `{0:1, 1:1}` to
prove the writes happened. A failed create (code-too-large ->
`OutOfGasError`) consumes its full 63/64 gas forward. Under the EIP-8037
transaction gas-limit cap the top-frame regular gas is capped at
`TX_MAX_GAS_LIMIT` (16,777,216), so two sequential failed creates leave
only `16.74M / 64^2` ~ 3,750 regular gas, while the EIP-8038 repricing
raised the warm `1->0` `SSTORE` to 10,100 (2,200 before). The second
`SSTORE` therefore hit `OutOfGasError` and the whole transaction
reverted, leaving the pre-state intact (`want {0:0, 1:0}, got
{0:1, 1:1}`). The regular budget is capped, so no `gas_limit` or
`state_gas_reservoir` choice can raise it; the test passes pre-Amsterdam
only because the cheaper `SSTORE` fits the same leftover.

Drop the result `SSTORE`s and assert in the post-state that the created
address is non-existent, checking the same rollback property without a
write in the gas-starved frame:

- The caller runs the two creates and stops; its nonce advancing to 3
  confirms both attempts executed.
- `CREATE2` targets one deterministic address for both attempts;
  `CREATE` targets the nonce-derived addresses. If `TSTORE` is rolled
  back the second create fails and nothing is deployed; if it were not,
  the second `CREATE2` would deploy a contract there.

This matches the sibling `test_contract_creation` idiom (compute the
created address, check its account). A positive control (forcing the
initcode to succeed) confirms the assertion still fails when a contract
is deployed, so the regression coverage is preserved. Verified at
Amsterdam, Cancun, and Osaka.
`stEIP2930/test_storage_costs.py` measures the regular gas consumed by
storage accesses via `Op.GAS` and stores it to a slot. The Amsterdam
enhancement added a single `sstore_set_delta` baked against provisional
EIP-8038 parameters (`state_cost(fork) - 17100`), and commit
`ef7fe9bebb8` updated those parameters to their final values, leaving
every measured access wrong: fresh `SSTORE`-sets were off by 7200, and
the cold `SLOAD`, cold value-unchanged `SSTORE`, and storage
clear/reset entries received no delta at all (off by 900, 800, and
7200/8000 respectively).

Replace the single delta with six fork-derived deltas, each computed
from the fork's own opcode gas model as
`Op.SSTORE.with_metadata(<transition>).gas_cost(fork) - <Cancun pure
cost>` (or `gas_costs().COLD_STORAGE_ACCESS - 2100` for the cold
`SLOAD`). Each is exactly 0 pre-EIP-8037 and tracks the final Amsterdam
parameters: `+88020`/`+88820` for warm/cold fresh sets, `+7200`/`+8000`
for warm/cold writes to existing slots (clear/reset), `+800` for cold
value-unchanged `SSTORE`s, and `+900` for cold `SLOAD`s. Apply each to
its matching expect entry; warm reads and no-op `SSTORE`s are
unchanged.

No test semantics change: the post-state still asserts the exact
measured gas, now correct at every fork. Fills clean across
Cancun -> Amsterdam (432 passed).
`stPreCompiledContracts/test_precomps_eip2929_cancun.py` measures the
regular gas of a `CALL` to an inactive precompile and isolates three
cost components across its `no`/`new`/`all` parametrizations. The
Amsterdam enhancement modelled only the new-account shift via a shared
`new_account_delta` (`create_state_gas() - 25000`), so after commit
`ef7fe9bebb8` the cold-account-access reprice (`COLD_ACCOUNT_ACCESS`
2600 -> 3000) was unaccounted for: the `no` entries (cold access only)
and `all` entries (new account plus cold access) were each off by 400,
while the `new` entries (new account only) were correct.

Derive a separate `cold_account_delta` (`COLD_ACCOUNT_ACCESS - 2600`,
0 pre-Amsterdam) alongside the unchanged `new_account_delta` and apply
them per class: `no` entries gain the cold delta, `new` entries keep
the account delta, and `all` entries gain both. Both deltas come from
the fork's gas model, so they stay 0 pre-EIP-8037 and track future
parameter changes.

No test semantics change: the post-state still asserts the exact
measured gas, now correct at every fork. Fills clean across
Cancun -> Amsterdam (5544 passed).
`stEIP2930/test_varied_context.py` measures gas around SSTORE, CALL, and
SELFDESTRUCT in many access-list contexts. The Amsterdam enhancement
used a single `sstore_set_delta` (`state_cost - 17100`) for both warm
and cold sets and folded it into `suicide_write_delta`; after commit
`ef7fe9bebb8` updated the final parameters, every measured access was
off. Cold SLOADs, cold value-unchanged accesses, and the SELFDESTRUCT
`ACCOUNT_WRITE`/cold-reprice components were unmodelled, and the
`*ValidGas` parametrizations forward a fixed in-bytecode gas budget that
the higher EIP-8038 SSTORE-write cost made insufficient, flipping their
success path to OOG.

Replace the single delta with per-transition deltas derived from the
fork's own gas model (warm/cold fresh sets, cold account access, cold
storage access, and the SELFDESTRUCT-to-non-alive composite of
new-account spill plus `ACCOUNT_WRITE` plus cold reprice). Apply each to
its matching expect entry: cold accesses gain the cold deltas, the
suicide-read/write entries gain the SELFDESTRUCT composite, and the cold
"invalid" suicide variants add the cold account and cold-set components
traced at their ops. Bump the two `*ValidGas` forwarded-gas budgets by
the inner SSTORE-write increase, preserving the Cancun margin so the
warm success path stays funded while the colder invalid path still OOGs.

All deltas are 0 pre-EIP-8037. No test semantics change: the post-state
still asserts the exact measured gas (or success/OOG), now correct at
every fork. Fills clean across Cancun -> Amsterdam (432 passed).
`stEIP2930/test_manual_create.py` measures the regular gas of a fresh
SSTORE-set inside a CREATE-deployed contract. The Amsterdam enhancement
used a single `sstore_set_delta` (`state_cost - 17100`) for both the
declared (warm) and undeclared (cold) sets, and commit `ef7fe9bebb8`
updated the final parameters, leaving the warm entry off by 7200 and the
cold entries off by 8000.

Replace the single delta with warm and cold fresh-set deltas derived
from the fork's own gas model (`Op.SSTORE.with_metadata(...).gas_cost(
fork) - <Cancun pure cost>`), each 0 pre-EIP-8037. Apply the warm delta
to the declared-key entry and the cold delta to the undeclared-key
entries.

No test semantics change: the post-state still asserts the exact
measured gas, now correct at every fork. Fills clean across
Cancun -> Amsterdam.
`stCreateTest/test_create_address_warm_after_fail.py` records the
measured cost of accessing a create address after a failed CREATE: a
cold account access hardcoded at 2828 (2600 plus opcode overhead).
EIP-8038 reprices a cold account access from 2600 to 3000, so commit
`ef7fe9bebb8` left every such measurement 400 low at Amsterdam.

Derive `cold_account_delta` (`COLD_ACCOUNT_ACCESS - 2600`, 0 pre-Amsterdam)
from the fork's gas model and add it to each cold-access measurement, so
the post-state tracks the final parameters and stays correct pre-fork.

No test semantics change. Fills clean across Cancun -> Amsterdam (the
successful-create parametrizations remain in the skip list).
The `stEIP150singleCodeGasPrices` ported tests measure, via `Op.GAS`, the
regular gas of EIP-2929 account and storage accesses and store it to a
slot, asserting hardcoded Cancun-era values. EIP-8038 reprices those
accesses (cold account 2600 -> 3000, cold storage 2100 -> 3000, plus a
flat `WARM_ACCESS` surcharge on the `EXTCODESIZE`/`EXTCODECOPY` code read
on both warm and cold forms, and the SSTORE state-gas spill), so the
literals were wrong at Amsterdam.

Express each measured value as `<Cancun literal> + <fork-derived delta>`,
where every delta comes from the fork's own gas model
(`gas_costs().COLD_ACCOUNT_ACCESS - 2600`, `COLD_STORAGE_ACCESS - 2100`,
`Op.EXTCODESIZE.gas_cost(fork)` etc., and `Op.SSTORE.with_metadata(...)`
for writes) and is exactly 0 pre-EIP-8037. `test_eip2929` is rewritten to
size each data index from a Cancun base table plus a fork-derived
per-operation access simulator. Banners mark each file manually-enhanced.

No test semantics change: the post-state still asserts the exact measured
gas, now correct at every fork. Fills clean across Cancun -> Amsterdam.
`test_address_opcodes` measures the regular gas of `BALANCE`,
`EXTCODESIZE`, `EXTCODEHASH`, and `EXTCODECOPY` on a first (cold or
pre-warmed) and a second (warm) access; the coinbase tests measure a CALL
that transfers value to the warm coinbase. EIP-8038 reprices the cold
account access (2600 -> 3000), adds a flat `WARM_ACCESS` surcharge to the
`EXTCODESIZE`/`EXTCODECOPY` code read, and reprices the CALL value
transfer (9000 -> 10300), so the hardcoded Cancun values were wrong at
Amsterdam.

Split the combined entries per opcode and per access and add each
opcode's own warm or cold `(Amsterdam - Cancun)` delta taken from the
fork gas model; the coinbase value uses `CALL_VALUE - 9000`. Every delta
is exactly 0 pre-EIP-8037.

No test semantics change. Fills clean across Cancun -> Amsterdam.
`stBadOpcode/test_measure_gas` and `test_operation_diff_gas` measure the
regular gas of account-touching opcodes (`EXTCODESIZE`/`EXTCODECOPY` and
the CALL family) and store it. EIP-8038 reprices the cold account access
(2600 -> 3000) and adds a flat `WARM_ACCESS` surcharge to the EXTCODE
code read, so the hardcoded values were wrong at Amsterdam.

Add each opcode's fork-derived warm or cold cost delta
(`Op.<NAME>.gas_cost(fork)` minus the Cancun cost, or
`COLD_ACCOUNT_ACCESS - 2600`), matching whether the measured iteration
runs the access warm or cold; each delta is 0 pre-EIP-8037. The CREATE
and CREATE2 parametrizations remain in the skip list.

No test semantics change. Fills clean across Cancun -> Amsterdam.
The `stEIP158Specific` CALL-to-suicide and EXTCODESIZE tests measure the
regular gas of a CALL (optionally with value) to a cold contract that may
SELFDESTRUCT, and of EXTCODESIZE on a non-existent account. EIP-8038
reprices the cold account access (2600 -> 3000), the CALL value transfer
(9000 -> 10300), the EXTCODESIZE code-read surcharge, and the SSTORE
write, so the hardcoded values were wrong at Amsterdam.

Express each measured value as the Cancun literal plus the fork-derived
sum of the components it touches (`COLD_ACCOUNT_ACCESS - 2600`,
`CALL_VALUE - 9000`, the EXTCODESIZE delta, and `Op.SSTORE.with_metadata`
deltas), each 0 pre-EIP-8037.

No test semantics change. Fills clean across Cancun -> Amsterdam.
`stEIP150Specific/test_suicide_to_existing_contract` and
`test_suicide_to_not_existing_contract` measure the regular gas of a CALL
that SELFDESTRUCTs to an existing or new beneficiary. EIP-8038 reprices
the cold account access (2600 -> 3000) on the CALL target and, for the
new-beneficiary case, the SELFDESTRUCT beneficiary, so the hardcoded
values were 400 (one cold access) and 800 (two cold accesses) low at
Amsterdam.

Add the fork-derived `COLD_ACCOUNT_ACCESS - 2600` delta once or twice per
the accesses each test makes; each is 0 pre-EIP-8037.

No test semantics change. Fills clean across Cancun -> Amsterdam.
The `stNonZeroCallsTest` tests measure the regular gas of a CALL/CALLCODE
that sends nonzero value to a cold account, then writes a result slot.
EIP-8038 reprices the cold account access (2600 -> 3000), the CALL value
transfer (9000 -> 10300), and the value-unchanged cold SSTORE, summing to
2500 at Amsterdam.

Express the measured value as the Cancun literal plus the fork-derived
sum of those components, each 0 pre-EIP-8037.

No test semantics change. Fills clean across Cancun -> Amsterdam.
`stSpecialTest/test_eoa_empty_paris` measures the regular gas of CALLs to
an empty account, with and without value. EIP-8038 reprices the cold
account access (2600 -> 3000) and the CALL value transfer (9000 ->
10300), so the value-bearing measurements were 1300 low and the
value-zero cold measurements 400 low at Amsterdam.

Add the fork-derived `CALL_VALUE - 9000` and `COLD_ACCOUNT_ACCESS - 2600`
deltas to the matching slots; each is 0 pre-EIP-8037. The value-0 warm
literals are unchanged.

No test semantics change. Fills clean across Cancun -> Amsterdam.
`stMemoryTest/test_oog` forwards a fixed in-bytecode gas budget to a
nested value-0 CALL to a cold contract; EIP-8038's cold account reprice
(2600 -> 3000) consumed the slack and made the RETURNDATACOPY success
path run out of gas, flipping its expected result.

Bump only that one forwarded calldata budget by the fork-derived
`COLD_ACCOUNT_ACCESS - 2600` (0 pre-EIP-8037). The companion failure-case
budget is left untouched and still runs out of gas as intended, so the
test still exercises both branches.

No test semantics change. Fills clean across Cancun -> Amsterdam.
`stEIP1153_transientStorage/test_14_revert_after_nested_staticcall` has
the caller write four fresh storage slots before a nested STATICCALL.
EIP-8037/8038 spill each fresh SSTORE's state gas into regular gas (empty
reservoir), pushing total consumption past the original transaction
budget so the final SSTORE ran out of gas and reverted the whole call.

Bump the transaction `gas_limit` by the summed fork-derived SSTORE
increases (`Op.SSTORE.with_metadata(...).gas_cost(fork)` deltas for the
three sets and one clear), which is 0 pre-EIP-8037. The inner STATICCALL
budget is untouched, preserving the static-violation semantics.

No test semantics change. Fills clean across Cancun -> Amsterdam.
danceratopz and others added 17 commits June 16, 2026 17:42
The `stRefundTest` tests assert the sender balance, which equals its
start minus the paid `gas_used * gas_price`. EIP-8038 raises SSTORE
clear/reset charges (cold clear 5000 -> 13000, etc.), so `gas_used`
rises and the hardcoded Cancun balances were wrong at Amsterdam.

Derive the per-SSTORE charge delta from the fork gas model
(`Op.SSTORE.with_metadata(...).gas_cost(fork)` minus the Cancun charge,
0 pre-EIP-8037) and adjust each balance by `gas_price * gas_used_delta`.
Where the EIP-3529 refund cap (`gas_used // 5`) binds at both forks the
extra charge raises `gas_used` by four fifths of itself; partial-cap
cases reconstruct `gas_used` as `gross - min(refund, gross // 5)` from a
fork-invariant base plus the fork-derived charge and refund. The
OOG-cascade case (`test_refund_no_oog_1`) also lifts the tx gas so its
clear still succeeds.

No test semantics change: the balances now track gas_used at every fork.
Fills clean across Cancun -> Amsterdam.
The `stTransactionTest` store-clears tests run ten (and an inner ten)
SSTORE clears. EIP-8038 raises each cold clear charge from 5000 to
13000, so the original tx gas_limit (and the inner CALL gas budget) no
longer covered the clears and the success path ran out of gas, changing
the post-state.

Bump the tx `gas_limit` and inner CALL gas by the fork-derived per-clear
charge delta (`Op.SSTORE.with_metadata(...).gas_cost(fork) - 5000`) times
the number of clears each frame performs (accounting for the 63/64 rule
on the inner CALL). The delta is 0 pre-EIP-8037, so earlier forks keep
the original budgets.

No test semantics change. Fills clean across Cancun -> Amsterdam.
`vmTests/test_suicide` asserts, for the `caller` case, a sender balance
that reflects `gas_used`. EIP-8038 reprices the outer CALL's cold account
access (2600 -> 3000), raising gas_used, so the hardcoded balance was 400
per access low at Amsterdam.

Adjust that balance by `gas_price` times the fork-derived
`COLD_ACCOUNT_ACCESS - 2600` per access (0 pre-EIP-8037). The `random`
and `myself` cases assert non-gas-dependent balances and are unchanged.

No test semantics change. Fills clean across Cancun -> Amsterdam.
The CREATE/CREATE2 "out of gas after init code" tests forward a tx gas
budget tuned so one parametrization runs out of gas after the init code
and another just clears the deploy threshold. EIP-8038 shifts that
threshold (the NEW_ACCOUNT cost becomes a spilling state-gas charge and
`OPCODE_CREATE_BASE` drops), so the previously-hardcoded Amsterdam window
constants were stale and the success parametrization wrongly ran out of
gas (a missing created account).

Replace the magic constants with `fork.oog_budget_lift(...)` (the state
gas EIP-8037 spills into regular gas, 0 pre-EIP-8037) plus the
fork-derived `OPCODE_CREATE_BASE` drop and code-deposit adjustment, so
the budget tracks the parameters: the out-of-gas parametrization still
runs out and the success one still deploys. `test_create2_smart_init_code`
lifts its success budget by the full spilled state gas
(`creates_before_oog=3, sstores_before_oog=2`).

No test semantics change. Fills clean across Cancun -> Amsterdam (the
out-of-gas case stays NONEXISTENT, the success case deploys).
`stRevertTest/test_revert_opcode_calls` d3 success path ends in a fresh
SSTORE-set in the transaction frame. EIP-8037 spills that set's state gas
into regular gas (empty reservoir), so the original `tx_gas[0]` ran out
at the final SSTORE and the stored result never landed.

Lift the g0 budget by `fork.oog_budget_lift(sstores_before_oog=1)` (the
spilled state gas, 0 pre-EIP-8037); g1 is unchanged.

No test semantics change. Fills clean across Cancun -> Amsterdam.
Move the EIP-8038 state-access gas repricing out of the combined
`EIP8037` mixin and into a dedicated `EIP8038` mixin.

The EIP mixins are composed in ascending order, so `EIP8038` sits one
level below `EIP8037` in the MRO. `EIP8037.gas_costs()` now reads the
EIP-8038 schedule via `super()` and folds its state-creation gas into
the shared `STORAGE_SET`, `TX_CREATE`, and `AUTH_PER_EMPTY_ACCOUNT`
totals. The access-cost logic moves with it: the `EXT*` warm-access
surcharge in `opcode_gas_map`, the regular `SSTORE` gas and refund, and
the `SELFDESTRUCT` account-write charge.

The change is behavior-preserving. The composed `Amsterdam` gas
schedule, the opcode gas, state, and refund calculators, and all
existing `EIP8037` validity markers stay identical. EIP-8038 now
resolves as its own EIP, so `is_eip_enabled(8038)` and
`valid_from("EIP8038")` become available.
Decompose the flat intrinsic transaction cost into recipient- and
value-dependent primitives, on top of the EIP-8037/8038 gas schedule.
Add infrastructure for testing EIP-2780's decomposed intrinsic gas and
state-aware top-frame charges.
Add state tests covering EIP-2780's intrinsic and top-frame gas
accounting.

- `test_value_moving_transactions.py`: parameterized over recipient
  types (EOA, CONTRACT, EMPTY_ACCOUNT, SELF, DELEGATION_7702) and
  values (0, non-zero) plus contract-creation and precompile cases.
  Verifies the framework's `intrinsic + top_frame + execution` model
  matches the spec by asserting the sender's post-tx balance.
- `test_intrinsic_gas_boundary.py`: pins `gas_limit = intrinsic - 1`
  across all recipient types and value variants to assert rejection
  with `INTRINSIC_GAS_TOO_LOW`. Top-frame boundary OOGs are covered
  by the dedicated charge tests in a later commit.
- `test_value_moving_with_tx_delegation.py`: covers the EIP-2780 ×
  EIP-7702 interaction where a type-4 transaction's authorization
  installs delegation on `tx.to`. Verifies suppression of the
  top-frame `COLD_ACCOUNT_ACCESS` (when the recipient is an existing
  EOA) and `NEW_ACCOUNT` (when the recipient is empty) charges,
  since the per-auth state gas already covers them.
Cover the four interesting outcomes for the post-intrinsic,
pre-EVM-dispatch charge layer that EIP-2780 introduces at the
top-level transaction frame.

- `test_top_frame_state_charge` parametrizes over (oog, success) for an
  empty recipient receiving value: gas one short of `NEW_ACCOUNT`
  OOGs at the state charge before the EVM dispatches; covering the
  charge lands the value transfer.
- `test_top_frame_regular_charge` parametrizes over (oog, success,
  evm_reverts) crossed with zero/non-zero value for an existing
  EIP-7702 delegated recipient: gas one short of `COLD_ACCOUNT_ACCESS`
  OOGs at the regular charge; with enough gas the delegated `STOP`
  lands the value transfer; a delegated `REVERT` rolls back the value
  transfer while the intrinsic, top-frame, and pre-revert EVM gas
  stay paid.
Lock down the EIP-2780 invariant that transaction-level account
charges always use the cold rate, even when the address is otherwise
warm, identical to the sender, or refers to itself.

Cases covered:
- Recipient or delegation target in the access list: pays the
  access-list cost but does not waive the cold charge.
- Recipient or delegation target is the block coinbase: implicit
  pre-warming does not waive the cold charge. Coinbase is pre-funded
  to isolate the intrinsic invariant from the orthogonal empty-
  recipient top-frame charge.
- Sender is the block coinbase: priority fee loops back to the
  sender, so net gas cost reduces to `gas_used * base_fee_per_gas`;
  intrinsic computation is unaffected.
- Delegation target is the sender, the recipient itself, or a
  precompile: top-frame charges fire as usual, and the dispatched
  EVM frame degenerates cleanly (empty code, the `INVALID` byte of
  the self-delegation prefix, or the disabled precompile body).
@danceratopz

Copy link
Copy Markdown
Owner

This was PR'd as ethereum#3017.

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