Skip to content

chore(l1): sync EIP-8037 with EIPs PR 11573#6546

Open
edg-l wants to merge 2 commits intobal-devnet-5from
bal-devnet-5-eip-8037-spec-sync
Open

chore(l1): sync EIP-8037 with EIPs PR 11573#6546
edg-l wants to merge 2 commits intobal-devnet-5from
bal-devnet-5-eip-8037-spec-sync

Conversation

@edg-l
Copy link
Copy Markdown
Contributor

@edg-l edg-l commented Apr 28, 2026

Summary

ethereum/EIPs#11573 ("Update EIP-8037: fixed CPSB + frame accounting") rewrote large parts of the spec we already implement on this branch. The bulk of the rewrite (frame-end state-gas accounting, deferred SELFDESTRUCT, revert/halt cancellation) was already in place; this PR closes the remaining drift between our code and the new EIP text.

Gaps closed

  1. Fixed CPSB doc + dead-const cleanup — PR 11573 collapses the dynamic, gas-limit-dependent cost_per_state_byte formula (with quantization) into a single permanent constant of 1174. We were already returning 1174, but the function carried a "TEMPORARY for bal-devnet-4" doc and the formula constants (BLOCKS_PER_YEAR, TARGET_STATE_GROWTH_PER_YEAR, CPSB_SIGNIFICANT_BITS, CPSB_OFFSET) were dead code. Removed the consts, rewrote the doc, replaced the obsolete dynamic-formula unit tests in test/tests/levm/eip8037_tests.rs with a single test_cpsb_is_fixed_at_1174 that asserts 1174 across a spread of inputs.

  2. Auth-bytes constant rename — Spec now defines STATE_BYTES_PER_AUTH_BASE = 23 and computes the per-auth state cost additively as (STATE_BYTES_PER_NEW_ACCOUNT + STATE_BYTES_PER_AUTH_BASE) x CPSB. We had precomputed STATE_BYTES_PER_AUTH_TOTAL = 135 and STATE_BYTES_PER_AUTH_ONLY = 23. Renamed _AUTH_ONLY to _AUTH_BASE and dropped _AUTH_TOTAL; consumers in crates/vm/levm/src/state_diff.rs:38 (bytes()), crates/vm/levm/src/utils.rs:570 (intrinsic gas in eip7702_set_access_code), and crates/vm/levm/src/utils.rs:690 (intrinsic_gas_dimensions) now compute the additive form inline. Bytes accounting is unchanged (135 = 112 + 23).

  3. System-call gas-limit bump — PR 11573 introduces SYSTEM_CALL_GAS_LIMIT = 30_000_000 + STATE_BYTES_PER_STORAGE_SET x CPSB x SYSTEM_MAX_SSTORES_PER_CALL (= 30,601,088 with SYSTEM_MAX_SSTORES_PER_CALL = 16, matching MAX_WITHDRAWAL_REQUESTS_PER_BLOCK from EIP-7002). The extra 601,088 is placed in state_gas_reservoir; gas_left keeps the legacy 30M execution budget. System calls remain not subject to the EIP-7825 TX_MAX_GAS_LIMIT cap and don't contribute to block_regular_gas_used/block_state_gas_used. Wired this in crates/vm/levm/src/constants.rs (new constants SYS_CALL_GAS_LIMIT_AMSTERDAM, SYS_CALL_STATE_GAS_RESERVOIR, SYSTEM_MAX_SSTORES_PER_CALL), crates/vm/backends/levm/mod.rs:2533 (system-call env uses the bumped budget on Amsterdam+), and crates/vm/levm/src/utils.rs:451 (reservoir setup in prepare_execution special-cases is_system_call and bypasses TX_MAX_GAS_LIMIT_AMSTERDAM).

  4. Test parity — Replaced the now-obsolete dynamic-CPSB unit tests with one that pins CPSB = 1174 across a spread of block_gas_limit inputs (1, 5M, 30M, 96M, 120M, 500M, u64::MAX).

Verification

  • cargo fmt --all, cargo check --workspace --tests, cargo clippy -p ethrex-levm -p ethrex-vm -p ethrex-blockchain --tests all clean.
  • levm::eip8037_* unit tests: 28/28 pass.
  • blockchain::builder_validator_parity_tests: 14/14 pass.
  • blockchain::mempool_tests: 16/16 pass.
  • make -C tooling/ef_tests/blockchain test against bal@v5.7.0 Amsterdam fixtures: identical pass/fail set vs. baseline (131 pass / 55 fail in eip8037_state_creation_gas_cost_increase, all 55 pre-existing fixture-vs-spec drift). The bal@v5.7.0 set predates PR 11573 so it does not exercise the new system-call gas-limit; that gap will be covered when EEST publishes a fixtures tag matching PR 11573.

Test plan

  • CI green (lint, build, unit, ef-tests-blockchain, ef-tests-state)
  • Spot-check on bal-devnet-5 kurtosis fixture that system calls (EIP-2935, 4788, 7002, 7251) execute to completion without OOG under heavier state-creation load

- pin CPSB at 1174 (drop dead dynamic-formula consts and update doc)
- rename STATE_BYTES_PER_AUTH_ONLY -> STATE_BYTES_PER_AUTH_BASE; drop
  STATE_BYTES_PER_AUTH_TOTAL (compute as NEW_ACCOUNT + AUTH_BASE inline)
- add SYSTEM_MAX_SSTORES_PER_CALL=16, SYS_CALL_STATE_GAS_RESERVOIR=601_088,
  SYS_CALL_GAS_LIMIT_AMSTERDAM=30_601_088; place the extra in
  state_gas_reservoir for system calls on Amsterdam+ and bypass
  TX_MAX_GAS_LIMIT for them
- replace dynamic-formula CPSB unit tests with a fixed-1174 assertion
@github-actions github-actions Bot added the L1 Ethereum client label Apr 28, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

Lines of code report

Total lines added: 20
Total lines removed: 6
Total lines changed: 26

Detailed view
+-----------------------------------------+-------+------+
| File                                    | Lines | Diff |
+-----------------------------------------+-------+------+
| ethrex/crates/vm/backends/levm/mod.rs   | 2362  | +5   |
+-----------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/constants.rs  | 67    | +4   |
+-----------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/gas_cost.rs   | 875   | -5   |
+-----------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/state_diff.rs | 270   | -1   |
+-----------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/utils.rs      | 633   | +11  |
+-----------------------------------------+-------+------+

Address review on PR #6546. Two issues:

1. prepare_execution read self.tx.gas_limit() to compute the reservoir.
   For system calls the tx is built from EIP1559Transaction::default(),
   so tx.gas_limit() = 0 and the reservoir was always 0 — the extra 601k
   we added to env.gas_limit just sat in gas_remaining as unreserved
   execution gas instead of being separated into state_gas_reservoir.
   Read env.gas_limit on the system-call branch (and keep tx.gas_limit()
   on the user-tx branch so eth_call/simulate paths, where env.gas_limit
   defaults to the block max, are unaffected).

2. SYSTEM_MAX_SSTORES_PER_CALL lived in gas_cost.rs while the
   601_088 reservoir literal lived in constants.rs, decoupled. Move
   SYSTEM_MAX_SSTORES_PER_CALL to constants.rs and derive
   SYS_CALL_STATE_GAS_RESERVOIR symbolically so the two stay in
   lockstep.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

Benchmark Results Comparison

No significant difference was registered for any benchmark run.

Detailed Results

Benchmark Results: BubbleSort

Command Mean [s] Min [s] Max [s] Relative
main_revm_BubbleSort 3.014 ± 0.013 2.997 3.040 1.12 ± 0.01
main_levm_BubbleSort 2.717 ± 0.068 2.677 2.895 1.01 ± 0.03
pr_revm_BubbleSort 3.039 ± 0.030 3.008 3.110 1.13 ± 0.01
pr_levm_BubbleSort 2.699 ± 0.011 2.685 2.723 1.00

Benchmark Results: ERC20Approval

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Approval 990.3 ± 12.3 980.8 1017.6 1.00
main_levm_ERC20Approval 1013.8 ± 6.5 1006.9 1028.3 1.02 ± 0.01
pr_revm_ERC20Approval 1003.0 ± 22.6 986.4 1060.2 1.01 ± 0.03
pr_levm_ERC20Approval 1022.5 ± 5.9 1014.9 1033.0 1.03 ± 0.01

Benchmark Results: ERC20Mint

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Mint 132.9 ± 0.9 132.1 135.2 1.00
main_levm_ERC20Mint 150.9 ± 1.5 149.9 154.8 1.14 ± 0.01
pr_revm_ERC20Mint 133.9 ± 1.6 132.8 138.0 1.01 ± 0.01
pr_levm_ERC20Mint 152.4 ± 3.2 149.6 159.8 1.15 ± 0.03

Benchmark Results: ERC20Transfer

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Transfer 233.9 ± 3.8 231.3 242.6 1.00
main_levm_ERC20Transfer 250.9 ± 1.9 248.6 255.0 1.07 ± 0.02
pr_revm_ERC20Transfer 236.9 ± 5.0 233.9 250.8 1.01 ± 0.03
pr_levm_ERC20Transfer 252.1 ± 6.6 248.8 270.5 1.08 ± 0.03

Benchmark Results: Factorial

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Factorial 222.7 ± 0.7 221.8 224.1 1.00
main_levm_Factorial 249.1 ± 4.1 246.3 260.3 1.12 ± 0.02
pr_revm_Factorial 224.2 ± 3.2 221.1 232.0 1.01 ± 0.01
pr_levm_Factorial 246.3 ± 1.5 242.9 248.0 1.11 ± 0.01

Benchmark Results: FactorialRecursive

Command Mean [s] Min [s] Max [s] Relative
main_revm_FactorialRecursive 1.664 ± 0.059 1.516 1.728 1.01 ± 0.04
main_levm_FactorialRecursive 9.110 ± 0.048 9.025 9.177 5.52 ± 0.12
pr_revm_FactorialRecursive 1.650 ± 0.035 1.595 1.700 1.00
pr_levm_FactorialRecursive 9.122 ± 0.054 9.077 9.248 5.53 ± 0.12

Benchmark Results: Fibonacci

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Fibonacci 201.8 ± 1.5 200.5 204.8 1.01 ± 0.01
main_levm_Fibonacci 225.5 ± 4.6 220.4 231.8 1.13 ± 0.02
pr_revm_Fibonacci 200.0 ± 0.8 199.2 201.6 1.00
pr_levm_Fibonacci 221.7 ± 3.9 218.1 229.5 1.11 ± 0.02

Benchmark Results: FibonacciRecursive

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_FibonacciRecursive 880.6 ± 7.6 868.0 892.8 1.06 ± 0.02
main_levm_FibonacciRecursive 829.4 ± 13.8 808.8 853.9 1.00
pr_revm_FibonacciRecursive 875.9 ± 10.8 857.9 891.0 1.06 ± 0.02
pr_levm_FibonacciRecursive 833.9 ± 23.3 814.2 892.4 1.01 ± 0.03

Benchmark Results: ManyHashes

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ManyHashes 8.4 ± 0.1 8.4 8.5 1.00 ± 0.01
main_levm_ManyHashes 9.9 ± 0.1 9.8 10.0 1.18 ± 0.01
pr_revm_ManyHashes 8.4 ± 0.1 8.3 8.4 1.00
pr_levm_ManyHashes 10.0 ± 0.2 9.8 10.5 1.20 ± 0.03

Benchmark Results: MstoreBench

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_MstoreBench 260.6 ± 8.5 253.1 275.3 1.12 ± 0.04
main_levm_MstoreBench 231.7 ± 1.8 230.3 236.3 1.00
pr_revm_MstoreBench 255.8 ± 3.4 253.2 265.1 1.10 ± 0.02
pr_levm_MstoreBench 233.8 ± 1.1 232.0 235.8 1.01 ± 0.01

Benchmark Results: Push

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Push 290.5 ± 1.3 288.5 292.7 1.05 ± 0.01
main_levm_Push 277.9 ± 1.7 276.4 282.2 1.00
pr_revm_Push 291.0 ± 1.3 289.1 294.1 1.05 ± 0.01
pr_levm_Push 289.3 ± 1.5 287.0 291.3 1.04 ± 0.01

Benchmark Results: SstoreBench_no_opt

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_SstoreBench_no_opt 173.0 ± 5.4 167.8 187.3 1.72 ± 0.08
main_levm_SstoreBench_no_opt 102.5 ± 4.4 99.0 111.5 1.02 ± 0.05
pr_revm_SstoreBench_no_opt 172.0 ± 3.4 168.1 177.8 1.71 ± 0.06
pr_levm_SstoreBench_no_opt 100.8 ± 3.1 99.2 109.5 1.00

@edg-l edg-l marked this pull request as ready for review April 28, 2026 16:31
@edg-l edg-l requested a review from a team as a code owner April 28, 2026 16:31
@ethrex-project-sync ethrex-project-sync Bot moved this to In Review in ethrex_l1 Apr 28, 2026
@github-actions
Copy link
Copy Markdown

🤖 Kimi Code Review

Overall Assessment: The PR correctly implements EIP-8037 (multidimensional gas) updates per the simplified EIP-11573 specification. The logic for system call gas reservoir splitting is sound, and safety checks (saturating arithmetic) are properly used. One import issue needs verification.

Issues Found

1. Missing Import Check (Potential Compilation Error)

  • File: crates/vm/levm/src/utils.rs
  • Line: 472 (in the diff hunk)
  • Issue: The code uses SYS_CALL_GAS_LIMIT but the diff does not show this constant being imported. The visible imports only modify gas_cost items.
  • Recommendation: Verify that SYS_CALL_GAS_LIMIT is imported from crate::constants (or wherever it resides). If not present, add:
    use crate::constants::SYS_CALL_GAS_LIMIT;

Correctness & Safety Observations

2. System Call Gas Logic (Correct)

  • The reservoir calculation for system calls in utils.rs (lines 469-472) correctly derives the state gas budget by subtracting the legacy 30M limit from the Amsterdam budget:
    let reservoir = if self.env.is_system_call {
        execution_gas.saturating_sub(SYS_CALL_GAS_LIMIT)
  • This yields SYS_CALL_STATE_GAS_RESERVOIR (601,088) for Amsterdam and 0 for pre-Amsterdam forks, matching the EIP-11573 specification.

3. Constant Calculation Verification

  • File: crates/vm/levm/src/constants.rs
  • The calculation 32 * 1174 * 16 = 601,088 for SYS_CALL_STATE_GAS_RESERVOIR is correct.
  • The sum 30,000,000 + 601,088 = 30,601,088 for SYS_CALL_GAS_LIMIT_AMSTERDAM is correct.

4. EIP-7702 Auth Byte Accounting

  • File: crates/vm/levm/src/state_diff.rs (lines 44-49)
  • The replacement of the magic constant 135 with STATE_BYTES_PER_NEW_ACCOUNT.saturating_add(STATE_BYTES_PER_AUTH_BASE) (112 + 23) improves maintainability and correctly reflects the EIP-8037 byte allocation.

5. Gas Limit Source Selection

  • File: crates/vm/levm/src/utils.rs (lines 461-466)
  • The branch selecting env.gas_limit for system calls vs tx.gas_limit() for user transactions is

Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt

@github-actions
Copy link
Copy Markdown

🤖 Claude Code Review

Now I have enough context to write a thorough review.


PR Review: chore(l1): sync EIP-8037 with EIPs PR 11573

Overview

This PR closes the drift between the implementation and the updated EIP-8037 spec (ethereum/EIPs#11573). The three main changes are: collapsing CPSB from a dynamic formula to the fixed constant 1174, renaming auth-bytes constants to match additive spec terminology, and introducing a per-system-call state-gas reservoir for Amsterdam+. The overall approach is sound and the PR is well-documented.


Correctness

Arithmetic verified:

  • SYS_CALL_STATE_GAS_RESERVOIR = 32 × 1174 × 16 = 601,088
  • SYS_CALL_GAS_LIMIT_AMSTERDAM = 30,000,000 + 601,088 = 30,601,088
  • STATE_BYTES_PER_NEW_ACCOUNT + STATE_BYTES_PER_AUTH_BASE = 112 + 23 = 135 (preserves old STATE_BYTES_PER_AUTH_TOTAL) ✓

is_system_call trust boundary: The flag is only set to true in generic_system_contract_levm (mod.rs:2558). All user-tx paths explicitly set it false (mod.rs:2228, mod.rs:2788). No external surface for bypassing TX_MAX_GAS_LIMIT_AMSTERDAM. ✓


Issues

1. Magic number 1174 duplicated between two definitions

constants.rs:35 inlines the literal 1174 in SYS_CALL_STATE_GAS_RESERVOIR:

pub const SYS_CALL_STATE_GAS_RESERVOIR: u64 =
    crate::gas_cost::STATE_BYTES_PER_STORAGE_SET * 1174 * SYSTEM_MAX_SSTORES_PER_CALL;

And gas_cost.rs:177 returns 1174 from cost_per_state_byte. The comment acknowledges this ("if CPSB ever changes, this constant and cost_per_state_byte must move together"), but there's no compile-time enforcement — a future change to CPSB can easily update the function body and silently leave the reservoir constant stale.

Introducing a named constant (e.g., pub const COST_PER_STATE_BYTE: u64 = 1174; in gas_cost.rs) would let both sites reference the same symbol. cost_per_state_byte could return it, and SYS_CALL_STATE_GAS_RESERVOIR could reference gas_cost::COST_PER_STATE_BYTE directly.

2. Silent zero-reservoir on misconfigured system call

utils.rs:477:

let reservoir = if self.env.is_system_call {
    execution_gas.saturating_sub(SYS_CALL_GAS_LIMIT)

On the happy path, execution_gas = SYS_CALL_GAS_LIMIT_AMSTERDAM and the reservoir is correct. But if gas_limit is not set to the expected SYS_CALL_GAS_LIMIT_AMSTERDAM + TX_BASE_COST (e.g., a future system contract with a different setup), saturating_sub silently yields 0. The system call then has no state-gas budget and will OOG on any new-slot SSTORE. This isn't a security risk (OOG is a safe failure), but it's a silent misconfiguration that would be hard to debug. A debug_assert!(execution_gas >= SYS_CALL_GAS_LIMIT, "system call gas_limit not set correctly") would make this fail loudly in dev builds.

3. Inline STATE_BYTES_PER_NEW_ACCOUNT + STATE_BYTES_PER_AUTH_BASE in three places

The expression now appears at:

  • state_diff.rs:49 (local auth_total_bytes)
  • utils.rs:601-603 (inline in get_intrinsic_gas)
  • utils.rs:723-725 (in intrinsic_gas_dimensions)

The PR description explains why STATE_BYTES_PER_AUTH_TOTAL was removed (to make the additive composition explicit), but three independent inline computations creates a new maintenance trap. A derived constant:

pub const STATE_BYTES_PER_AUTH_TOTAL: u64 =
    STATE_BYTES_PER_NEW_ACCOUNT + STATE_BYTES_PER_AUTH_BASE; // 135

would be the cleanest resolution — it retains the additive transparency in the definition while keeping use-sites DRY.

4. Test gap for the new system-call reservoir path

The eip8037_tests.rs update replaces old CPSB tests with test_cpsb_is_fixed_at_1174 (appropriate), but there are no automated unit tests covering:

  • The is_system_call = true branch in prepare_execution (utils.rs:470-492)
  • That the reservoir equals SYS_CALL_STATE_GAS_RESERVOIR post-call
  • That gas_remaining reflects SYS_CALL_GAS_LIMIT (not SYS_CALL_GAS_LIMIT_AMSTERDAM) after setup

The devnet spot-check in the test plan is manual. Given this is the most novel semantic change in the PR, at minimum a unit test constructing a system-call environment and asserting the post-prepare_execution state would increase confidence.


Minor Notes

  • The _block_gas_limit underscore prefix in cost_per_state_byte correctly signals an unused parameter retained for forward-compatibility. Good.
  • The #[expect(clippy::arithmetic_side_effects, reason = "bounded constants")] attribute in utils.rs:599 correctly suppresses the warning for the inline addition — it's still bounded.
  • Both mod.rs:2529-2534 and utils.rs:456-461 explain the reservoir split. The duplication is intentional (two different call sites), but they'll need to stay synchronized if behavior changes.
  • Removing the #[ignore = "bal-devnet-4: ..."] tests and replacing them with a single parameterized test_cpsb_is_fixed_at_1174 is the right call — no more dead ignored tests in CI.

Summary

The core changes are correct and well-motivated. The main actionable items are:

  1. Extract COST_PER_STATE_BYTE = 1174 as a named constant to bind cost_per_state_byte and SYS_CALL_STATE_GAS_RESERVOIR to the same definition.
  2. Consider re-introducing STATE_BYTES_PER_AUTH_TOTAL as a derived constant to avoid the three-site inline duplication.
  3. Add a unit test for the is_system_call reservoir-split path in prepare_execution.
  4. Optionally, add a debug_assert! to catch misconfigured system-call gas limits early.

Automated review by Claude (Anthropic) · sonnet · custom prompt

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 28, 2026

Greptile Summary

This PR syncs the EIP-8037 implementation with the upstream spec rewrite in ethereum/EIPs#11573: CPSB is now a fixed constant (1174), the auth-bytes naming is updated to the additive STATE_BYTES_PER_NEW_ACCOUNT + STATE_BYTES_PER_AUTH_BASE form, and Amsterdam system calls receive an additional SYS_CALL_STATE_GAS_RESERVOIR (32 × 1174 × 16 = 601,088) split into state_gas_reservoir, keeping the legacy 30M execution budget intact. The arithmetic is consistent across all three affected call sites and the numerical value is unchanged (135 bytes for a full 7702 auth).

Confidence Score: 4/5

Safe to merge; changes are numerically faithful to the spec and all altered paths are guarded by fork checks.

No logic or security issues found. Two P2 style observations: the CPSB literal is duplicated in SYS_CALL_STATE_GAS_RESERVOIR rather than derived from the canonical function, and cost_per_state_byte silently discards its argument. Neither affects correctness today.

crates/vm/levm/src/constants.rs — inlined CPSB literal; crates/vm/levm/src/gas_cost.rs — unused parameter

Important Files Changed

Filename Overview
crates/vm/levm/src/constants.rs Adds three new constants for Amsterdam system-call gas budgeting; arithmetic is correct (32117416 = 601,088; 30M + 601,088 = 30,601,088) but inlines the CPSB literal rather than deriving it from the canonical function.
crates/vm/levm/src/gas_cost.rs Removes dead dynamic-formula constants and the renamed STATE_BYTES_PER_AUTH_TOTAL/ONLY pair; cost_per_state_byte now unconditionally returns 1174 but silently accepts a _block_gas_limit arg.
crates/vm/levm/src/state_diff.rs Renames constants in bytes(), computes auth_total_bytes additively; result is numerically identical (112 + 23 = 135) to the old STATE_BYTES_PER_AUTH_TOTAL.
crates/vm/levm/src/utils.rs System-call reservoir split logic is correctly gated on is_system_call; pre-consumes exactly SYS_CALL_STATE_GAS_RESERVOIR leaving 30M execution budget; ordinary-tx path unchanged.
crates/vm/backends/levm/mod.rs Correctly selects SYS_CALL_GAS_LIMIT_AMSTERDAM vs SYS_CALL_GAS_LIMIT based on fork; the + TX_BASE_COST padding for intrinsic gas consumption is preserved.
test/tests/levm/eip8037_tests.rs Old #[ignore]-tagged dynamic-formula tests removed; replaced with a single parametric test_cpsb_is_fixed_at_1174 covering the full expected spread of block-gas-limit values.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[generic_system_contract_levm] -->|fork >= Amsterdam| B["sys_call_gas_limit = SYS_CALL_GAS_LIMIT_AMSTERDAM = 30_601_088"]
    A -->|fork < Amsterdam| C["sys_call_gas_limit = SYS_CALL_GAS_LIMIT = 30_000_000"]
    B --> D["env.gas_limit = sys_call_gas_limit + TX_BASE_COST"]
    C --> D
    D --> E[prepare_execution]
    E --> F{fork >= Amsterdam?}
    F -->|No| G[No reservoir split]
    F -->|Yes| H{is_system_call?}
    H -->|Yes| I["execution_gas = env.gas_limit - total_gas\nreservoir = execution_gas - SYS_CALL_GAS_LIMIT = 601_088\ngas_remaining for execution = 30_000_000"]
    H -->|No - user tx| J["regular_gas_budget = TX_MAX_GAS_LIMIT_AMSTERDAM - regular_gas\ngas_left = min(regular_gas_budget, execution_gas)\nreservoir = execution_gas - gas_left"]
    I --> K["state_gas_reservoir = reservoir\ngas_remaining reduced by reservoir"]
    J --> K
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: crates/vm/levm/src/constants.rs
Line: 34-35

Comment:
**Hardcoded CPSB value creates maintenance coupling**

`SYS_CALL_STATE_GAS_RESERVOIR` inlines `1174` directly rather than deriving it through `cost_per_state_byte`. This means a future change to the CPSB value in `gas_cost::cost_per_state_byte` will silently leave `SYS_CALL_STATE_GAS_RESERVOIR` (and therefore `SYS_CALL_GAS_LIMIT_AMSTERDAM`) stale. The comment already flags this risk, but it may be worth at minimum a `debug_assert_eq!(SYS_CALL_STATE_GAS_RESERVOIR, gas_cost::STATE_BYTES_PER_STORAGE_SET * cost_per_state_byte(0) * SYSTEM_MAX_SSTORES_PER_CALL)` in a test to make the divergence loud.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: crates/vm/levm/src/gas_cost.rs
Line: 176-178

Comment:
**Silently-ignored parameter may mislead callers**

The `_block_gas_limit` argument is now permanently unused but still accepted. Any caller that constructs its own `block_gas_limit` value and passes it expecting it to influence the cost will silently get `1174` regardless. Consider making the contract explicit by changing the signature to `cost_per_state_byte() -> u64` (a breaking change at call sites, but honest) or at least adding an inline note to make it clear the call sites don't need to supply a meaningful value.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(l1): read env.gas_limit for system-c..." | Re-trigger Greptile

@github-actions
Copy link
Copy Markdown

🤖 Codex Code Review

Findings

  1. crates/vm/backends/levm/mod.rs:2544 still only tops up TX_BASE_COST for Amsterdam system calls. But add_intrinsic_gas() also charges calldata intrinsic gas, and the new split in crates/vm/levm/src/utils.rs:475 takes that missing amount out of the newly added reservoir. For the 32-byte beacon-root/block-hash calls at crates/vm/backends/levm/mod.rs:2361 and crates/vm/backends/levm/mod.rs:2385, LEVM therefore does not actually grant the full fixed SYS_CALL_GAS_LIMIT_AMSTERDAM budget the comment describes. If the protocol intent is “30M execution + fixed state reservoir, no intrinsic charged”, this needs to add the full intrinsic gas of the synthetic call, not just the base cost.

  2. The new comment in crates/vm/levm/src/utils.rs:463 says Amsterdam should read “whichever source carries the budget”, but the code still uses self.tx.gas_limit() for every non-system path at crates/vm/levm/src/utils.rs:470. That is wrong for GenericTransaction simulation paths: env_from_generic() stores the requested gas in env.gas_limit at crates/vm/backends/levm/mod.rs:2763, while vm_from_generic() wraps it in a default tx with gas_limit == 0 at crates/vm/backends/levm/mod.rs:2820. So Amsterdam eth_call/create_access_list still skips the reservoir split entirely and can simulate different GAS/state-gas behavior than a real tx whenever the caller supplies gas above the 16,777,216 regular cap.

Otherwise, the fixed-CPSB change and the AUTH_* byte-accounting refactor look internally consistent.

I couldn’t run the Rust tests in this sandbox because cargo/rustup need writable home dirs under /home/runner/.cargo and /home/runner/.rustup, which are read-only here.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

Comment on lines +34 to +35
pub const SYS_CALL_STATE_GAS_RESERVOIR: u64 =
crate::gas_cost::STATE_BYTES_PER_STORAGE_SET * 1174 * SYSTEM_MAX_SSTORES_PER_CALL;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Hardcoded CPSB value creates maintenance coupling

SYS_CALL_STATE_GAS_RESERVOIR inlines 1174 directly rather than deriving it through cost_per_state_byte. This means a future change to the CPSB value in gas_cost::cost_per_state_byte will silently leave SYS_CALL_STATE_GAS_RESERVOIR (and therefore SYS_CALL_GAS_LIMIT_AMSTERDAM) stale. The comment already flags this risk, but it may be worth at minimum a debug_assert_eq!(SYS_CALL_STATE_GAS_RESERVOIR, gas_cost::STATE_BYTES_PER_STORAGE_SET * cost_per_state_byte(0) * SYSTEM_MAX_SSTORES_PER_CALL) in a test to make the divergence loud.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/vm/levm/src/constants.rs
Line: 34-35

Comment:
**Hardcoded CPSB value creates maintenance coupling**

`SYS_CALL_STATE_GAS_RESERVOIR` inlines `1174` directly rather than deriving it through `cost_per_state_byte`. This means a future change to the CPSB value in `gas_cost::cost_per_state_byte` will silently leave `SYS_CALL_STATE_GAS_RESERVOIR` (and therefore `SYS_CALL_GAS_LIMIT_AMSTERDAM`) stale. The comment already flags this risk, but it may be worth at minimum a `debug_assert_eq!(SYS_CALL_STATE_GAS_RESERVOIR, gas_cost::STATE_BYTES_PER_STORAGE_SET * cost_per_state_byte(0) * SYSTEM_MAX_SSTORES_PER_CALL)` in a test to make the divergence loud.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 176 to 178
pub fn cost_per_state_byte(_block_gas_limit: u64) -> u64 {
1174
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Silently-ignored parameter may mislead callers

The _block_gas_limit argument is now permanently unused but still accepted. Any caller that constructs its own block_gas_limit value and passes it expecting it to influence the cost will silently get 1174 regardless. Consider making the contract explicit by changing the signature to cost_per_state_byte() -> u64 (a breaking change at call sites, but honest) or at least adding an inline note to make it clear the call sites don't need to supply a meaningful value.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/vm/levm/src/gas_cost.rs
Line: 176-178

Comment:
**Silently-ignored parameter may mislead callers**

The `_block_gas_limit` argument is now permanently unused but still accepted. Any caller that constructs its own `block_gas_limit` value and passes it expecting it to influence the cost will silently get `1174` regardless. Consider making the contract explicit by changing the signature to `cost_per_state_byte() -> u64` (a breaking change at call sites, but honest) or at least adding an inline note to make it clear the call sites don't need to supply a meaningful value.

How can I resolve this? If you propose a fix, please make it concise.

/// the block gas limit; PR 11573 collapses that to a single fixed constant, so
/// `block_gas_limit` is no longer an input. The argument is retained to keep call
/// sites stable; if a future EIP re-derives CPSB, change the body.
pub fn cost_per_state_byte(_block_gas_limit: u64) -> u64 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we still need this function if the value will be left fixed?

Copy link
Copy Markdown
Contributor

@ElFantasma ElFantasma left a comment

Choose a reason for hiding this comment

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

Just a couple of nit suggestions

// existing 30M execution margin under the higher state-creation costs.
//
// 30_000_000 + 32 * 1174 * 16 = 30_601_088
pub const SYS_CALL_GAS_LIMIT_AMSTERDAM: u64 = SYS_CALL_GAS_LIMIT + SYS_CALL_STATE_GAS_RESERVOIR;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

SYS_CALL_GAS_LIMIT_AMSTERDAM = 30_000_000 + 32 * 1174 * 16 = 30_601_088 checks out against the spec. SYSTEM_MAX_SSTORES_PER_CALL = 16 is correctly anchored to MAX_WITHDRAWAL_REQUESTS_PER_BLOCK (EIP-7002, the loosest bound). The hardcoded 1174 here (rather than calling cost_per_state_byte()) is justified by the doc — cost_per_state_byte isn't const. Worth a const_assert! (or similar) somewhere that pins cost_per_state_byte(0) == 1174 so the coupling can't drift silently if a future PR parameterizes CPSB.

// paths `env.gas_limit` may default to the block max while the wrapping
// `tx` has `gas_limit = 0`. So we read whichever source carries the budget
// for the path we're on.
let gas_limit = if self.env.is_system_call {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Doc/impl mismatch: the surrounding comment says "For ordinary user txs the two agree (set together at the env-builder), but for eth_call/simulation paths env.gas_limit may default to the block max while the wrapping tx has gas_limit = 0. So we read whichever source carries the budget for the path we're on" — but the branch below only switches on is_system_call, not on is_simulation/eth_call. So for an Amsterdam+ eth_call (non-system-call) we hit the tx.gas_limit() arm, which is 0, making execution_gas = 0 and the reservoir/gas_left logic effectively no-op. In practice that's probably fine because eth_call doesn't depend on the GAS opcode return value the way real txs do, but the source comment is misleading as written.

Two possible fixes — pick whichever matches your intent:

  • Tighten the comment to system-call vs user-tx only and drop the eth_call digression, since the code doesn't actually treat eth_call specially.
  • Add an eth_call/simulation-aware branch (e.g., key on self.env.is_simulation if that flag exists) that also reads env.gas_limit instead of tx.gas_limit(), if you do want simulations to get the same reservoir handling as system calls.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L1 Ethereum client

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

4 participants