feat: EIP-8037 (State Creation Gas Cost Increase) — scaffolding + partial implementation#4285
Open
gabrocheleau wants to merge 5 commits intomasterfrom
Open
feat: EIP-8037 (State Creation Gas Cost Increase) — scaffolding + partial implementation#4285gabrocheleau wants to merge 5 commits intomasterfrom
gabrocheleau wants to merge 5 commits intomasterfrom
Conversation
Adds the EIP entry and parameter blocks for EIP-8037 without activating it on any hardfork yet. - common: register EIP 8037 with required EIPs (2780, 6780, 7702, 7825, 7976, 7981) and minimum hardfork Amsterdam - evm/params: regular-gas overrides for state-creating opcodes (sstoreSetGas 2900, createGas 9000, callNewAccountGas 0, createDataGas 0, codeDepositHashWordGas 6) and the new state-gas constants (costPerStateByte 1174, stateBytesPerStorageSet 32, stateBytesPerNewAccount 112, stateBytesPerAuthBase 23, systemMaxSstoresPerCall 16) - tx/params: EIP-7702 regular-gas overrides under 8037 (perAuthBaseGas 7500, perEmptyAccountCost 0) No behavior change — these only take effect when EIP-8037 is activated, which lands in a follow-up commit alongside the reservoir plumbing.
Step 2 of EIP-8037 (State Creation Gas Cost Increase):
- common/hardforks: include 8037 in the Amsterdam EIP list, activating
the regular-gas overrides from the EIP-8037 params blocks (sstore set
2900, create 9000, callNewAccount 0, perAuthBaseGas 7500,
perEmptyAccountCost 0, txCreationGas 9000, createDataGas 0).
- evm: add `stateGasReservoir` and `executionStateGasUsed` to EVM and
EVMInterface. These hold the per-tx reservoir state and are read /
written by opcodes and frame-exit hooks in follow-up commits.
- tx/params: add `txCreationGas: 9000` under 8037.
- vm/runTx: split intrinsic gas into regular and state portions when
8037 is active, applying the spec's reservoir formula:
execution_gas = tx.gas - intrinsic_gas
regular_gas_budget = TX_MAX_GAS_LIMIT - intrinsic_regular_gas
gas_left = min(regular_gas_budget, execution_gas)
reservoir = execution_gas - gas_left
The reservoir is installed on the EVM before runCall, the EVM runs
with `gas_left` as its gasLimit, and totalGasSpent post-call accounts
for any reservoir consumption.
- vm/runTx: corrects the 7702 intrinsic regular-gas computation under
8037 (getDataGas charges per-auth via `perEmptyAccountCost`, which is
0 under 8037; the per-auth `perAuthBaseGas` is added back here).
- vm/runTx: skip the legacy 7702 existing-authority refund when 8037
is active (it would otherwise produce a negative regular-gas refund;
the state-gas refund is added in a later commit).
State-gas is not yet charged on any opcode — those land in steps 3
and 4. The full state_gas_reservoir / state_gas_pricing fixture
groups still fail (57/57 reservoir, expected: tests require actual
state-gas accounting); this commit is plumbing only.
Step 3 of EIP-8037:
- evm/interpreter: add `chargeStateGas(amount, ctx)` and
`refillStateGasReservoir(amount, ctx)` methods. `chargeStateGas` draws
from the per-tx reservoir first, spilling to `gasLeft` (which can
OOG) when the reservoir is exhausted. `refillStateGasReservoir`
always refills the reservoir regardless of whether the original
charge spilled, per spec.
- evm/opcodes/SSTORE: at the end of the opcode, when the slot was
zero at the start of the transaction (`original == 0`):
current=0, new!=0 -> charge stateBytesPerStorageSet * costPerStateByte
current!=0, new=0 -> refill stateBytesPerStorageSet * costPerStateByte
Other transitions produce no state-gas adjustment, per the EIP-8037
table. The regular-gas portion of the SSTORE charge is still applied
by the existing dynamic-gas function (sstoreSetGas = 2900 under
8037, was 20000).
- tx/util: under 8037 the EIP-7825 transaction gas-limit cap applies
to the regular-gas dimension only, so the tx-construction-time cap
check is skipped. The runTx path validates
`max(intrinsic_regular_gas, calldata_floor_gas_cost) <= TX_MAX_GAS_LIMIT`
instead.
- vm/runTx: use `tx.common.param('maxTransactionGasLimit')` for the
cap (the param lives in the tx params block).
state_gas_reservoir tests: 18/57 passing (was 0/57). Remaining
failures expect account-creation state gas, block-level 2D gas
accounting, and revert-path refunds, which land in step 4.
Step 4 (partial) of EIP-8037:
- vm/types: add `txStateGas` and `txRegularGas` to RunTxResult so
runBlock can keep two independent block-level accumulators.
- vm/runTx: under 8037, expose per-dimension breakdowns:
tx_state_gas = intrinsic_state_gas + execution_state_gas_used
tx_regular_gas = intrinsic_regular_gas + execution_regular_gas_used
where execution_regular_gas_used backs out the slice of state-gas
that spilled into gas_left from the executionGasUsed total.
- vm/runBlock: accumulate `block_regular_gas_used` and
`block_state_gas_used` independently and set
gas_used = max(block_regular_gas_used, block_state_gas_used)
for the block header (per spec). The per-tx pre-execution check is
also relaxed to the spec's two-dimension form:
min(TX_MAX_GAS_LIMIT, tx.gas) <= regular_available
tx.gas <= state_available
What still fails (tracked for the next commits, not yet implemented):
- CREATE / CREATE2 frame-exit state-gas charge for the new account
and for code deposit ((stateBytesPerNewAccount + L) * costPerStateByte).
- CALL* with value to a non-existent account: frame-exit state-gas
charge of stateBytesPerNewAccount * costPerStateByte.
- Frame-revert / exceptional-halt refund of state-gas charges to the
parent reservoir (needs journal hook so SSTORE charges in a child
frame are unwound on revert).
- EIP-7702 authorization state-gas: intrinsic per-auth charge of
(stateBytesPerNewAccount + stateBytesPerAuthBase) * costPerStateByte and refund
of stateBytesPerNewAccount * costPerStateByte when authority is non-empty.
- SELFDESTRUCT deferred refund at end-of-tx for accounts created and
destructed in the same tx.
- 20% refund cap formula: still uses combined gas; spec uses
tx.gas - gas_left - state_gas_reservoir for the cap base.
- System call gas adjustment (30M + reservoir headroom for system
contracts).
Test status against the snobal-devnet-6@v1.1.0 fixtures:
- state_gas_reservoir: 18/57 passing (was 0/57 before this branch)
- state_gas_pricing: 4/74 passing (was 0/74 before this branch)
Codecov Report❌ Patch coverage is Additional details and impacted files
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
📦 Bundle Size Analysis
Values are minified+gzipped bundles of each package entry. Workspace deps are bundled; external deps are excluded. Generated by bundle-size workflow |
52f744d to
df77986
Compare
4 tasks
gabrocheleau
added a commit
that referenced
this pull request
May 6, 2026
- evm: snapshot stateGasReservoir + executionStateGasUsed at every
message-level journal.checkpoint(); on revert / exceptional halt
restore them, refunding any state-gas charged within the reverted
frame and undoing any in-frame reservoir refills (per spec). On
commit drop the snapshot. The snapshot stack is reset at the start
of each tx.
- evm: charge new-account + code-deposit state-gas at the end of a
successful CREATE / CREATE2 frame:
state-gas = (stateBytesPerNewAccount + L) * costPerStateByte
(depth > 0 = CREATE opcode)
state-gas = L * costPerStateByte
(depth 0 = creation tx;
the stateBytesPerNewAccount
portion is intrinsic)
The regular code-deposit cost is replaced under 8037 by
6 * ceil(L/32) * codeDepositHashWordGas.
State-gas is drawn from the reservoir first, spilling to gas_left;
if the spill cannot fit in the frame's remaining budget the CREATE
is converted to OOG and the snapshot is restored.
- evm: charge new-account state-gas on successful CALL* frame exit
when the destination account did not exist (or was empty) before
the frame and the call carried non-zero value (mirroring the
existing callNewAccountGas trigger). delegatecall is excluded.
Precompile addresses are excluded too — they are code-only entities,
not real account state, so funding a precompile via CALL-with-value
does not create new "stored" account state.
- evm/vm: EIP-8037 SELFDESTRUCT deferred refund (account + code-deposit
portion). EVM tracks the state-gas charged per newly-created address;
runTx, after the call returns, refunds those amounts directly to the
reservoir for any address that was both created and SELFDESTRUCTed in
the same tx (per EIP-6780 + EIP-8037). The refund is not subject to
the 20% cap. Storage-slot per-account tracking is a follow-up.
- vm/runTx: clear the EVM's per-frame snapshot stack and the per-tx
created-account state-gas map at the start of each tx so leftover
state from a previous tx cannot leak.
Test results vs the snobal-devnet-6@v1.1.0 dev fixtures:
Full Amsterdam dev suite 1215 / 1985 passing (parent #4285: 1187)
state_gas_reservoir 27 / 57 (parent: 18)
state_gas_pricing 4 / 74 (parent: 4)
Net +28 vs parent across the full dev suite, 0 regressions in any
group. The journal-based revert refund unblocks `nested_failure_*`,
`subcall_revert_*`, and several `top_level_failure_*` cases. The
precompile bypass keeps `test_bal_precompile_funded` and
`test_transfer_to_special_address-precompile_*` passing. The
SELFDESTRUCT deferred refund keeps `test_create_selfdestruct_*` and
the same-tx `test_selfdestruct_to_*` cases passing.
`state_gas_pricing` is unchanged. The remaining pricing failures
appear to expect costPerStateByte to scale with the block gas limit
(test names include `_scales_with_cpsb`), while the EIP text I worked
from defines costPerStateByte as a fixed constant of 1174. Triage in
the next branch (could be a spec change since the reference, or one
of the incorrect-fixture cases Dragan flagged).
Still queued for the next branch:
- EIP-7702 authorization state-gas (intrinsic per-auth + non-empty refund)
- Storage-slot per-account tracking for SELFDESTRUCT refund
- 20% refund cap formula update (tx.gas - gas_left - reservoir)
- System-call gas adjustment (30M + reservoir headroom)
- costPerStateByte parametric vs constant triage
Still queued for the next branch: - EIP-7702 authorization state-gas (intrinsic per-auth + non-empty refund) - Storage-slot per-account tracking for SELFDESTRUCT refund - 20% refund cap formula update (tx.gas - gas_left - reservoir) - System-call gas adjustment (30M + reservoir headroom) - costPerStateByte parametric vs constant triage
11 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Initial scaffolding and partial implementation of EIP-8037 (State Creation Gas Cost Increase) on top of the new snobal-devnet-6@v1.1.0 dev fixtures pulled in by #4284.
The EIP introduces a separate "state-gas" dimension with its own per-tx reservoir and a 2D block-level gas accounting model. This PR lands the foundational pieces; remaining items (CREATE/CALL state-gas, journal-based revert refund, 7702 auth state-gas, SELFDESTRUCT deferred refund, refund cap formula) are listed below for follow-up.
Commits
costPerStateByte,stateBytesPerStorageSet,stateBytesPerNewAccount,stateBytesPerAuthBase,systemMaxSstoresPerCall).stateGasReservoirandexecutionStateGasUsedtoEVM/EVMInterface.runTxsplits intrinsic gas into regular and state portions and applies the spec's reservoir formula:chargeStateGas/refillStateGasReservoiron the interpreter (reservoir-first, gas-left spill, OOG on overflow). SSTORE charges32 × costPerStateByteonoriginal=0, current=0, new!=0; refills onoriginal=0, current!=0, new=0. Also relaxes the EIP-7825 tx-level cap so it applies to the regular-gas dimension only under 8037.block_regular_gas_used/block_state_gas_usedaccumulators and setsgas_used = max(...)for the block header. Per-tx pre-execution check uses the spec's two-dimension form.Test results vs the snobal-devnet-6@v1.1.0 fixtures
state_gas_reservoirstate_gas_pricing22/131 of the targeted dev fixtures pass on this branch (up from 0/131). The other ~109 failures all need pieces not implemented yet (see below) — they are not regressions, they are the known-unimplemented surface of the EIP.
Not yet implemented (follow-up commits)
(112+L)·costPerStateByte)create_state_gas_scales_with_cpsb, creation-tx reservoir tests112·costPerStateByte)call_new_account_state_gas_scales_with_cpsband combosnested_failure_*,subcall_failure_*,top_level_failure_*,revert_discards_*(~20 tests)auth_state_gas_scales_with_cpsbselfdestruct_new_beneficiary_scales_with_cpsbtx.gas - gas_left - reservoir)refund_cap_includes_state_gas,sstore_refund_scales_with_cpsbNotes
Test plan
npm run build(common, tx, evm, vm)npm run test:est:dev:blockchainagainststate_gas_reservoirandstate_gas_pricingsubdirectoriestest:est:dev:blockchainsuite to confirm no regressions outside the EIP-8037 fixtures