feat(spec-specs): implement EIP-8037 state creation gas cost increase#2181
feat(spec-specs): implement EIP-8037 state creation gas cost increase#2181spencer-tb wants to merge 13 commits intoethereum:eips/amsterdam/eip-8037from
Conversation
Add GasType enum (REGULAR, STATE) and state growth metering constants (NEW_ACCOUNT_BYTES, STORAGE_SET_BYTES, PER_AUTH_BASE_BYTES, etc.). Modify charge_gas() to support dual gas dimensions with reservoir model for state gas: state gas draws from state_gas_reservoir_left first, then gas_left when the reservoir is empty. Add state_gas_reservoir to Message and TransactionEnvironment, state_gas_reservoir_left and gas_used tracking to Evm, and dual gas accounting (block_regular_gas_used, block_state_gas_used, cumulative_gas_used) to BlockOutput. Implement get_state_gas_per_byte() with proportional quantization (~3% step, level-independent crossing speed ~32 blocks). Update incorporate_child_on_success/error to propagate state_gas_reservoir_left and per-type gas_used (delta-based).
Split intrinsic gas into regular and state dimensions. validate_transaction now accepts gas_limit and returns (intrinsic_regular_gas, intrinsic_state_gas, calldata_floor_gas_cost). CREATE transactions charge REGULAR_GAS_CREATE (regular) + NEW_ACCOUNT_BYTES * state_gas_per_byte (state). EIP-7702 authorizations charge PER_AUTH_BASE_COST (7500 regular) + (NEW_ACCOUNT_BYTES + PER_AUTH_BASE_BYTES) * sgpb (state). Validation enforces that intrinsic regular gas and calldata floor both fit within TX_MAX_GAS_LIMIT (16M), rather than capping tx.gas itself, since state gas can use the full tx.gas budget.
Implement reservoir model in process_transaction: split execution gas into regular_gas_budget (capped at TX_MAX_GAS_LIMIT - intrinsic_regular) and state_gas_reservoir (the remainder). Track block_regular_gas_used and block_state_gas_used separately, with block header gas_used = max(regular, state). Apply EIP-7623 calldata floor to block-level regular gas accounting. check_transaction validates regular and state gas limits independently: regular gas is capped at TX_MAX_GAS_LIMIT per-tx, while state gas can use full tx.gas. tx_gas_used uses tx.gas - gas_left - reservoir_left (correctly accounts for both gas pools). gas_used tracks execution charges as deltas; intrinsic gas is added separately for block accounting, reflecting set_delegation adjustments. Receipt cumulative_gas_used tracks cumulative user payments (post-refund, post-floor), so receipt[i] - receipt[i-1] gives per-tx gas consumption.
SSTORE 0->non-zero: charge STORAGE_SET_BYTES * sgpb as state gas, plus GAS_STORAGE_UPDATE - GAS_COLD_SLOAD as regular gas. Refund for 0->X->0 restoration includes both state gas and the regular write cost (cancelled writes don't do IO). CREATE/CREATE2: split into REGULAR_GAS_CREATE (regular) + NEW_ACCOUNT_BYTES * sgpb (state). Code deposit charges len(code) * sgpb (state) + keccak256 hash cost (regular). CALL to new account: replace GAS_NEW_ACCOUNT with NEW_ACCOUNT_BYTES * sgpb (state). SELFDESTRUCT to new: likewise replaced with state gas. All CALL variants pass full reservoir to children (no 63/64 rule for state gas) and use _escrow_subcall_regular_gas to avoid double-counting forwarded gas in the caller's gas_used. set_delegation: adjusts intrinsic_state_gas downward for existing accounts (instead of using refund_counter), returns None. ExceptionalHalt: zero both gas_left and state_gas_reservoir_left. AddressCollision: zero both gas pools and gas_used. process_message/process_create_message: pass state_gas_reservoir from TransactionEnvironment through Message to Evm.
- Update quantization formula to use CPSB_SIGNIFICANT_BITS=4 and CPSB_OFFSET=4451 (binary floating-point quantization) - Update REGULAR_GAS_CREATE from 2600 to 9000 (same as GAS_CALL_VALUE) - Consolidate TX_MAX_GAS_LIMIT import in fork.py - Remove unused TransactionGasLimitExceededError
- Remove `block_gas_used_in_tx` which was a leftover from EIP-7778's single-dimension gas accounting, made dead by EIP-8037's two-dimensional block accounting (block_regular_gas_used / block_state_gas_used). - Fix `cumulative_gas_used` to accumulate post-floor `tx_gas_used` instead of pre-floor `tx_gas_used_after_refund`, ensuring the calldata floor is reflected in receipts.
The t8n tool referenced `block_output.block_gas_used` which no longer exists in Amsterdam's BlockOutput. Use `max(block_regular_gas_used, block_state_gas_used)` per EIP-8037, with a hasattr fallback for older forks that still have `block_gas_used`.
The quantized value can be less than CPSB_OFFSET when the gas limit is below ~13M, causing an unsigned integer underflow before max() can clamp the result. Use an explicit comparison to avoid the underflow and return the floor value of 1.
- Attribute remaining gas_left to REGULAR and state_gas_reservoir_left to STATE in gas_used dict before zeroing on ExceptionalHalt, so block-level two-dimensional accounting is accurate for halted frames. - Fix PER_AUTH_BASE_COST comment (breakdown was inaccurate). - Add missing docstring for TX_MAX_GAS_LIMIT constant. - Fix stale __all__ in vm/__init__.py (Environment -> BlockEnvironment, add GasType).
Move GasType enum below all imports to fix E402 module-level import ordering. Fix import block sorting in interpreter.py.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| caller=tx_env.origin, | ||
| target=tx.to, | ||
| gas=tx_env.gas, | ||
| state_gas_reservoir=tx_env.state_gas_reservoir, |
There was a problem hiding this comment.
Not necessarily anything wrong, but if we're already providing the tx_env, why do we need to duplicate it inside of Message?
| sub_call: Uint | ||
|
|
||
|
|
||
| def get_state_gas_per_byte(gas_limit: Uint) -> Uint: |
There was a problem hiding this comment.
| def get_state_gas_per_byte(gas_limit: Uint) -> Uint: | |
| def state_gas_per_byte(gas_limit: Uint) -> Uint: |
Little benefit to including the get_ prefix.
|
|
||
| def charge_gas(evm: Evm, amount: Uint) -> None: | ||
| def charge_gas( | ||
| evm: Evm, amount: Uint, gas_type: GasType = GasType.REGULAR |
There was a problem hiding this comment.
Instead of using an enum to change behaviour, would it make more sense to create a charge_state_gas function?
| charge_gas(evm, REGULAR_GAS_CREATE + extend_memory.cost + init_code_gas) | ||
| charge_gas(evm, NEW_ACCOUNT_BYTES * state_gas_per_byte, GasType.STATE) |
There was a problem hiding this comment.
charge_gas may only be called exactly once per opcode, otherwise you'll emit multiple trace events.
| ) | ||
|
|
||
|
|
||
| def _escrow_subcall_regular_gas(evm: Evm, sub_call_gas: Uint) -> None: |
There was a problem hiding this comment.
We don't normally use underscore prefixed names.
| def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]: | ||
| def calculate_intrinsic_cost( | ||
| tx: Transaction, gas_limit: Uint | ||
| ) -> Tuple[Uint, Uint, Uint]: |
There was a problem hiding this comment.
This is approaching the level of complexity where named return values might be necessary, like with a dataclass.
| data_cost = tokens_in_calldata * STANDARD_CALLDATA_TOKEN_COST | ||
| data_gas = tokens_in_calldata * STANDARD_CALLDATA_TOKEN_COST |
There was a problem hiding this comment.
Are these renames part of Amsterdam, or just general improvements? If it's the latter, you'll need to backport them.
| auth_state_gas = ( | ||
| (NEW_ACCOUNT_BYTES + PER_AUTH_BASE_BYTES) | ||
| * state_gas_per_byte | ||
| * Uint(len(tx.authorizations)) |
| intrinsic_regular_gas = Uint( | ||
| TX_BASE_COST | ||
| + data_gas | ||
| + create_regular_gas | ||
| + access_list_gas | ||
| + auth_regular_gas | ||
| ) |
There was a problem hiding this comment.
Hm, the wrapping cast to Uint seems weird to me. Are the values not all already Uint?
| block_output.block_state_gas_used, | ||
| ) | ||
| else: | ||
| self.gas_used = block_output.block_gas_used |
There was a problem hiding this comment.
This seems like an argument for backporting the block_gas_used -> block_regular_gas_used name.
🗒️ Description
Implement EIP-8037: State Creation Gas Cost Increase on top of the Amsterdam fork, with spec alignment updates and bug fixes.
Key Changes
REGULAR(computation, memory) andSTATE(account creation, storage set, code deposit) viaGasTypeenum.cost_per_state_byte: Scales with block gas limit targeting 100 GiB annual state growth, using binary floating-point quantization (CPSB_SIGNIFICANT_BITS=4,CPSB_OFFSET=4451).gas_left(capped atTX_MAX_GAS_LIMIT) andstate_gas_reservoir. State gas draws from reservoir first, thengas_left.header.gas_used = max(block_regular_gas_used, block_state_gas_used).NEW_ACCOUNT_BYTES=112,STORAGE_SET_BYTES=32,PER_AUTH_BASE_BYTES=23.Updates on top of original implementation
Uintunderflow inget_state_gas_per_byteat gas limits below 13Mt8n_types.pycrash from removedblock_gas_usedfieldExceptionalHaltblock_gas_used_in_txvariable and fixedcumulative_gas_usedto use post floor value🔗 Related Issues or PRs
EIP spec update: ethereum/EIPs#11292
issue #2040
✅ Checklist
toxchecks to avoid unnecessary CI fails, see also Code Standards and Enabling Pre-commit Checks:uvx tox -e statictype(scope):.Cute Animal Picture