Skip to content

feat(spec-specs): implement EIP-8037 state creation gas cost increase#2181

Draft
spencer-tb wants to merge 13 commits intoethereum:eips/amsterdam/eip-8037from
spencer-tb:eips/amsterdam/eip-8037
Draft

feat(spec-specs): implement EIP-8037 state creation gas cost increase#2181
spencer-tb wants to merge 13 commits intoethereum:eips/amsterdam/eip-8037from
spencer-tb:eips/amsterdam/eip-8037

Conversation

@spencer-tb
Copy link
Contributor

@spencer-tb spencer-tb commented Feb 10, 2026

🗒️ Description

Implement EIP-8037: State Creation Gas Cost Increase on top of the Amsterdam fork, with spec alignment updates and bug fixes.

Key Changes

  • Two-dimensional gas metering: Split gas into REGULAR (computation, memory) and STATE (account creation, storage set, code deposit) via GasType enum.
  • Dynamic 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).
  • Reservoir model: Execution gas split into gas_left (capped at TX_MAX_GAS_LIMIT) and state_gas_reservoir. State gas draws from reservoir first, then gas_left.
  • Block accounting: header.gas_used = max(block_regular_gas_used, block_state_gas_used).
  • Harmonized state byte constants: NEW_ACCOUNT_BYTES=112, STORAGE_SET_BYTES=32, PER_AUTH_BASE_BYTES=23.

Updates on top of original implementation

  • Aligned quantization formula with latest EIP spec revision (EIPs PR #11292)
  • Fixed Uint underflow in get_state_gas_per_byte at gas limits below 13M
  • Fixed t8n_types.py crash from removed block_gas_used field
  • Attributed burned gas to correct dimension on ExceptionalHalt
  • Removed dead block_gas_used_in_tx variable and fixed cumulative_gas_used to use post floor value

🔗 Related Issues or PRs

EIP spec update: ethereum/EIPs#11292

issue #2040

✅ Checklist

  • All: Ran fast tox checks to avoid unnecessary CI fails, see also Code Standards and Enabling Pre-commit Checks:
    uvx tox -e 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

metapod

fradamt and others added 10 commits February 10, 2026 13:20
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).
@spencer-tb spencer-tb added A-spec-specs Area: Specification—The Ethereum specification itself (eg. `src/ethereum/*`) C-feat Category: an improvement or new feature P-high labels Feb 10, 2026
@spencer-tb spencer-tb changed the title feat(amsterdam): implement EIP-8037 state creation gas cost increase feat(spec-specs): implement EIP-8037 state creation gas cost increase Feb 10, 2026
spencer-tb and others added 3 commits February 10, 2026 13:30
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,
Copy link
Contributor

Choose a reason for hiding this comment

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

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:
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of using an enum to change behaviour, would it make more sense to create a charge_state_gas function?

Comment on lines +216 to +217
charge_gas(evm, REGULAR_GAS_CREATE + extend_memory.cost + init_code_gas)
charge_gas(evm, NEW_ACCOUNT_BYTES * state_gas_per_byte, GasType.STATE)
Copy link
Contributor

Choose a reason for hiding this comment

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

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:
Copy link
Contributor

Choose a reason for hiding this comment

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

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]:
Copy link
Contributor

Choose a reason for hiding this comment

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

This is approaching the level of complexity where named return values might be necessary, like with a dataclass.

Comment on lines -611 to +624
data_cost = tokens_in_calldata * STANDARD_CALLDATA_TOKEN_COST
data_gas = tokens_in_calldata * STANDARD_CALLDATA_TOKEN_COST
Copy link
Contributor

Choose a reason for hiding this comment

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

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))
Copy link
Contributor

Choose a reason for hiding this comment

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

We have ulen for this if you'd like.

Comment on lines +660 to 666
intrinsic_regular_gas = Uint(
TX_BASE_COST
+ data_gas
+ create_regular_gas
+ access_list_gas
+ auth_regular_gas
)
Copy link
Contributor

Choose a reason for hiding this comment

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

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
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems like an argument for backporting the block_gas_used -> block_regular_gas_used name.

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/*`) C-feat Category: an improvement or new feature P-high

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants