spencer.taylor-brown@ethereum.org
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Emoji Reply
      • Invitee
      • No invitee
    • Publish Note

      Publish Note

      Everyone on the web can find and read all notes of this public team.
      Once published, notes can be searched and viewed by anyone online.
      See published notes
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Sharing URL Help
Menu
Options
Versions and GitHub Sync Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Emoji Reply
Invitee
No invitee
Publish Note

Publish Note

Everyone on the web can find and read all notes of this public team.
Once published, notes can be searched and viewed by anyone online.
See published notes
Engagement control
Commenting
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
  • Everyone
Emoji Reply
Enable
Import from Dropbox Google Drive Gist Clipboard
   owned this note    owned this note      
Published Linked with GitHub
Subscribed
  • Any changes
    Be notified of any changes
  • Mention me
    Be notified of mention me
  • Unsubscribe
Subscribe
# EIP-8037: Framework Rollout Plan (Per-Block cpsb) Per-block cpsb is staying. This is the framework-first rollout — no more per-test gas bumps. All changes share a common foundation: threading `block_gas_limit: int | None = None` through the gas calculation chain. This is backward compatible (`None` = current hardcoded default). --- ## Foundation: `block_gas_limit` Parameter + Amsterdam cpsb Plumbing ### Method Signatures to Update | File | Methods | |------|---------| | `base_fork.py` | `gas_costs()`, `transaction_intrinsic_cost_calculator()`, `transaction_intrinsic_state_gas()`, `opcode_gas_calculator()`, `opcode_refund_calculator()` | | `vm/bases.py` | `opcode_gas_calculator()` and `opcode_refund_calculator()` protocol definitions | | `forks.py` | All overrides (~40), plus `opcode_gas_map()` (10 overrides), `opcode_refund_map()` (uses `cls.gas_costs()`) | | `bytecode.py` | `Bytecode.gas_cost()`, `Bytecode.refund()` | | **Total** | **~12 methods, ~40 overrides** | ### Amsterdam's 5 Internal `cost_per_state_byte()` Call Sites All 5 currently call `cls.cost_per_state_byte()` with the hardcoded default (120M). All 5 must use the passed `block_gas_limit` instead. **Confirmed in PR #2235**: Each method calls `cpsb = cls.cost_per_state_byte()` with no parameter. The default comes from `_DEFAULT_BLOCK_GAS_LIMIT = 120_000_000` hardcoded in the Amsterdam class (cannot import from `test_types.block_types` due to circular dependency — see comment in PR #2235). | Line | Method | How it uses cpsb | |------|--------|-----------------| | 3499 | `gas_costs()` | Computes G_STORAGE_SET, G_CREATE, G_NEW_ACCOUNT, G_AUTHORIZATION | | 3543 | `transaction_intrinsic_state_gas()` | Computes intrinsic state gas for creation + auth | | 3568 | `_calculate_sstore_gas()` | SSTORE cost (0→nonzero includes 32 * cpsb) | | 3602 | `_calculate_sstore_refund()` | SSTORE refund (includes state_gas_storage_set) | | 3655 | `_calculate_return_gas()` | Code deposit cost (code_size * cpsb) | **Critical**: The 3 private `_calculate_*` methods receive `gas_costs` as a parameter but **also independently call `cls.cost_per_state_byte()` with the hardcoded default**. They must receive cpsb as a parameter instead. ### The Full Call Hierarchy ``` cost_per_state_byte(gas_limit=...) ← already parameterized ├── gas_costs(block_gas_limit=...) ← NEEDS PARAM (line 3499) │ ├── transaction_intrinsic_cost_calculator() ← NEEDS PARAM (Prague line 2864) │ ├── opcode_gas_map() ← NEEDS PARAM (Amsterdam line 3410) │ │ └── opcode_gas_calculator() ← NEEDS PARAM │ │ └── Bytecode.gas_cost(fork) ← NEEDS PARAM │ └── opcode_refund_map() ← NEEDS PARAM (calls cls.gas_costs()) │ └── opcode_refund_calculator() ← NEEDS PARAM │ └── Bytecode.refund(fork) ← NEEDS PARAM ├── transaction_intrinsic_state_gas() ← NEEDS PARAM (line 3543) ├── _calculate_sstore_gas() ← NEEDS PARAM (line 3568, via opcode_gas_map) ├── _calculate_sstore_refund() ← NEEDS PARAM (line 3602, via opcode_refund_map) └── _calculate_return_gas() ← NEEDS PARAM (line 3655, via opcode_gas_map) ``` ## 1. Non-Static Tests ### Problem PR [#2235](https://github.com/ethereum/execution-specs/pull/2235) introduced ~45 gas limit bumps and ~25 fork-conditional patterns across 57 test files. Common patterns from the PR: ```python # Fork-conditional (most common — ~25 instances) gas_limit = 500_000 if fork >= Amsterdam else 100_000 gas_limit = 5_000_000 if fork >= Amsterdam else 500_000 # Subcall gas (selfdestruct tests) subcall_gas = 500_000 if fork >= Amsterdam else 100000 # Sender balance bumps sender = pre.fund_eoa(amount=500_000_000 if extra_gas else 4_000_000) ``` These are fragile — the "right" number depends on the block gas limit, which can change. The PR also bumped the generator default buffer from 500K to 1M (`generators.py:615`). ### Solution: `auto_gas` Metadata + Block Share Strategy Instead of computing gas analytically, assign gas from the block's budget. Non-static tests check post-state via **Python assertions** (storage slots, code, nonces) — not hardcoded expected values — so "enough gas" is sufficient. ### Why Not a Sentinel in `gas_limit` `gas_limit` is typed `HexNumber` (`test_types/transaction_types.py:313`). Pydantic validation coerces values through `HexNumber` reconstruction (`base_types.py:121`), so an `AutoGas(int)` sentinel would be stripped — `isinstance(tx.gas_limit, AutoGas)` fails after model validation. ### `AutoGasConfig` Model Field Use explicit metadata on `Transaction` instead: ```python class AutoGasConfig(BaseModel): """Configuration for automatic gas resolution.""" min_reservoir: int = Field(0, ge=0) # minimum state_gas_reservoir (Amsterdam+) # In Transaction (test_types/transaction_types.py): class Transaction(TransactionGeneric[HexNumber], ...): auto_gas: AutoGasConfig | None = Field(None, exclude=True) # exclude=True: never serialized to t8n input ``` **`exclude=True` footgun**: `copy()` is implemented via `model_dump()` (`base_types/pydantic.py:31`). Excluded fields won't survive this path — pipeline helpers like the gas optimization path in `state.py:142` use `.copy(...)`. Fix: override `copy()` / `model_copy()` on Transaction to preserve `auto_gas`, or use a serialization-only exclusion (`json_schema_extra` or custom `model_dump` override) rather than field-level `exclude=True`. Test authors opt in explicitly: ```python # Most tests — just need "enough gas" tx = Transaction(auto_gas=AutoGasConfig(), ...) # Tests that need a specific reservoir size tx = Transaction(auto_gas=AutoGasConfig(min_reservoir=20_000_000), ...) # Tests with explicit gas — no auto_gas field, gas_limit used as-is tx = Transaction(gas_limit=100_000, ...) ``` `gas_limit` keeps its default of `21_000`. The resolver overwrites it only when `auto_gas is not None`. No sentinel in `gas_limit`, no Pydantic coercion issues. ### Resolution Strategy: `block_share` Resolve `auto_gas` before signing in **both** flows: - **`state.py`** (line 351) — state test flow - **`blockchain.py`** (line 603) — blockchain test flow **Single-tx block**: auto tx gets the full block gas limit. **Multi-tx block**: split remaining block gas across auto txs in order, after subtracting explicit tx gas. ### Fork Cap API New mode/value split to eliminate Osaka/Amsterdam ambiguity. **Must accept `block_number`/`timestamp`** — the existing `transaction_gas_limit_cap()` is block/time-aware for transition fork support (`test_tx_gas_limit_transition_fork.py:57`): ```python # In base_fork.py @classmethod def tx_gas_cap_mode( cls, *, block_number: int = 0, timestamp: int = 0 ) -> str: """Return tx gas cap semantics: 'none' | 'hard_total' | 'regular_only'.""" return "none" @classmethod def tx_gas_cap_value( cls, *, block_number: int = 0, timestamp: int = 0 ) -> int | None: """Return the tx gas cap value, or None if no cap.""" return None ``` **Fork mapping:** | Fork range | `tx_gas_cap_mode` | `tx_gas_cap_value` | Semantics | |------------|-------------------|-------------------|-----------| | Pre-Osaka (Genesis–Prague) | `"none"` | `None` | No cap. Equal shares, no reservoir. | | Osaka–BPO* | `"hard_total"` | `16_777_216` | `gas_limit` field clamped to cap (EIP-7825). No reservoir concept. `min_reservoir` incompatible. | | Amsterdam+ | `"regular_only"` | `16_777_216` | Cap applies to **regular gas only** (EIP-8037). Total `gas_limit` can exceed cap — excess flows to `state_gas_reservoir`. `min_reservoir` enforced when set. | Osaka and Amsterdam share the same numeric cap (`2^24`) but the semantics differ fundamentally. At Osaka the cap is a hard ceiling on the entire `gas_limit` field. At Amsterdam the same cap only constrains the regular gas portion — the total `gas_limit` can be much larger because surplus feeds the state gas reservoir. This is why the mode/value split exists: the value alone is ambiguous without knowing whether it's a hard total or a regular-only cap. **Consistency guards** (enforced at fork class load or first call): ```python mode = cls.tx_gas_cap_mode() value = cls.tx_gas_cap_value() if mode in ("hard_total", "regular_only") and value is None: raise ForkConfigError(f"tx_gas_cap_mode={mode} requires tx_gas_cap_value != None") if mode == "none" and value is not None: raise ForkConfigError(f"tx_gas_cap_mode='none' but tx_gas_cap_value={value}") ``` **Compatibility shim**: Keep existing `transaction_gas_limit_cap()` (`forks.py:1143`, `forks.py:3102`) mapped from the new API. Used widely — `benchmark.py:371`, `benchmark.py:454`, `pre_alloc.py:367`, `generators.py:793`, and test files. Returns `cap_value` for both `hard_total` and `regular_only` (consumers expect `int` when a cap exists), `None` only for `"none"`: ```python # In base_fork.py — backward-compatible shim @classmethod def transaction_gas_limit_cap( cls, *, block_number: int = 0, timestamp: int = 0 ) -> int | None: """Compatibility: returns cap value when any cap exists, None otherwise.""" mode = cls.tx_gas_cap_mode( block_number=block_number, timestamp=timestamp ) if mode in ("hard_total", "regular_only"): return cls.tx_gas_cap_value( block_number=block_number, timestamp=timestamp ) return None ``` ### Dependency: Foundation Must Land First The resolver calls `transaction_intrinsic_cost_calculator(block_gas_limit=...)` and `transaction_intrinsic_state_gas(block_gas_limit=...)`. The current base signature at `forks.py:816` does not have the `block_gas_limit` param yet. The Foundation section (Section 0) must land first — it threads `block_gas_limit` through all gas calculation entry points and unblocks this resolver. ### Resolver Resolution keys off `tx.auto_gas` metadata (not `gas_limit` value), so repeated generation runs are idempotent. Uses a **two-pass allocator**: pass 1 reserves per-tx minimums, pass 2 distributes remaining budget. All assignments are computed into a plan first, then applied atomically — no partial mutation on failure. ```python def resolve_auto_gas(fork, env, txs): """Resolve auto_gas metadata to concrete gas limits before signing.""" auto_indices = [i for i, tx in enumerate(txs) if tx.auto_gas is not None] if not auto_indices: return # Block context for transition-fork-aware lookups bn, ts = env.number, env.timestamp # --- Guards --- for i in auto_indices: tx = txs[i] # Re-signable: secret_key must be set # (test_types/transaction_types.py:394, test_types/transaction_types.py:549) if tx.secret_key is None: raise AutoGasBudgetError( "auto_gas requires a re-signable tx (secret_key must be set). " "Pre-signed txs need explicit gas_limit." ) # Intentional-failure txs if tx.error is not None: raise AutoGasBudgetError( f"auto_gas cannot be used with tx.error={tx.error}. " f"Intentional failure tests need explicit gas_limit." ) # Receipt gas assertions if tx.expected_receipt and tx.expected_receipt.gas_used is not None: raise AutoGasBudgetError( "auto_gas cannot be used with expected_receipt.gas_used. " "Gas-exact tests need explicit gas_limit." ) if tx.expected_receipt and tx.expected_receipt.cumulative_gas_used is not None: raise AutoGasBudgetError( "auto_gas cannot be used with expected_receipt.cumulative_gas_used. " "Gas-exact tests need explicit gas_limit." ) cap_mode = fork.tx_gas_cap_mode(block_number=bn, timestamp=ts) cap_value = fork.tx_gas_cap_value(block_number=bn, timestamp=ts) explicit_sum = sum( txs[i].gas_limit for i in range(len(txs)) if i not in auto_indices ) # --- Pre-loop sanity checks --- # Policy: reject if explicit txs alone exceed block gas limit. # Stricter than protocol validity (protocol checks cumulative gas_used, # not declared gas_limit sum), but ensures the allocator has budget to # work with. Intentional — makes auto_gas deterministic and avoids # silently assigning zero-gas shares. if explicit_sum > env.gas_limit: raise AutoGasBudgetError( f"Explicit tx gas ({explicit_sum}) already exceeds " f"block gas limit ({env.gas_limit})." ) if cap_mode == "hard_total": for i in auto_indices: if txs[i].auto_gas.min_reservoir: raise AutoGasBudgetError( f"min_reservoir={txs[i].auto_gas.min_reservoir} requested " f"but fork {fork} has a hard tx gas cap. " f"Use explicit gas_limit." ) # --- Pass 1: compute per-tx minimum gas --- minimums = {} for i in auto_indices: tx = txs[i] intrinsic = fork.transaction_intrinsic_cost_calculator( block_gas_limit=env.gas_limit, block_number=bn, timestamp=ts, )( calldata=tx.data, contract_creation=tx.to is None, access_list=tx.access_list, authorization_list_or_count=tx.authorization_list, ) intrinsic_state = fork.transaction_intrinsic_state_gas( block_gas_limit=env.gas_limit, block_number=bn, timestamp=ts, contract_creation=tx.to is None, authorization_count=len(tx.authorization_list or []), ) intrinsic_total = intrinsic + intrinsic_state if cap_mode == "regular_only" and tx.auto_gas.min_reservoir: required_exec_for_res = ( max(0, cap_value - intrinsic) + tx.auto_gas.min_reservoir ) floor = intrinsic_total + required_exec_for_res else: floor = intrinsic_total minimums[i] = floor reserved = sum(minimums.values()) remaining = env.gas_limit - explicit_sum if reserved > remaining: raise AutoGasBudgetError( f"Minimum gas for auto txs ({reserved}) exceeds available " f"budget ({remaining}). Use explicit gas_limit." ) # --- Pass 2: distribute remaining budget above minimums --- surplus = remaining - reserved n = len(auto_indices) plan = {} # index -> gas_limit for j, i in enumerate(auto_indices): # Last tx gets exact surplus remainder if j == n - 1: bonus = surplus else: bonus = surplus // (n - j) surplus -= bonus share = minimums[i] + bonus if cap_mode == "hard_total": share = min(share, cap_value) plan[i] = share # --- Apply atomically --- for i, gas_limit in plan.items(): txs[i].gas_limit = gas_limit ``` Usage in the pre-signing window: ```python # blockchain.py (line 603) env = block.set_environment(previous_env) # gas limit KNOWN env = env.set_fork_requirements(self.fork) resolve_auto_gas(self.fork, env, block.txs) # resolve auto_gas txs = [tx.with_signature_and_sender() for tx in block.txs] # signing HERE ``` ### Resolver Behavior by Fork **Pre-Osaka (`"none"`):** Auto txs split the full block gas limit. A single auto tx gets the entire block budget (e.g., 120M). No cap constraints. **Osaka–BPO* (`"hard_total"`):** Auto txs are hard-clamped to `cap_value` (16M). Even with a 120M block, each auto tx gets at most 16M. This is correct — at Osaka there is no state gas reservoir, so 16M is the genuine protocol maximum. Tests needing >16M gas cannot use `auto_gas` at Osaka, but this is unlikely since the state gas cost increases from EIP-8037 don't exist yet. **Amsterdam+ (`"regular_only"`):** Auto txs can receive the full block share (potentially >>16M). The 16M cap constrains regular gas only — excess flows to state gas reservoir. This is where `min_reservoir` becomes meaningful, ensuring heavy-state tests (e.g., `test_all_opcodes.py` needing 50M) have sufficient reservoir budget. ### Explicit Gas Always Wins If `tx.auto_gas is None` (no metadata), `gas_limit` is never modified. This preserves OOG tests, threshold tests, and gas-semantics tests. Resolution is keyed off metadata, not the `gas_limit` value. ### Default-Auto Gating The current `gas_limit` default is `21_000` (`test_types/transaction_types.py:313`). Globally flipping unspecified gas limits to auto would change legacy behavior (balances, optimization paths). **`auto_gas` must be explicitly set per-tx.** No implicit "unspecified gas => auto" behavior. If a framework-wide default is desired later, gate it behind Amsterdam+ fork check or a framework flag — not as a global default. ### Eligibility Criteria `auto_gas` is appropriate for tests that are: - **Re-signable** — `secret_key` is set (not pre-signed with v/r/s) - **No `tx.error` set** — not an intentional failure/OOG test - **No `expected_receipt.gas_used`** — not asserting exact gas consumption - **No `expected_receipt.cumulative_gas_used`** — not asserting cumulative gas (`specs/helpers.py:315`) - **No sender balance assertions** — gas spending changes the sender's final balance - **Post-state-only assertions** — storage/code/nonce checks, not exact gas or balance - **Non-OOG semantics** — the test expects the transaction to succeed - **No gas-sensitive execution** — no GAS opcode / gas-dependent branching where huge gas changes behavior - **Not in gas optimization / benchmark mode** — these need exact gas control. **Enforcement**: the resolver must be gated out of the `BenchmarkTestFiller` flow. `BenchmarkTestFiller.generate_blockchain_test()` constructs its own `BlockchainTest` with pre-computed transactions — it does not go through the standard `blockchain.py` signing window where `resolve_auto_gas` runs. But if a benchmark test manually constructs a `BlockchainTest` with `auto_gas` txs, the resolver would fire. Add an explicit op-mode check: skip resolution when `BaseTest.op_mode == OpMode.BENCHMARK` Works for both single-tx and multi-tx blocks (two-pass allocator handles splitting). ### Tests That Still Need Explicit Gas **Computed explicit gas** — derive gas from `fork.gas_costs(block_gas_limit=...)` so it auto-adjusts with cpsb: ```python gas_costs = fork.gas_costs(block_gas_limit=env.gas_limit) gas_limit = num_sstores * gas_costs.G_STORAGE_SET + base_overhead ``` **Hardcoded explicit gas** — for OOG tests, exact-gas tests, gas-sensitive branching. These remain fork-conditional where needed. ### Scope Of the ~45 gas limit bumps: - **Eligible for `auto_gas`**: majority — single-tx state tests with storage/nonce assertions - **`auto_gas` + `min_reservoir`**: heavy state tests needing reservoir (e.g., `test_all_opcodes.py`) - **Computed explicit**: tests with exact gas math or many state operations - **Hardcoded explicit**: OOG tests, gas-sensitive tests, exact-balance tests --- ## 2. Benchmark Tests ### Problem Benchmarks manually compute gas costs for iteration counts and assert **exact** gas matching (zero tolerance): ```python gas_costs = fork.gas_costs() # step 1: get costs loop_cost = gas_costs.G_CREATE + gas_costs.G_SELF_DESTRUCT + ... # step 2: per-iteration cost num_contracts = (attack_gas_limit - base_costs) // loop_cost # step 3: iterations expected_benchmark_gas_used = iterations * loop_cost + base_costs # step 4: expected gas # step 5: create transactions # step 6: assert gas_used == expected_benchmark_gas_used (exact match) ``` If step 1 returns wrong costs (hardcoded default doesn't match actual block gas limit), every subsequent step is wrong. `auto_gas` doesn't help here — benchmarks need **exact** gas, not "enough". ### Solution **`block_gas_limit` and `gas_benchmark_value` are separate knobs.** They must not be conflated: - **`block_gas_limit`** → controls cpsb / gas schedule (what gas constants are). Pass `Environment().gas_limit` (the block's actual gas limit). - **`gas_benchmark_value`** → controls workload budget (how much gas the benchmark uses). This is the attack/iteration budget. Pass the block's gas limit as `block_gas_limit` to all entry points so they return correct gas constants: ```python # Use the block's actual gas limit for gas schedule, NOT gas_benchmark_value block_gas_limit = Environment().gas_limit # or env.gas_limit # ~18 call sites gas_costs = fork.gas_costs(block_gas_limit=block_gas_limit) # ~18 call sites intrinsic_calc = fork.transaction_intrinsic_cost_calculator(block_gas_limit=block_gas_limit) # ~2 call sites loop_gas = loop.gas_cost(fork, block_gas_limit=block_gas_limit) # ~2 call sites loop_refund = loop.refund(fork, block_gas_limit=block_gas_limit) ``` `gas_benchmark_value` remains the workload budget — how many iterations, how many contracts to create, etc. The gas *schedule* (G_CREATE, G_STORAGE_SET, cpsb) comes from `block_gas_limit`. Once entry points return correct values, ALL downstream math self-corrects: - `loop_cost` → correct (uses correct G_CREATE, G_STORAGE_SET, etc.) - `num_contracts` / `iterations` → correct (derived from loop_cost) - `expected_benchmark_gas_used` → correct (derived from iterations) - setup tx counts → correct (derived from gas_costs.G_CREATE) - intrinsic gas → correct (uses correct state gas) - SSTORE/RETURN opcode gas → correct (uses correct cpsb) Keep exact assertions. Compute expectations with the same gas context. ### Amsterdam Cap Semantics: Don't Over-Split **8 call sites** currently treat `transaction_gas_limit_cap()` as a hard total cap: - `conftest.py:93` — `tx_gas_limit` fixture - `benchmark.py:371,454` — `generate_blocks_from_code_generator()`, `split_transaction()` path - `benchmark.py:473` — `generate_fixed_opcode_count_transactions()` (fixed opcode count mode) - `generators.py:793,846` — `tx_iterations_by_gas_limit()`, `tx_iterations_by_total_iteration_count()` - `test_tx_context.py:65` At Amsterdam with `regular_only`, total `gas_limit` can exceed 16M — excess flows to state gas reservoir. Treating 16M as a hard ceiling over-splits transactions, adding unwanted intrinsic overhead per extra tx and breaking exact gas assertions (warm/cold patterns change, receipt `gas_used` sums diverge). **Fix**: These sites must use the new cap API **with block context** (for transition fork correctness). The behavior depends on cap mode: - **`"hard_total"`** (Osaka): split at `cap_value` as today — total `gas_limit` cannot exceed 16M. - **`"regular_only"`** (Amsterdam): per-tx **regular gas** is still capped at 16M, but total `gas_limit` can exceed it (excess → reservoir). Regular-gas-heavy benchmarks (no state ops) still need splitting at `cap_value` for the regular portion. State-heavy benchmarks can assign total `gas_limit` above the cap. The split decision depends on whether the workload is regular-gas-bound or state-gas-bound. - **`"none"`** (pre-Osaka): no splitting needed. ```python # conftest.py — updated tx_gas_limit fixture @pytest.fixture def tx_gas_limit(fork: Fork, gas_benchmark_value: int, env: Environment) -> int: mode = fork.tx_gas_cap_mode( block_number=env.number, timestamp=env.timestamp ) cap = fork.tx_gas_cap_value( block_number=env.number, timestamp=env.timestamp ) if mode == "hard_total": return min(cap, gas_benchmark_value) # "regular_only": total gas_limit can exceed cap, but regular gas # per tx is still capped. Return full budget — let split_transaction / # generators handle per-tx regular-gas capping when needed. return gas_benchmark_value ``` Similarly, `generators.py:793` (`tx_iterations_by_gas_limit`), `generators.py:846` (`tx_iterations_by_total_iteration_count`), and `benchmark.py:473` (`generate_fixed_opcode_count_transactions`) should use `tx_gas_cap_mode()` with block context instead of the scalar `transaction_gas_limit_cap()`. For `hard_total`, split as today. For `regular_only`, only split when the workload is regular-gas-bound — state-heavy workloads can use the full budget unsplit. **Shared classifier**: To avoid inconsistent `regular_only` decisions across 8+ call sites, add a shared helper: ```python def benchmark_tx_gas_limit( fork: Fork, gas_budget: int, *, block_number: int = 0, timestamp: int = 0, ) -> int: """Return the effective per-tx gas limit for benchmark splitting.""" mode = fork.tx_gas_cap_mode(block_number=block_number, timestamp=timestamp) cap = fork.tx_gas_cap_value(block_number=block_number, timestamp=timestamp) if mode == "hard_total": return min(cap, gas_budget) # "regular_only" and "none": full budget (splitting handled downstream) return gas_budget ``` All 8 cap-mode call sites use this instead of inline logic. ### IteratingBytecode Internal Chain Needs `block_gas_limit` The plan's "pass `block_gas_limit` to entry points" is necessary but not sufficient. The `IteratingBytecode` gas calculation chain has internal calls that also need context: **Direct offenders** (call gas APIs without context): | Method | File:Line | What it calls without context | |--------|-----------|------------------------------| | `iterating_subcall_gas_cost()` | `generators.py:588` | `Bytecode.gas_cost(fork=fork)` — no `block_gas_limit` | | `gas_cost_by_iteration_count()` | `generators.py:609,611,619,621` | `Bytecode.gas_cost(fork=fork)` for setup/iterating/cleanup | | `tx_gas_cost_by_iteration_count()` | `generators.py:660` | `fork.transaction_intrinsic_cost_calculator()` — no `block_gas_limit` | | `FixedIterationsBytecode.gas_cost()` | `generators.py:1079` | Discards `block_number`/`timestamp`, never had `block_gas_limit` | **Forwarding methods** (must accept and pass `block_gas_limit` to reach the above): | Method | File:Line | Calls | |--------|-----------|-------| | `iterating_subcall_reserve()` | `generators.py:590` | `iterating_subcall_gas_cost()` | | `tx_gas_limit_by_iteration_count()` | `generators.py:683` | `tx_gas_cost_by_iteration_count()` + `iterating_subcall_reserve()` | | `_binary_search_iterations()` | `generators.py:705` | `tx_gas_limit_by_iteration_count()` | | `tx_iterations_by_gas_limit()` | `generators.py:773` | `_binary_search_iterations()` | | `tx_iterations_by_total_iteration_count()` | `generators.py:831` | `_binary_search_iterations()` | | `transactions_by_gas_limit()` | `generators.py:876` | `tx_iterations_by_gas_limit()` | | `transactions_by_total_iteration_count()` | `generators.py:944` | `tx_iterations_by_total_iteration_count()` | **Fix**: Thread `block_gas_limit` through the full `IteratingBytecode` method chain. Every method that computes gas must accept and forward `block_gas_limit`: ```python def gas_cost_by_iteration_count( self, *, fork: Type[ForkOpcodeInterface], iteration_count: int, block_gas_limit: int | None = None, ) -> int: ... loop_gas_cost = self.iterating.gas_cost( fork=fork, block_gas_limit=block_gas_limit ) ... ``` This is part of the Foundation work — the `Bytecode.gas_cost()` signature already accepts `block_number`/`timestamp`, adding `block_gas_limit` follows the same pattern. ### Fixed Opcode Count Mode Benchmarks have two modes: **gas-based** (default) and **fixed opcode count** (`benchmark.py:357`). In fixed opcode count mode: - `skip_gas_used_validation = True` — no exact gas assertions, measuring by operation count not gas usage - `generate_fixed_opcode_count_transactions()` (`benchmark.py:465`) creates a single tx per block — no splitting - Still uses `transaction_gas_limit_cap() or gas_benchmark_value` at line 473 to set `gas_limit` Since gas validation is skipped, cpsb changes don't break assertions. But the `gas_limit` still needs to be large enough for the tx to execute all target opcodes. With Amsterdam `regular_only`, capping at 16M may starve state-heavy fixed-count benchmarks. Same fix applies: use `tx_gas_cap_mode()` and only hard-cap for `hard_total`. ### Stateful Benchmarks (bloatnet) Stateful benchmarks under `tests/benchmark/stateful/bloatnet/` use **two distinct patterns**, both affected: **Pattern A — `BlockchainTestFiller` with manual gas math** (`test_single_opcode.py:127-181`, `test_multi_opcode.py:75-112`): ```python gas_costs = fork.gas_costs() # NO block_gas_limit intrinsic_calc = fork.transaction_intrinsic_cost_calculator() # NO block_gas_limit intrinsic_gas = intrinsic_calc(calldata=b"", contract_creation=False) # Manual iteration count iterations = (tx_gas_limit - intrinsic_gas - overhead) // per_op_cost # Manual splitting num_txs = max(1, math.ceil(gas_benchmark_value / tx_gas_limit)) ``` These tests compute gas from scratch — they don't use `BenchmarkTestFiller` or `IteratingBytecode`. Call sites lacking `block_gas_limit`: - `test_single_opcode.py`: lines 133/136 (SSTORE), 275/278 (SLOAD) - `test_multi_opcode.py`: lines 79/82 (BALANCE+EXTCODESIZE), 260/264 (EXTCODECOPY+CREATE2), 449/452 (SLOAD+SSTORE mixed), 642/645 (SLOAD+SSTORE mixed ratio) Fix: pass `block_gas_limit` at each call site. The manual splitting (`num_txs = max(1, math.ceil(gas_benchmark_value / tx_gas_limit))`) uses `tx_gas_limit` from the fixture, which goes through `conftest.py:93` — already covered by the cap-mode fix above. **Pattern B — `BenchmarkTestFiller` with `IteratingBytecode`** (`test_single_opcode.py:744-808`, `test_single_opcode.py:879-943`, `test_deep_branch.py:325-373`): ```python iterations = loop.tx_iterations_by_gas_limit(fork=fork, tx_gas_limit=tx_gas_limit) # ... IteratingBytecode generators handle splitting (transactions_by_gas_limit) ... expected_benchmark_gas_used = iterations * per_iter_cost + overhead ``` Splitting is done by `IteratingBytecode` generators (`generators.py:876` `transactions_by_gas_limit()`, `generators.py:944` `transactions_by_total_iteration_count()`), not by `BenchmarkTestFiller` itself. When `blocks` is provided to the filler, it does not split (`benchmark.py:366-367`). These use the standard `IteratingBytecode` chain — already covered by the "IteratingBytecode Internal Chain" fix above. The `expected_benchmark_gas_used` assertions at lines 803 and 938 self-correct once the chain receives correct `block_gas_limit`. **Pattern C — `BlockchainTestFiller` with gas-based loop** (`test_extcodesize_bytecode_sizes.py:174-220`): These tests use a gas-based loop exit strategy (contract checks remaining gas internally) rather than computing iteration counts. No explicit `gas_costs()` or `intrinsic_cost_calculator()` calls — the contract self-regulates. However, they still compute `num_txs` from `tx_gas_limit` and need cap-mode-aware splitting. Not covered by `split_transaction` or generator logic — needs explicit handling. **Stateful-specific concern**: Stateful tests often exercise ERC20 contracts where SSTORE and SLOAD dominate. These are the operations most affected by cpsb — G_STORAGE_SET goes from 20K (old) to 40K+ at 120M block gas limit. The iteration count drops ~2x, which is correct behavior (benchmarking state throughput at that gas limit), but the exact gas assertions must use the matching constants. ### EIP-8037 cpsb-Affected Gas Constants The `cost_per_state_byte` (cpsb) is derived from the block gas limit via a quantized formula (per EIP-8037 spec; Amsterdam `cost_per_state_byte()` already accepts `gas_limit` param — confirmed in PR #2235). These are the gas constants that become functions of cpsb — the ones benchmarks compute manually: | Constant | Formula | @ 30M | @ 60M | @ 120M | Old (fixed) | |----------|---------|-------|-------|--------|-------------| | `G_STORAGE_SET` | `(G_STORAGE_UPDATE - G_COLD_SLOAD) + 32 * cpsb` | 7,700 | 24,084 | 40,468 | 20,000 | | `G_CREATE` | `REGULAR_GAS_CREATE + 112 * cpsb` | 25,800 | 83,144 | 140,488 | 32,000 | | `G_NEW_ACCOUNT` | `112 * cpsb` | 16,800 | 74,144 | 131,488 | 25,000 | | `G_AUTHORIZATION` | `PER_AUTH_BASE_COST + (112+23) * cpsb` | varies | varies | varies | 25,000 | | `G_CODE_DEPOSIT_BYTE` | `cpsb + GAS_KECCAK256_WORD * ceil(len/32) / len` | varies | varies | varies | 200 | (Constants verified against PR #2181 `vm/gas.py`: `REGULAR_GAS_CREATE = 9000`, `PER_AUTH_BASE_COST = 7500`, `STATE_BYTES_PER_STORAGE_SET = 32`, `STATE_BYTES_PER_NEW_ACCOUNT = 112`, `STATE_BYTES_PER_AUTH_BASE = 23`) **Impact on benchmark gas math**: Every benchmark that manually computes `per_op_cost` using these constants will get different values at different block gas limits. This is intentional — benchmarks measure throughput at a given network size — but the exact gas assertions **must** use constants from the same `block_gas_limit` as the execution. **SSTORE is the dominant driver**: Most stateful benchmarks (ERC20, storage access patterns) revolve around SSTORE. At 120M, `G_STORAGE_SET` doubles vs the old fixed cost — iteration counts halve, tx splitting changes. **CREATE dominates setup**: Setup blocks deploying contracts are hit hardest — `G_CREATE` goes from 32K to 140K at 120M (4.4x). More setup txs needed, larger fixtures, but this is overhead, not measured throughput. ### Dependencies Section 2 depends on: - **Foundation** (Section 0): `block_gas_limit` param on `gas_costs()`, `transaction_intrinsic_cost_calculator()`, `Bytecode.gas_cost()`, etc. - **Fork Cap API** (defined in Section 1): `tx_gas_cap_mode()`/`tx_gas_cap_value()` and the backward-compatible `transaction_gas_limit_cap()` shim. The Fork Cap API could be split into Foundation as a standalone sub-PR since Section 2 needs it independently of the auto_gas resolver. ### Setup Blocks Setup blocks also use `gas_costs` for computing how many ops fit per setup tx. Same fix: ```python # test_selfdestruct_existing -- setup phase gas_costs = fork.gas_costs(block_gas_limit=block_gas_limit) estimated_gas_per_creation = gas_costs.G_CREATE + 3_000 max_creations_per_tx = int(tx_gas_limit // estimated_gas_per_creation) num_setup_txs = math.ceil(num_contracts / max_creations_per_tx) ``` ### Files to Modify | File | Entry points used | Call sites | |------|------------------|-----------| | `tests/benchmark/conftest.py` | `transaction_gas_limit_cap()` → `tx_gas_cap_mode()`/`tx_gas_cap_value()` | 1 | | `packages/.../specs/benchmark.py` | `transaction_gas_limit_cap()`, `split_transaction()` | 3 | | `packages/.../tools/tools_code/generators.py` | Full `IteratingBytecode` chain (see table above), `transaction_gas_limit_cap()` | ~12 | | `tests/benchmark/compute/instruction/test_tx_context.py` | `transaction_gas_limit_cap()` | 1 | | `tests/benchmark/compute/instruction/test_storage.py` | `gas_costs()`, `intrinsic_cost_calculator()`, `Bytecode.gas_cost()`, `Bytecode.refund()` | ~6 | | `tests/benchmark/compute/instruction/test_system.py` | `gas_costs()`, `intrinsic_cost_calculator()`, `memory_expansion_calc()` | ~8 | | `tests/benchmark/compute/instruction/test_account_query.py` | `gas_costs()`, `intrinsic_cost_calculator()` | ~2 | | `tests/benchmark/compute/instruction/test_call_context.py` | `intrinsic_cost_calculator()` | ~2 | | `tests/benchmark/compute/instruction/test_keccak.py` | `gas_costs()`, `intrinsic_cost_calculator()`, `memory_expansion_calc()` | ~3 | | `tests/benchmark/compute/scenario/test_transaction_types.py` | `gas_costs()`, `intrinsic_cost_calculator()` | ~8 | | `tests/benchmark/compute/scenario/test_unchunkified_bytecode.py` | `gas_costs()`, `intrinsic_cost_calculator()` | ~2 | | `tests/benchmark/compute/precompile/test_alt_bn128.py` | `gas_costs()`, `intrinsic_cost_calculator()`, `memory_expansion_calc()` | ~3 | | `tests/benchmark/compute/precompile/test_sha256.py` | `intrinsic_cost_calculator()` | ~1 | | `tests/benchmark/compute/precompile/test_identity.py` | `intrinsic_cost_calculator()` | ~1 | | `tests/benchmark/compute/precompile/test_ripemd160.py` | `intrinsic_cost_calculator()` | ~1 | | `tests/benchmark/compute/precompile/test_bls12_381.py` | `intrinsic_cost_calculator()` | ~1 | | `tests/benchmark/compute/helpers.py` | `gas_costs()`, `memory_expansion_calc()` | ~2 | | `tests/benchmark/stateful/bloatnet/test_single_opcode.py` | `gas_costs()`, `intrinsic_cost_calculator()`, `tx_iterations_by_gas_limit()`, `transactions_by_gas_limit()` | ~8 | | `tests/benchmark/stateful/bloatnet/test_multi_opcode.py` | `gas_costs()`, `intrinsic_cost_calculator()` (4 test functions × 2 calls each) | ~8 | | `tests/benchmark/stateful/bloatnet/depth_benchmarks/test_deep_branch.py` | `tx_iterations_by_gas_limit()`, `transactions_by_gas_limit()` | ~2 | | `tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py` | Manual `num_txs` from `tx_gas_limit` (gas-based loop, no explicit `gas_costs()`) | ~2 | | Non-benchmark EIP test files (~24 files) | Various | ~24 | | **Total** | | **~101** | ### Edge Cases - Block gas limit is set via `--block-gas-limit` CLI flag (`filler.py:624`), flows to `EnvironmentDefaults.gas_limit` (`filler.py:743`), and is consistent across all blocks in a benchmark. **Two access paths**: `Environment().gas_limit` (creates new Environment from `EnvironmentDefaults` — used for pre-signing gas math in benchmarks) and `env.gas_limit` (from `Block.set_environment()` — used by the auto_gas resolver in Section 1). These are the same value for benchmarks because blocks don't override gas_limit, but they come from different code paths. CI multi-gas-limit runs just re-invoke fill/execute with different `--block-gas-limit` values. - `GasCosts` is frozen → create new instances per block gas limit, cache by `(fork, block_gas_limit, block_number, timestamp)` tuple - `Bytecode` internal gas cache (`bytecode.py:285,310`) must include `block_gas_limit` in key — see Foundation caching note - `IteratingBytecode` internal chain (`generators.py:588-660`) must forward `block_gas_limit` to all `Bytecode.gas_cost()` and `fork.*_calculator()` calls - `FixedIterationsBytecode.gas_cost()` (`generators.py:1079`) must not discard `block_gas_limit` — currently drops `block_number`/`timestamp` too - `regular_only` cap at Amsterdam: per-tx regular gas is still capped at 16M, but total `gas_limit` can exceed it. Split for `hard_total` always; for `regular_only`, only split when the workload is regular-gas-bound (no/few state ops). State-heavy workloads can use full budget unsplit. Manual stateful tests computing `num_txs` from `tx_gas_limit` (e.g., `test_extcodesize_bytecode_sizes.py`) also need cap-mode-aware handling. - Stateful bloatnet benchmarks use two patterns (manual `BlockchainTestFiller` gas math and `BenchmarkTestFiller` + `IteratingBytecode`) — both need `block_gas_limit` but through different code paths - `G_STORAGE_SET` doubles at 120M vs old fixed cost — SSTORE-heavy benchmarks (ERC20, storage access) halve their iteration counts, which is correct but exact assertions must match --- ## 3. Static Tests ### Problem **1,475 failures** across **698 filler files** in **47 test directories**. All post-state mismatches from `>=Cancun` expect sections. These are JSON/YAML filler-based tests — no Python fork conditionals. ### Why EIP-8037 Breaks Static Tests EIP-8037 introduces a **dual-dimension gas model** that fundamentally changes how gas is charged. Static tests hardcode transaction gas limits in JSON — these were calibrated for pre-Amsterdam fixed gas costs but are now insufficient because gas costs scale dynamically with the block gas limit. **The dual-dimension split.** Every gas constant that touches state is split into two components: | Constant | Pre-Amsterdam (fixed) | Amsterdam: regular | Amsterdam: state | Source | |----------|----------------------|-------------------|-----------------|--------| | `G_STORAGE_SET` | 20,000 | 2,900 (`G_STORAGE_UPDATE - G_COLD_SLOAD`) | `STATE_BYTES_PER_STORAGE_SET` (32) × cpsb | `storage.py`, `vm/gas.py` | | `G_CREATE` | 32,000 | `REGULAR_GAS_CREATE` (9,000) | `STATE_BYTES_PER_NEW_ACCOUNT` (112) × cpsb | `system.py`, `vm/gas.py` | | `G_NEW_ACCOUNT` | 25,000 | 0 | `STATE_BYTES_PER_NEW_ACCOUNT` (112) × cpsb | `system.py` | | `G_CODE_DEPOSIT` | 200/byte | `GAS_KECCAK256_WORD × ceil(len/32)` / len | cpsb/byte | `interpreter.py` | | `PER_AUTH_BASE_COST` | 12,500 | `PER_AUTH_BASE_COST` (7,500) | (`STATE_BYTES_PER_NEW_ACCOUNT` + `STATE_BYTES_PER_AUTH_BASE`) × cpsb = (112 + 23) × cpsb | `eoa_delegation.py`, `vm/gas.py` | Where `cpsb = cost_per_state_byte(block_gas_limit)` is computed per the EIP-8037 formula (verified against PR #2181 `vm/gas.py:state_gas_per_byte()` and PR #2235 `Amsterdam.cost_per_state_byte()`): ```python TARGET = 100 * 1024**3 # TARGET_STATE_GROWTH_PER_YEAR BLOCKS_PER_YEAR = 2_628_000 OFFSET = 9578 # COST_PER_STATE_BYTE_OFFSET SIG_BITS = 5 # COST_PER_STATE_BYTE_SIGNIFICANT_BITS raw = (gas_limit * BLOCKS_PER_YEAR + 2 * TARGET - 1) // (2 * TARGET) # ceiling division shifted = raw + OFFSET shift = max(shifted.bit_length() - SIG_BITS, 0) cpsb = max(((shifted >> shift) << shift) - OFFSET, 1) ``` **The reservoir model.** Transaction gas is split into two budgets at execution start (verified against PR #2181 `fork.py:975-979`): ```python # PR #2181 fork.py process_transaction() intrinsic = validate_transaction(tx, block_env.block_gas_limit) intrinsic_gas = intrinsic.regular + intrinsic.state # IntrinsicGasCost dataclass execution_gas = tx.gas - intrinsic_gas regular_gas_budget = TX_MAX_GAS_LIMIT - intrinsic.regular # 16,777,216 - regular intrinsic gas = min(regular_gas_budget, execution_gas) # gas_left state_gas_reservoir = Uint(execution_gas - gas) # tx gas ABOVE regular budget ``` State gas operations draw from the reservoir first, then overflow to `gas_left` (PR #2181 `charge_state_gas()`). The `GAS` opcode returns only `gas_left`. Block validation uses `max(block_gas_used, block_state_gas_used)` not the sum (PR #2181 `fork.py:271-274`). **Why tests fail.** Consider a typical static test with `currentGasLimit: 10000000000` (10B block gas limit) and `gasLimit: 100000` (100K tx gas): 1. `cpsb(10B)` ≈ 121,494 (enormous — cpsb scales with block gas limit) 2. A single SSTORE costs: regular `2,900` + state `32 × 121,494 = 3,887,808` 3. The tx has 100K gas total — OOGs immediately on any state operation Even at the framework default `DEFAULT_BLOCK_GAS_LIMIT = 120M`: 1. `cpsb(120M)` ≈ 1,174 2. `G_STORAGE_SET` total = `2,900 + 32 × 1,174 = 40,468` (vs old 20,000) 3. Tests with tight gas budgets OOG; tests with headroom get different final balances The key insight: **cpsb is computed from the test's own `env.currentGasLimit` (block gas limit), not the tx gas limit.** Static tests often set block gas limits to arbitrarily large values (10B+), making cpsb and therefore state gas costs enormous. The hardcoded tx gas limits were never designed for this. ### Solution: Dynamic Gas Adjustment in the Filler Pipeline The framework must dynamically adjust the transaction gas limit at Amsterdam+ to fund both the regular gas budget and the state gas reservoir. This happens in the filler pipeline (`state_static.py`), not in the JSON files. #### Critical EIP-8037 gas model detail **EIP-7825 caps regular gas, not total tx gas.** At Amsterdam, a transaction's total gas can (and should) exceed the 16,777,216 regular gas cap. The excess funds the `state_gas_reservoir`: ``` execution_gas = tx.gas_limit - intrinsic_gas regular_gas_budget = TX_GAS_LIMIT_CAP - intrinsic_regular_gas # ≈ 16M gas_left = min(regular_gas_budget, execution_gas) state_gas_reservoir = execution_gas - gas_left # tx gas ABOVE 16M ``` Setting `tx.gas_limit = 16,777,216` (the regular cap) leaves `state_gas_reservoir = 0`, forcing all state gas to draw from `gas_left`. At high cpsb values, this is insufficient. The tx gas must be set **higher than the regular cap** to properly fund the reservoir. #### Dependencies **Section 3 requires the specs PR (#2181) to land first.** Specifically, the Amsterdam transaction validation must allow `tx.gas_limit > TX_GAS_LIMIT_CAP` (EIP-8037 caps regular gas, not total gas). PR #2181 already handles this — it removes `TransactionGasLimitExceededError` and the `tx.gas > TX_MAX_GAS_LIMIT` rejection, replacing it with `max(intrinsic.regular, calldata_floor) > TX_MAX_GAS_LIMIT`. Without this change, bumping tx gas to `env.gas_limit` (which can far exceed 16M) would be rejected by transaction validation. #### Framework Change: `state_static.py` **Injection point**: `test_state_vectors()` at line ~192, between tx creation and `state_test()` call. This is where `fork`, `env`, and `exception` are all available. ```python tx = self.transaction.get_transaction(tags, d, g, v, exception) # EIP-8037: state gas costs scale with block_gas_limit via cpsb. # Static tests hardcode tx gas limits calibrated for pre-Amsterdam # fixed costs. At Amsterdam+, we must fund both the regular gas # budget AND the state gas reservoir. Set tx gas to the block gas # limit (env.gas_limit) so the reservoir is properly funded. # Guard on exception: do not adjust gas for tests that expect # a specific failure — tx.copy() would also drop the tx.error field. if fork >= Amsterdam and exception is None: block_gas = int(env.gas_limit) tx = tx.copy(gas_limit=block_gas) ``` **`exception is None` guard**: Two reasons: 1. Tests with `expectException` intentionally fail. Adjusting gas would mask the failure. 2. `tx.copy()` rebuilds via `model_dump()` which excludes `tx.error` (`transaction_types.py:326`). Calling copy on an exception tx would drop the error metadata. The guard prevents this. **Why `env.gas_limit` (block gas limit)?** The reservoir model requires `tx.gas > TX_GAS_LIMIT_CAP` for the state reservoir to have any budget. Using the block gas limit: 1. Properly funds the state reservoir: `state_gas_reservoir = block_gas - intrinsic - regular_budget` 2. Is consistent with the cpsb derivation (cpsb is computed from this same block gas limit) 3. Is the maximum valid tx gas for the block **Why not just the EIP-7825 cap (16M)?** At 16M, the reservoir is empty. A test with `currentGasLimit: 10B` would compute `cpsb(10B) ≈ 121,494`, making a single SSTORE cost `32 × 121,494 = 3,887,808` state gas — impossible with an empty reservoir and only 16M regular gas. **Reservoir overflow provides a safety net.** Even when the reservoir is insufficient for a state gas charge, `charge_state_gas()` (PR #2181) overflows to `gas_left`: ```python if evm.state_gas_left >= amount: evm.state_gas_left -= amount elif evm.state_gas_left + evm.gas_left >= amount: remainder = amount - evm.state_gas_left evm.state_gas_left = Uint(0) evm.gas_left -= remainder else: raise OutOfGasError ``` This means tests can survive with less reservoir than the total state gas cost. The plan's approach of bumping to `env.gas_limit` is maximally conservative — it gives the maximum possible reservoir, ensuring even tests with enormous cpsb values have enough gas in the right dimension. **Reservoir preserved on failure.** Per PR #11328: on exceptional halt, only `gas_left` is consumed (zeroed). The `state_gas_reservoir` is returned to the parent frame because state changes are reverted. This means if a test OOGs on regular gas, the reservoir is not wasted — it flows back correctly. **Sender balance and affordability**: tx gas = `env.gas_limit` requires sender balance `≥ gas × gas_price`. For tests with huge `currentGasLimit` and limited sender balance (e.g., `randomStatetest121Filler.json` with `currentGasLimit = 0x7fffffffffffffff`, sender balance `1e18`, gasPrice `10`), the tx can't afford the gas. The tx will fail the intrinsic gas check and produce a reverted post-state. **This is the correct Amsterdam behavior** — if a test's block gas limit makes state gas costs unaffordable, the tx legitimately fails under the new gas model. No sender balance boosting, no special handling. The same logic runs in both the capture script and the runtime, so results always reproduce. #### Batch Capture Workflow **Single-pass, consistent with runtime.** The capture script applies the same gas adjustment as the runtime (`tx.gas = env.gas_limit` when `exception is None`). No two-pass triage, no divergence between capture and runtime behavior. For each `(d, g, v, fork=Amsterdam)` combination: 1. **Build the test**: Resolve tags, build env, build tx (with gas adjustment if applicable) 2. **Run through `t8n.evaluate()` directly** — not through `make_state_test_fixture()`, which asserts on post-state mismatch (`state.py:375-379`). Extract `transition_tool_output.alloc` and `result.receipts[0].status` (success/failure). 3. **Compare** the captured post-state against the existing matching expect section. 4. **If different** → new `>=Amsterdam` expect section needed. 5. **If identical** → test works as-is, no new section. **Baseline section selection**: Must replicate the exact first-match semantics from `test_state_vectors()` (`state_static.py:172-202`): iterate expect sections in order, check `has_index(d, g, v)` AND `fork in expect.network`, use first match. Multi-section files (e.g., `push0Filler.yml` with different fork ranges) require this precision. **Receipt status**: The capture records `result.receipts[0].status` (0 = failure, 1 = success) for diagnostic purposes. This distinguishes successful execution with different balances from OOG reverts — useful for reporting but not for the capture logic itself (we always capture whatever post-state results). **Per-`(d,g,v)` granularity**: Mixed files with both exception and non-exception indexes (e.g., `lowGasLimitFiller.yml` with different data indexes) are handled individually. Exception vectors skip gas adjustment; non-exception vectors get it. #### Capture Script: `capture_amsterdam_expects.py` New CLI command alongside `modify_static_test_gas_limits.py`. **Direct t8n capture**: The capture builds the `StateTest` manually and calls `t8n.evaluate()` directly, extracting `transition_tool_output.alloc` without the assertion at `state.py:375-379`. **File format handling**: Uses the same JSON/YAML dual-parser as `modify_static_test_gas_limits.py` (`modify_static_test_gas_limits.py:73-87`), with `NoIntResolver` for YAML files. **Fork-range handling**: Does not assume all failing tests use `>=Cancun`. Identifies the existing matching expect section per `(d,g,v)` using the same first-match + `has_index` logic as `test_state_vectors`. Some tests use `>=Prague`, `>=Osaka`, or compound ranges like `>=Cancun<Prague`. **Structure-preserving file modification**: - **JSON files**: Targeted string insertion — locate the `"expect": [` position, insert the new section as a serialized JSON object before the first existing entry. Preserves formatting, comment keys, and key ordering. - **YAML files**: Use `ruamel.yaml` (round-trip mode) which preserves comments, key ordering, and formatting. Falls back to raw string insertion if `ruamel.yaml` is unavailable. - This avoids the destructive churn of `json.dump()` or `yaml.dump()` re-serialization. **Grouping**: For each file, captured post-states are grouped by result. If all `(d, g, v)` combos produce the same Amsterdam post-state, a single section with wildcard indexes (`-1`) is used. Otherwise, per-combination sections with specific indexes are created. ### Tests with Multiple Gas Limits / Data / Value For tests with `gasLimit: ["21000", "50000"]` (multiple `g` indexes): - Each `g` index gets the same gas adjustment (`env.gas_limit`) - Post-states are captured per `(d, g, v)` combination - If results differ across combinations, separate expect sections with specific `indexes` are created ### Skip Criteria Gas adjustment is skipped for: 1. **`expectException` tests**: The `exception is None` guard in the injection point handles this at runtime and in capture. Per-`(d,g,v,fork)` granularity — not per-file, since mixed files exist. 2. **Tests where Amsterdam post-state matches existing expect**: No new section needed. ### Fork Comparison Semantics Fork comparisons use the inheritance-based `__le__`/`__ge__` operators (`base_fork.py:215-238`), not chronological ordering. `>=Amsterdam` matches Amsterdam and its subclass forks, which is correct. However, BPO sibling forks (e.g., `BPO3` if it doesn't inherit from Amsterdam) would NOT match `>=Amsterdam`. The capture script must verify that the fork hierarchy is linear for the target test's fork range. The `ForkConstraint.match()` (`expect_section.py:190-203`) and `ForkSet` parser (`expect_section.py:220-241`) already handle this correctly — they iterate `get_forks()` and check each constraint. The plan relies on this existing infrastructure. ### Existing Tooling Reuse | Tool | Reuse | |------|-------| | `modify_static_test_gas_limits.py` | **CLI structure + file parsing** (click command, JSON/YAML dual-parser with `NoIntResolver`, `StaticTestFile`, dry-run, regex-based file modification pattern) | | `t8n.evaluate()` | **Direct post-state capture** (called without `verify_post_alloc` assertion) | | `StateTest.verify_modified_gas_limit()` | **Binary search** for tightening gas (optional second pass to find minimum sufficient gas) | | `ForkSet` / `ForkConstraint` | **Already parses `>=Amsterdam`** — no changes needed | ### File Changes | File | Change | |------|--------| | `state_static.py` | ~8 lines: gas adjustment in `test_state_vectors()` | | `capture_amsterdam_expects.py` (new) | ~300 lines: CLI for single-pass capture + structure-preserving JSON/YAML modification | | 698 `*Filler.json`/`.yml` files | Automated: new `>=Amsterdam` expect sections prepended | | No changes to `general_transaction.py` | Gas adjustment happens downstream | | No changes to `expect_section.py` | `ForkSet` already handles `>=Amsterdam` | | No changes to `environment.py` | `env.gas_limit` already flows from JSON | ### Ongoing Maintenance The `>=Amsterdam` expect sections are tied to each test's specific `env.currentGasLimit` (which determines cpsb). If a test's block gas limit changes, its expect section needs recapturing — the same batch script handles this. If cpsb parameters change in the EIP (constants, formula), ALL 698 sections need recapturing. This is a one-command operation (`capture_amsterdam_expects.py --recapture`). **This maintenance burden is inherent to per-block cpsb.** Every time gas costs change as a function of block gas limit, static test post-states must be re-derived. Compared to pinning cpsb at fork level: zero maintenance — all tests work as-is. --- ## CI Guardrails ### 1. Multi-Gas-Limit CI **Goal**: Verify Amsterdam non-static tests pass at multiple block gas limits, since cpsb (and therefore gas costs) scale with `block_gas_limit`. **Existing infrastructure**: The `--block-gas-limit` CLI flag already exists (`filler.py:624`). It sets `EnvironmentDefaults.gas_limit` at session start (`filler.py:742-743`). No framework changes needed to parameterize. **Where to put it**: New tox environments in `tox.ini`, referenced from `test.yaml` jobs. NOT `feature.yaml` — that's consumed by release workflows (`release_fixture_feature.yaml`, `release_fixture_full.yaml`), not by PR CI (`test.yaml`). The `feature_only` flag makes entries opt-in/manual, not automatic. **Implementation**: Add tox environments: ```ini [testenv:fill-amsterdam-30m] commands = fill --evm-bin={env:EVM_BIN:evmone-t8n} \ --fork=Amsterdam --block-gas-limit=30000000 \ -m "not slow and not benchmark" \ -n auto --maxprocesses 6 [testenv:fill-amsterdam-60m] commands = fill --evm-bin={env:EVM_BIN:evmone-t8n} \ --fork=Amsterdam --block-gas-limit=60000000 \ -m "not slow and not benchmark" \ -n auto --maxprocesses 6 [testenv:fill-amsterdam-120m] commands = fill --evm-bin={env:EVM_BIN:evmone-t8n} \ --fork=Amsterdam --block-gas-limit=120000000 \ -m "not slow and not benchmark" \ -n auto --maxprocesses 6 ``` Pin 120M explicitly (do not rely on `DEFAULT_BLOCK_GAS_LIMIT` which is derived from `CURRENT_MAINNET_BLOCK_GAS_LIMIT * 2` and could drift). Add to `test.yaml` as a matrix job: ```yaml multi-gas-limit: name: Amsterdam ${{ matrix.gas-limit }} needs: static runs-on: [self-hosted-ghr, size-xl-x64] strategy: fail-fast: false matrix: gas-limit: [30m, 60m, 120m] steps: # ... setup ... - run: uvx tox -e fill-amsterdam-${{ matrix.gas-limit }} ``` **No `--fill-static-tests`**: Static tests are unaffected by `--block-gas-limit` because their gas comes from the JSON `currentGasLimit` field (`environment.py:19,65`), not from `EnvironmentDefaults.gas_limit`. Static tests are validated by their own `>=Amsterdam` expect sections. **Execute mode**: `--block-gas-limit` only covers fill mode. The execute plugin exposes `--transaction-gas-limit` instead (`execute.py:94,191`). Multi-gas-limit validation for execute mode requires separate work (noted as a gap — matches existing TODO at `benchmark.yaml:88`). **pypy3**: The multi-gas-limit tox envs use CPython. PyPy3 coverage is a nice-to-have but not critical for gas-limit validation — the gas math is identical across interpreters. ### 2. `gas_costs()` Enforcement: mypy Over Custom Lint **Goal**: After Foundation lands, catch calls to `fork.gas_costs()` at Amsterdam+ that omit the `block_gas_limit` parameter. **NOT via `ethereum-spec-lint`**: The existing linter only traverses hardfork modules (`lint/__init__.py:165,58`), not test files or the `execution_testing` package. Adding test-file traversal would require significant new infrastructure. **Preferred approach: mypy enforcement.** After Foundation lands, make `block_gas_limit` a **required parameter** (no default value) for Amsterdam's `gas_costs()` override. Pre-Amsterdam forks keep the default. Then mypy (which already runs in the `static` tox env) catches any call that omits the parameter when targeting Amsterdam+. ```python # Amsterdam's gas_costs() in forks.py — no default for block_gas_limit @classmethod def gas_costs( cls, *, block_number: int = 0, timestamp: int = 0, block_gas_limit: int, # REQUIRED — no default ) -> GasCosts: ``` This is simpler, more robust, and requires zero custom lint infrastructure. ### 3. Benchmark Regression Gate **Goal**: Benchmark exact gas assertions must pass at all block gas limit levels. **Current state**: `benchmark.yaml` sanity checks use `benchmark-gas-values` tox env (`benchmark.yaml:61`), which runs with `--gas-benchmark-values 1` (`tox.ini:160`) and `--generate-pre-alloc-groups` (`tox.ini:161`) at the default block gas limit. Benchmarks currently target Osaka. **Implementation**: When Amsterdam benchmarks are added (Section 2 of this plan), extend the benchmark sanity matrix in `benchmark.yaml`: ```yaml # In benchmark.yaml sanity-checks matrix - name: Benchmark Gas Values (Amsterdam 30M) tox-env: benchmark-gas-values-amsterdam-30m - name: Benchmark Gas Values (Amsterdam 60M) tox-env: benchmark-gas-values-amsterdam-60m - name: Benchmark Gas Values (Amsterdam 120M) tox-env: benchmark-gas-values-amsterdam-120m ``` Each tox env runs: ```bash fill --evm-bin={env:EVM_BIN:evmone-t8n} \ --gas-benchmark-values 1 \ --generate-pre-alloc-groups \ --fork Amsterdam \ --block-gas-limit=<value> \ -m "benchmark and not slow" \ -n auto --maxprocesses 10 --dist=loadgroup ``` (Matches existing `benchmark-gas-values` structure with `--generate-pre-alloc-groups` included.) ### 4. Capture Script Validation **Goal**: After the batch capture script inserts `>=Amsterdam` expect sections, CI must verify the results are correct. **Implementation**: The main `py3` job in `test.yaml` already fills with `--until Amsterdam`, which includes Amsterdam. After the capture script inserts `>=Amsterdam` expect sections, the normal CI fill validates them. The existing `bal` entry in `feature.yaml` provides additional coverage for release builds. ### 5. Known Gaps | Gap | Status | |-----|--------| | **Execute mode multi-gas-limit** | Blocked on `--transaction-gas-limit` vs `--block-gas-limit` distinction. Matches existing TODO (`benchmark.yaml:88`). | | **PyPy3 multi-gas-limit** | Not critical — gas math is interpreter-independent. Can add later if needed. | | **Static tests at varied block gas limits** | By design, static tests use their own `currentGasLimit`. No CI parameterization needed. | ### CI File Changes | File | Change | |------|--------| | `tox.ini` | Add `fill-amsterdam-30m`, `fill-amsterdam-60m`, `fill-amsterdam-120m` tox envs; add `benchmark-gas-values-amsterdam-*` tox envs | | `.github/workflows/test.yaml` | Add `multi-gas-limit` matrix job | | `.github/workflows/benchmark.yaml` | Add Amsterdam multi-gas-limit sanity matrix entries | | `.github/configs/feature.yaml` | No changes needed for CI guardrails (keep `bal` for release builds only) | | No changes to `ethereum-spec-lint` | Use mypy enforcement instead | --- ## Cross-Verification Against Source PRs This plan was verified against the three source PRs and the EIP-8037 spec: | Aspect | Verified Against | Result | |--------|-----------------|--------| | cpsb formula (constants, ceiling division, quantization) | PR #2181 `vm/gas.py`, PR #2235 `Amsterdam.cost_per_state_byte()` | Exact match | | Split gas constants (regular/state per op) | PR #2181 `storage.py`, `system.py`, `interpreter.py`, `eoa_delegation.py` | Exact match | | Reservoir model (`execution_gas`, `gas_left`, `state_gas_reservoir`) | PR #2181 `fork.py:975-979` | Exact match | | TX_MAX_GAS_LIMIT semantics (caps regular, not total) | PR #2181 removes `TransactionGasLimitExceededError` | Confirmed | | Block gas = `max(regular_gas_used, state_gas_used)` | PR #2181 `fork.py:271-274` | Exact match | | 5 internal cpsb call sites | PR #2235 `Amsterdam.gas_costs()`, `_calculate_sstore_gas/refund()`, `_calculate_return_gas()`, `transaction_intrinsic_state_gas()` | All confirmed | | Child frame reservoir (full, no 63/64 rule) | PR #2181 `system.py` | Confirmed | | Exceptional halt (reservoir preserved) | PR #11328 clarification, PR #2181 `interpreter.py` | Confirmed | | `charge_state_gas()` overflow to `gas_left` | PR #2181 `vm/gas.py` | Confirmed | | EIP-7702 auth refund to reservoir + `intrinsic_state_gas` adjustment | PR #2181 `eoa_delegation.py`, PR #11328 | Confirmed | | Code deposit: `cpsb * size` (state) + `keccak_word * ceil(len/32)` (regular) | PR #2181 `interpreter.py:235-249` | Exact match | | Non-static test patterns (~45 bumps, ~25 conditionals, 57 files) | PR #2235 diff | All patterns confirmed | **One stale reference found**: The slides (`EIP-8037-slides.md:198`) reference `fork.safe_gas_limit()` which was superseded by the simpler `env.gas_limit` approach in this plan. The slides should be updated. --- ## Summary | Section | What | |---------|------| | **Foundation** | `block_gas_limit` param through gas + refund chain, caching by `(fork, block_gas_limit, block_number, timestamp)`. Aligns with spec-side `IntrinsicGasCost` dataclass and `validate_transaction(tx, gas_limit)` changes (PR #2181). | | **Non-static** | `auto_gas` metadata (`AutoGasConfig`) on Transaction, resolved via two-pass block-share allocator before signing. Replaces ~45 fragile gas bumps and ~25 fork-conditional patterns from PR #2235. Eligible: re-signable, post-state-only, non-OOG, no gas/balance assertions. Heavy/semantic tests use computed explicit gas via `fork.gas_costs(block_gas_limit=...)` | | **Benchmarks** | Explicit `block_gas_limit` wiring at ~101 call sites (separate from `gas_benchmark_value`) | | **Static** | EIP-8037 dual-dimension gas breaks 698 static tests (cpsb scales state gas costs with block gas limit). Dynamic tx gas adjustment in `state_static.py` (bump to `env.gas_limit` to fund state reservoir — EIP-7825 caps regular gas, not total; confirmed by `TransactionGasLimitExceededError` removal in PR #2181). Reservoir overflow to `gas_left` provides safety net. Single-pass capture via direct `t8n.evaluate()`. Same logic in runtime and capture (no divergence). Structure-preserving JSON/YAML modification. Per-`(d,g,v)` exception guard. Depends on specs PR #2181 landing first. | | **CI** | Multi-gas-limit tox envs in `test.yaml` matrix (30M/60M/120M pinned, non-static only). Benchmark regression gate with Amsterdam sanity matrix in `benchmark.yaml`. mypy enforcement for `gas_costs()` (required param, no default at Amsterdam). Capture validated by existing `py3` fill job. | **Foundation is PR-sized** and unblocks everything else. Can be done first as a standalone patch. **Compared to pinning cpsb at fork level: zero work.** All tests work as-is.

Import from clipboard

Advanced permission required

Your current role can only read. Ask the system administrator to acquire write and comment permission.

This team is disabled

Sorry, this team is disabled. You can't edit this note.

This note is locked

Sorry, only owner can edit this note.

Reach the limit

Sorry, you've reached the max length this note can be.
Please reduce the content or divide it to more notes, thank you!

Import from Gist

Import from Snippet

or

Export to Snippet

Are you sure?

Do you really want to delete this note?
All users will lose their connection.

Create a note from template

Create a note from template

Oops...
This template has been removed or transferred.
Upgrade
All
  • All
  • Team
No template.

Create a template

Upgrade

Delete template

Do you really want to delete this template?
Turn this template into a regular note and keep its content, versions, and comments.

This page need refresh

You have an incompatible client version.
Refresh to update.
New version available!
See releases notes here
Refresh to enjoy new features.
Your user state has changed.
Refresh to load new user state.

Sign in

Sign in via SAML

or

Sign in via GitHub

Help

  • English
  • 中文
  • Français
  • Deutsch
  • 日本語
  • Español
  • Català
  • Ελληνικά
  • Português
  • italiano
  • Türkçe
  • Русский
  • Nederlands
  • hrvatski jezik
  • język polski
  • Українська
  • हिन्दी
  • svenska
  • Esperanto
  • dansk

Documents

Tutorials

Book Mode Tutorial

Slide Example

Resources

Releases

Blog

Policy

Terms

Privacy

Cheatsheet

Syntax Example Reference
# Header Header 基本排版
- Unordered List
  • Unordered List
1. Ordered List
  1. Ordered List
- [ ] Todo List
  • Todo List
> Blockquote
Blockquote
**Bold font** Bold font
*Italics font* Italics font
~~Strikethrough~~ Strikethrough
19^th^ 19th
H~2~O H2O
++Inserted text++ Inserted text
==Marked text== Marked text
[link text](https:// "title") Link
![image alt](https:// "title") Image
`Code` Code 在筆記中貼入程式碼
```javascript
var i = 0;
```
var i = 0;
:smile: :smile: Emoji list
{%youtube youtube_id %} Externals
$L^aT_eX$ LaTeX
:::info
This is a alert area.
:::

This is a alert area.

Versions and GitHub Sync
Upgrade to Prime

  • Edit verison name
  • Delete

revision author avatar     named on  

More Less

Note content is identical to the latest version.
Compare with
    Choose a version
    No search result
    Version not found
Sign in to link this note to GitHub
Learn more
This note is not linked with GitHub
 

Remove version name

Do you want to remove this version name and description?

Transfer ownership

Transfer to
    Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

      Link with GitHub

      Please authorize HackMD on GitHub
      • Please sign in to GitHub and install the HackMD app on your GitHub repo.
      • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
      Learn more  Sign in to GitHub

      Push the note to GitHub Push to GitHub Pull a file from GitHub

        Authorize again
       

      Choose which file to push to

      Select repo
      Refresh Authorize more repos
      Select branch
      Select file
      Select branch
      Choose version(s) to push
      • Save a new version and push
      • Choose from existing versions
      Include title and tags
      Available push count

      Upgrade

      Pull from GitHub

       
      File from GitHub
      File from HackMD

      GitHub Link Settings

      File linked

      Linked by
      File path
      Last synced branch
      Available push count

      Upgrade

      Danger Zone

      Unlink
      You will no longer receive notification when GitHub file changes after unlink.

      Syncing

      Push failed

      Push successfully