Skip to content

feat(l1): bal-devnet-6 support (EIP-7928 BAL + EIP-8037 2D gas)#6574

Merged
edg-l merged 48 commits into
mainfrom
bal-devnet-6-pr
May 13, 2026
Merged

feat(l1): bal-devnet-6 support (EIP-7928 BAL + EIP-8037 2D gas)#6574
edg-l merged 48 commits into
mainfrom
bal-devnet-6-pr

Conversation

@edg-l

@edg-l edg-l commented May 6, 2026

Copy link
Copy Markdown
Contributor

Motivation

Bring ethrex up to bal-devnet-6 spec so we can participate in the devnet. Rolls up the work that landed across bal-devnet-3bal-devnet-4bal-devnet-5bal-devnet-6. The original bal-devnet-6 branch is kept for ongoing devnet work; this *-pr branch is the one intended for merge.

Description

  • EIP-7928 (BAL) — Block Access List support. Fixtures bumped to snøbal-devnet-6 (v1.1.0), BlockAccessIndex widened from u16 to u32, shadow recorder for per-tx access validation in parallel execution, SYSTEM_ADDRESS exclusion.
  • EIP-8037 (2D gas) — full EELS PR Bring nodes to memory on cache miss #2689 semantics:
    • cost_per_state_byte pinned to 1174 for bal-devnet-4..6.
    • Top-level / sub-frame ExceptionalHalt reclassifies state gas → regular dim (both CALL and CREATE return paths).
    • REVERT cascade keeps state_gas_credit_against_drain elevated so credit burns propagate.
    • CREATE-TX top-halt formula: gross_spill − credit_against_drain − already_reclassified reclassified to regular dim.
    • EIP-7702 set_delegation refund: subtracts from state_gas_used / intrinsic_state_gas_charged (bal-devnet-7-prep accounting, SELFDESTRUCT-style). See note below.
    • credit_against_drain capped by regular_gas_reclassified so phantom-drain credits don't cancel real spill.
    • check_2d_gas_allowance runs per-tx before BAL recording (rejected txs don't pollute the BAL).
  • EIP-7976 / EIP-7981 — calldata + access-list floor cost adjustments wired into intrinsic gas + sender refund.
  • Engine / Execution-APIexecution-apis PR 786 FCU semantics: no-reorg restricted to finalized prefix (point 2), -38006 TooDeepReorg returned when the requested reorg depth exceeds ethrex's state-history retention (point 6).
  • ef-tests — runner continues processing blocks after expected-exception (fork-transition tests); 74 bal-devnet-6 Amsterdam fixtures intentionally skipped (see docs/known_issues.md).

bal-devnet-7 direction (intentional)

The bal-devnet-6 spec acknowledges as a known-bug that set_delegation for an existing authority should subtract the auth refund from tx_state_gas (mirroring SELFDESTRUCT) but currently does not. Upstream has a regression test (9b3961a65 test_snobal_block_gas_used_inflated_by_7702_auth_refund) that locks in the un-subtracted behavior for bal-devnet-6 verification. This PR carries the bal-devnet-7-prep subtraction so we are already aligned with where EELS is moving; the cost is that 74 snobal-devnet-6 fixtures expect the old un-subtracted accounting and are skipped via tooling/ef_tests/blockchain/tests/all.rs::SKIPPED_BASE until fixtures bump to snobal-devnet-7.

Checklist

  • Updated STORE_SCHEMA_VERSION (crates/storage/lib.rs) if the PR includes breaking changes to the Store requiring a re-sync. (N/A — no Store schema changes)

@edg-l edg-l force-pushed the bal-devnet-6-pr branch from ec5141c to 8a19f23 Compare May 6, 2026 06:23
@github-actions github-actions Bot added the L1 Ethereum client label May 6, 2026
@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown

Lines of code report

Total lines added: 817
Total lines removed: 8
Total lines changed: 825

Detailed view
+------------------------------------------------------------------------+-------+------+
| File                                                                   | Lines | Diff |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/blockchain/constants.rs                                  | 18    | +2   |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/blockchain/error.rs                                      | 149   | +2   |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/blockchain/fork_choice.rs                                | 169   | +21  |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/blockchain/mempool.rs                                    | 464   | +11  |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/blockchain/payload.rs                                    | 841   | +28  |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/common/types/block_access_list.rs                        | 1107  | +15  |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/common/types/genesis.rs                                  | 1021  | +3   |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/common/validation.rs                                     | 243   | -2   |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/block_producer/payload_builder.rs           | 271   | +37  |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/networking/rpc/engine/fork_choice.rs                     | 565   | +13  |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/networking/rpc/rpc.rs                                    | 1189  | +1   |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/networking/rpc/utils.rs                                  | 335   | +7   |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/vm/backends/levm/mod.rs                                  | 2357  | +147 |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/call_frame.rs                                | 390   | +16  |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/environment.rs                               | 98    | +1   |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/errors.rs                                    | 244   | +3   |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/gas_cost.rs                                  | 875   | +19  |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/hooks/default_hook.rs                        | 536   | +78  |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/hooks/l2_hook.rs                             | 669   | -6   |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/opcode_handlers/stack_memory_storage_flow.rs | 327   | +1   |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/opcode_handlers/system.rs                    | 1063  | +114 |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/utils.rs                                     | 604   | +144 |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/vm.rs                                        | 709   | +151 |
+------------------------------------------------------------------------+-------+------+
| ethrex/crates/vm/lib.rs                                                | 16    | +3   |
+------------------------------------------------------------------------+-------+------+

@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown

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 2.995 ± 0.018 2.974 3.033 1.12 ± 0.01
main_levm_BubbleSort 2.720 ± 0.014 2.708 2.748 1.01 ± 0.01
pr_revm_BubbleSort 3.002 ± 0.017 2.981 3.038 1.12 ± 0.01
pr_levm_BubbleSort 2.685 ± 0.012 2.666 2.700 1.00

Benchmark Results: ERC20Approval

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Approval 979.7 ± 4.2 974.5 989.2 1.00
main_levm_ERC20Approval 1044.4 ± 14.3 1028.3 1074.8 1.07 ± 0.02
pr_revm_ERC20Approval 990.9 ± 9.5 981.2 1005.7 1.01 ± 0.01
pr_levm_ERC20Approval 1026.5 ± 7.0 1015.5 1035.4 1.05 ± 0.01

Benchmark Results: ERC20Mint

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Mint 135.9 ± 2.2 133.6 140.3 1.01 ± 0.02
main_levm_ERC20Mint 151.4 ± 1.3 149.8 153.4 1.12 ± 0.01
pr_revm_ERC20Mint 134.9 ± 0.9 133.9 136.9 1.00
pr_levm_ERC20Mint 151.4 ± 2.0 149.7 156.4 1.12 ± 0.02

Benchmark Results: ERC20Transfer

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Transfer 236.3 ± 1.9 233.7 239.0 1.00
main_levm_ERC20Transfer 254.9 ± 1.9 252.5 258.1 1.08 ± 0.01
pr_revm_ERC20Transfer 236.5 ± 4.2 233.0 245.5 1.00 ± 0.02
pr_levm_ERC20Transfer 252.7 ± 3.1 248.4 257.7 1.07 ± 0.02

Benchmark Results: Factorial

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Factorial 229.3 ± 1.9 227.8 232.7 1.01 ± 0.01
main_levm_Factorial 248.1 ± 10.6 243.1 278.1 1.09 ± 0.05
pr_revm_Factorial 228.1 ± 1.7 226.6 230.9 1.00
pr_levm_Factorial 245.3 ± 1.3 244.1 248.3 1.08 ± 0.01

Benchmark Results: FactorialRecursive

Command Mean [s] Min [s] Max [s] Relative
main_revm_FactorialRecursive 1.644 ± 0.044 1.576 1.716 1.01 ± 0.04
main_levm_FactorialRecursive 9.392 ± 0.026 9.368 9.458 5.77 ± 0.17
pr_revm_FactorialRecursive 1.628 ± 0.047 1.547 1.683 1.00
pr_levm_FactorialRecursive 9.437 ± 0.036 9.384 9.510 5.80 ± 0.17

Benchmark Results: Fibonacci

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Fibonacci 206.9 ± 1.6 204.0 209.3 1.01 ± 0.01
main_levm_Fibonacci 225.1 ± 7.5 220.3 244.3 1.09 ± 0.04
pr_revm_Fibonacci 205.8 ± 0.8 204.7 207.8 1.00
pr_levm_Fibonacci 222.9 ± 3.5 219.6 230.2 1.08 ± 0.02

Benchmark Results: FibonacciRecursive

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_FibonacciRecursive 882.6 ± 8.7 871.1 897.2 1.41 ± 0.02
main_levm_FibonacciRecursive 628.3 ± 19.9 615.7 681.6 1.01 ± 0.03
pr_revm_FibonacciRecursive 851.0 ± 7.3 840.0 857.6 1.36 ± 0.02
pr_levm_FibonacciRecursive 624.2 ± 8.1 614.3 641.5 1.00

Benchmark Results: ManyHashes

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ManyHashes 8.6 ± 0.1 8.4 8.7 1.00
main_levm_ManyHashes 10.2 ± 0.1 10.1 10.3 1.19 ± 0.01
pr_revm_ManyHashes 8.9 ± 0.7 8.6 10.9 1.04 ± 0.08
pr_levm_ManyHashes 10.1 ± 0.2 9.8 10.4 1.18 ± 0.02

Benchmark Results: MstoreBench

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_MstoreBench 270.6 ± 7.6 263.4 283.8 1.19 ± 0.03
main_levm_MstoreBench 230.6 ± 8.2 226.5 253.7 1.01 ± 0.04
pr_revm_MstoreBench 267.8 ± 5.4 263.4 276.0 1.18 ± 0.02
pr_levm_MstoreBench 227.5 ± 0.4 227.0 228.2 1.00

Benchmark Results: Push

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Push 295.4 ± 1.8 293.4 298.8 1.06 ± 0.01
main_levm_Push 280.0 ± 8.5 275.1 303.6 1.01 ± 0.03
pr_revm_Push 294.1 ± 1.2 291.1 295.4 1.06 ± 0.01
pr_levm_Push 277.4 ± 1.7 274.4 279.0 1.00

Benchmark Results: SstoreBench_no_opt

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_SstoreBench_no_opt 167.0 ± 6.2 162.5 179.6 1.67 ± 0.06
main_levm_SstoreBench_no_opt 100.2 ± 0.5 99.7 101.0 1.00
pr_revm_SstoreBench_no_opt 167.2 ± 4.4 162.9 176.6 1.67 ± 0.04
pr_levm_SstoreBench_no_opt 100.4 ± 0.6 99.7 101.9 1.00 ± 0.01

@edg-l edg-l force-pushed the bal-devnet-6-pr branch 4 times, most recently from 7d45d6a to 1744a7c Compare May 6, 2026 14:28
@edg-l edg-l moved this to In Progress in ethrex_l1 May 6, 2026
@edg-l edg-l force-pushed the bal-devnet-6-pr branch from dd3016e to 0976534 Compare May 7, 2026 10:14
@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown

⚠️ Known Issues — intentionally skipped tests

Source: docs/known_issues.md

Known Issues

Tests intentionally excluded from CI. Source of truth for the Known
Issues
section the L1 workflow appends to each ef-tests job summary
and posts as a sticky PR comment.

Hive — bal-devnet-6 Amsterdam consume-engine tests — 32 functions / 54 cases

Same root cause as the blockchain-runner skip list (see EF Tests —
Blockchain
below): snobal-devnet-6 fixtures expect bal-devnet-6 spec
semantics, but our impl runs ahead due to the bal-devnet-7-prep
set_delegation SELFDESTRUCT-style refund subtraction. These fixtures
are routed through hive's eels/consume-engine simulator and produce
the same failures. Excluded via KNOWN_EXCLUDED_TESTS (substring
match on test_<name>[fork_Amsterdam, anchoring to the Amsterdam fork
so legacy Prague/Osaka variants still run).

Affected EELS test functions (32)
  • test_auth_refund_block_gas_accounting
  • test_auth_refund_bypasses_one_fifth_cap
  • test_auth_state_gas_scales_with_cpsb
  • test_auth_with_calldata_and_access_list
  • test_auth_with_multiple_sstores
  • test_authorization_exact_state_gas_boundary
  • test_authorization_to_precompile_address
  • test_authorization_with_sstore
  • test_bal_7702_delegation_clear
  • test_bal_7702_delegation_create
  • test_bal_7702_delegation_update
  • test_bal_7702_double_auth_reset
  • test_bal_7702_double_auth_swap
  • test_bal_7702_null_address_delegation_no_code_change
  • test_bal_all_transaction_types
  • test_bal_selfdestruct_to_7702_delegation
  • test_bal_withdrawal_to_7702_delegation
  • test_duplicate_signer_authorizations
  • test_existing_account_auth_header_gas_used_uses_worst_case
  • test_existing_account_refund
  • test_existing_account_refund_enables_sstore
  • test_existing_auth_with_reverted_execution_preserves_intrinsic
  • test_many_authorizations_state_gas
  • test_mixed_auths_header_gas_used_uses_worst_case
  • test_mixed_new_and_existing_auths
  • test_mixed_valid_and_invalid_auths
  • test_multi_tx_block_auth_refund_and_sstore
  • test_multiple_refund_types_in_one_tx
  • test_simple_gas_accounting
  • test_sstore_state_gas_all_tx_types
  • test_transfer_with_all_tx_types
  • test_varying_calldata_costs

EF Tests — Stateless coverage narrowed to EIP-8025 optional-proofs

make -C tooling/ef_tests/blockchain test calls test-stateless-zkevm
instead of test-stateless. The zkevm@v0.3.3 fixtures are filled against
bal@v5.6.1, out of sync with current bal spec; the broad target trips ~549
fixtures. Re-broaden once the zkevm bundle is regenerated.

Why and resolution path

PR #6527 broadened
test-stateless to extract the entire for_amsterdam/ tree from the
zkevm bundle and run all of it under --features stateless; combined with
this branch's bal-devnet-6+ semantics (and bal-devnet-7-prep
set_delegation re-application) that scope produces ~549
GasUsedMismatch / ReceiptsRootMismatch /
BlockAccessListHashMismatch failures.

test-stateless-zkevm filters cargo to the eip8025_optional_proofs
suite, which still validates the stateless harness without the bal-version
mismatch.

Re-broaden by switching test: back to test-stateless in
tooling/ef_tests/blockchain/Makefile once the zkevm bundle is regenerated
against the current bal spec.

EF Tests — Blockchain bal-devnet-6 (Amsterdam fork) — 74 tests

snobal-devnet-6 fixtures expect bal-devnet-6 spec semantics, but our impl
runs ahead due to the bal-devnet-7-prep set_delegation SELFDESTRUCT-style
refund subtraction. Skipped in
tooling/ef_tests/blockchain/tests/all.rs::SKIPPED_BASE, anchored on
[fork_Amsterdam so legacy Prague / Osaka variants still run.

Bucket breakdown (74 total) and resolution path
EIP Bucket Count
EIP-7702 set_code_txs 24
EIP-7702 set_code_txs_2 15
EIP-7702 gas 1
EIP-8037 state_gas_set_code 17
EIP-8037 state_gas_pricing 1
EIP-8037 state_gas_sstore 1
EIP-7928 block_access_lists_eip7702 8
EIP-7928 block_access_lists 1
EIP-7778 gas_accounting 3
EIP-7708 transfer_logs 1
EIP-7976 refunds 1
EIP-1344 chainid (Amsterdam fork-transition fixture) 1
Total 74

Re-enable once we either:

  • (a) bump fixtures to a snobal-devnet-7 release that locks in the new
    accounting; or
  • (b) revert the bal-devnet-7-prep subtraction for bal-devnet-6
    compatibility.
Full test list (74)

EIP-7702 — for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/

  • delegation_clearing
  • delegation_clearing_and_set
  • delegation_clearing_failing_tx
  • delegation_clearing_tx_to
  • eoa_tx_after_set_code
  • ext_code_on_chain_delegating_set_code
  • ext_code_on_self_delegating_set_code
  • ext_code_on_self_set_code
  • ext_code_on_set_code
  • many_delegations
  • nonce_overflow_after_first_authorization
  • nonce_validity
  • reset_code
  • self_code_on_set_code
  • self_sponsored_set_code
  • set_code_multiple_valid_authorization_tuples_same_signer_increasing_nonce
  • set_code_multiple_valid_authorization_tuples_same_signer_increasing_nonce_self_sponsored
  • set_code_to_log
  • set_code_to_non_empty_storage_non_zero_nonce
  • set_code_to_self_destruct
  • set_code_to_self_destructing_account_deployed_in_same_tx
  • set_code_to_sstore
  • set_code_to_sstore_then_sload
  • set_code_to_system_contract

EIP-7702 — for_amsterdam/prague/eip7702_set_code_tx/set_code_txs_2/

  • call_pointer_to_created_from_create_after_oog_call_again
  • call_to_precompile_in_pointer_context
  • contract_storage_to_pointer_with_storage
  • delegation_replacement_call_previous_contract
  • double_auth
  • pointer_measurements
  • pointer_normal
  • pointer_reentry
  • pointer_resets_an_empty_code_account_with_storage
  • pointer_reverts
  • pointer_to_pointer
  • pointer_to_precompile
  • pointer_to_static
  • pointer_to_static_reentry
  • static_to_pointer

EIP-7702 — for_amsterdam/prague/eip7702_set_code_tx/gas/

  • account_warming

EIP-8037 — for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_set_code/

  • auth_refund_block_gas_accounting
  • auth_refund_bypasses_one_fifth_cap
  • auth_with_calldata_and_access_list
  • auth_with_multiple_sstores
  • authorization_exact_state_gas_boundary
  • authorization_to_precompile_address
  • authorization_with_sstore
  • duplicate_signer_authorizations
  • existing_account_auth_header_gas_used_uses_worst_case
  • existing_account_refund
  • existing_account_refund_enables_sstore
  • existing_auth_with_reverted_execution_preserves_intrinsic
  • many_authorizations_state_gas
  • mixed_auths_header_gas_used_uses_worst_case
  • mixed_new_and_existing_auths
  • mixed_valid_and_invalid_auths
  • multi_tx_block_auth_refund_and_sstore

EIP-8037 — state_gas_pricing/

  • auth_state_gas_scales_with_cpsb

EIP-8037 — state_gas_sstore/

  • sstore_state_gas_all_tx_types

EIP-7928 — for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_eip7702/

  • bal_7702_delegation_clear
  • bal_7702_delegation_create
  • bal_7702_delegation_update
  • bal_7702_double_auth_reset
  • bal_7702_double_auth_swap
  • bal_7702_null_address_delegation_no_code_change
  • bal_selfdestruct_to_7702_delegation
  • bal_withdrawal_to_7702_delegation

EIP-7928 — block_access_lists/

  • bal_all_transaction_types

EIP-7778 — for_amsterdam/amsterdam/eip7778_block_gas_accounting_without_refunds/gas_accounting/

  • multiple_refund_types_in_one_tx
  • simple_gas_accounting
  • varying_calldata_costs

EIP-7708 — for_amsterdam/amsterdam/eip7708_eth_transfer_logs/transfer_logs/

  • transfer_with_all_tx_types

EIP-7976 — for_amsterdam/amsterdam/eip7976_increase_calldata_floor_cost/refunds/

  • gas_refunds_from_data_floor

EIP-1344 — for_amsterdam/istanbul/eip1344_chainid/chainid/

  • chainid (Amsterdam fork-transition fixture)

@edg-l edg-l force-pushed the bal-devnet-6-pr branch from d7dde28 to 71f2fc0 Compare May 7, 2026 11:54
edg-l added a commit that referenced this pull request May 7, 2026
- Preamble no longer says "on the current branch" — the doc lives in-tree
  and applies wherever it's checked out.
- Comment heading drops "on this branch" likewise.
- Bal-devnet-6 section drops the "Tracking via PR #6574" line, which
  pointed back at the PR introducing this doc and stops being meaningful
  the moment that PR merges. The resolution-path bullets already cover
  what's needed to re-enable.
@edg-l edg-l marked this pull request as ready for review May 7, 2026 15:40
@edg-l edg-l requested review from a team, ManuelBilbao, avilagaston9 and ilitteri as code owners May 7, 2026 15:40
@ethrex-project-sync ethrex-project-sync Bot moved this from In Progress to In Review in ethrex_l1 May 7, 2026
@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown

🤖 Kimi Code Review

⚠️ Warning: Diff was truncated from 10193 to 10000 lines. Some changes were not reviewed.

This is a substantial PR implementing the Amsterdam fork (EIP-7928 Block Access Lists, EIP-8037 Multidimensional Gas, EIP-7976/7981 Floor Cost adjustments) with extensive testing. Below is the review feedback organized by category.

Critical Consensus & Safety

1. Fork Choice Reorg Depth Validation

File: crates/blockchain/fork_choice.rs
Lines: 73-81, 95-128

The implementation of execution-apis PR #786 correctly distinguishes between:

  • Spec check: Rejecting reorgs that replace finalized blocks (canonical_link_height < stored_finalized)
  • Implementation cap: The 128-block state-history limit (REORG_DEPTH_LIMIT)

Issue: The error variant TooDeepReorg is used for both cases but with different limit values (spec limit vs. 128). This could confuse operators debugging reorg rejections. Consider splitting into two error variants or clarifying the message.

// Line 118-122: Spec violation
return Err(InvalidForkChoice::TooDeepReorg {
    reorg_depth,
    limit: latest.saturating_sub(stored_finalized), // This is the spec limit
});

// Line 127-131: Implementation limit  
return Err(InvalidForkChoice::TooDeepReorg {
    reorg_depth,
    limit: REORG_DEPTH_LIMIT, // This is the implementation cap
});

2. BAL Index Type Widening (u16 → u32)

File: crates/common/types/block_access_list.rs
Status: ✅ Correctly implemented

The change from u16 to u32 for BlockAccessIndex is properly propagated through:

  • RLP encoding/decoding
  • Storage change tracking
  • Validation logic (validate_bal_indices)
  • Test assertions

The unwrap_or(u32::MAX) fallbacks in payload building (lines 464, 651) are acceptable since >4 billion transactions per block is physically impossible, but consider expect with a message for clarity.

VM & Gas Accounting

3. EIP-8037 Clamp-and-Spill Refund Logic

File: crates/vm/levm/src/vm.rs
Lines: 653-710 (credit_state_gas_refund)

The refund mechanism splitting credits between applied_to_spill and applied_to_drain is subtle. The debug assertions are good, but verify the invariant:

// Line 691-699: This math must exactly match EELS fork.py
let frame_outstanding_delta = self
    .state_gas_spill_outstanding
    .saturating_sub(self.current_call_frame.state_gas_spill_outstanding_snapshot);
let applied_to_spill = clamped.min(frame_outstanding_delta);

Risk: If state_gas_spill_outstanding_snapshot is not correctly captured at frame entry, the split will be wrong, causing consensus failures on contracts with nested CREATE/SSTORE patterns.

4. Intrinsic Gas Parity

File: crates/vm/levm/src/utils.rs (lines 737-890)
File: crates/blockchain/mempool.rs (lines 516-538)

The standalone intrinsic_gas_dimensions function must stay byte-for-byte compatible with VM::get_intrinsic_gas. The parity tests in test_intrinsic_parity_* are essential—ensure they run in CI for every fork.

Note: The Amsterdam CREATE intrinsic split (REGULAR_GAS_CREATE + state portion) is correctly implemented in both paths.

5. System Call Gas Limit

File: crates/vm/backends/levm/mod.rs (line 2542)

System calls now use the actual block_gas_limit instead of i64::MAX for EIP-8037 CPSB calculation:

block_gas_limit: block_header.gas_limit,  // Was i64::MAX
is_system_call: true,

This prevents OOG on state-gas-heavy system calls (EIP-2935, EIP-4788) when cost_per_state_byte is large. ✅ Correct.

Block Building & Validation

6. Builder/Validator Parity

File: test/tests/blockchain/builder_validator_parity_tests.rs

This new test suite is critical infrastructure. It verifies that blocks built via build_payload pass validation via add_block_pipeline_bal.

Gap: The validate_corrupted_bal helper (lines 195-210) mutates the BAL and re-hashes the header, but doesn't test the case where the BAL hash itself is invalid (corrupted but matching hash). Consider adding a test for hash mismatches.

7. 2D Gas Inclusion Check

File: crates/blockchain/payload.rs (lines 656-670)

The EIP-8037 per-transaction 2D check is correctly placed before BAL recording:

// EIP-8037 ... per-tx 2D inclusion check against running block totals. Run BEFORE we touch...
if context.is_amsterdam
    && let Err(e) = check_2d_gas_allowance(...)
{
    return Err(e.into());
}

This prevents rejected transactions from polluting the BAL. ✅ Correct.

Testing & Tooling

8. Fixture Version Management

File: tooling/ef_tests/blockchain/Makefile

The separation of vectors (snobal-devnet-6) and vectors_zkevm (older bal@v5.6.1) prevents stale fixtures from clobbering new implementations. The test-stateless-zkevm target narrowing to eip8025_optional_proofs is a pragmatic workaround until zkevm regenerates fixtures.

Action Required: Add a TODO comment in the Makefile with a link to the zkevm fixture regeneration tracking issue so this narrowing can be removed later.

9. Known Issues Documentation

File: docs/known_issues.md

Excellent documentation of the 74 intentionally skipped Amsterdam tests due to the bal-devnet-6 vs devnet-7-prep semantic mismatch. The categorization by EIP (7702, 8037, 7928, etc.) is helpful for future triage.

Suggestion: Add the specific commit hash or date when these can be re-enabled (e.g., "Re-enable after fixture bump to snobal-devnet-7").

Minor Issues

10. Error Code Documentation

File: crates/networking/rpc/utils.rs (line 25)

The comment lists error codes -38001 to -38006, but the TooDeepReorg variant uses -38006. Verify this doesn't conflict with any existing Engine API specs (it appears correct per PR #786).

11. Prestate Tracer Storage Filtering

File: crates/vm/backends/levm/tracing.rs (lines 188-215)

The build_pre_state_map function filters storage to only include touched slots. The logic for merging initial_accounts_state with pre_snapshot correctly handles slots first accessed in the current transaction.

Optimization: The retain closure (lines 206-213) could be extracted to a helper for readability, but functionally correct.

Summary

Approvals:

  • ✅ EIP-8037 reservoir and clamp-and-spill implementation matches spec complexity
  • ✅ EIP-7928 BAL validation with shadow recorder is robust
  • ✅ Fork choice reorg depth limits correctly implement safety checks
  • ✅ Comprehensive test coverage for edge cases (SELFDESTRUCT refunds, nested halts, 0→N→0 SSTORE)

Recommendations:

  1. High: Verify the credit_state_gas_refund split math against the latest EELS reference implementation commit (e87390b) before merging—this is the highest risk consensus code.
  2. Medium: Add explicit test for TooDeepReorg error message clarity to distinguish spec vs implementation limits.
  3. Low: Add TODO comment in Makefile for zkevm fixture regeneration tracking.

The PR is well-structured for a complex fork implementation. The builder/validator parity tests are particularly valuable for preventing consensus failures in production block building.


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

@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown

🤖 Claude Code Review

Code Review: PR 6574 — bal-devnet-6 Support (EIP-7928 BAL + EIP-8037 2D Gas)

This is a large, well-motivated PR. The architecture and documentation are clear, and the overall approach is sound. There are a few correctness and robustness issues worth addressing before merge.


Overview

  • EIP-7928 (BAL): u16 → u32 index widening, shadow recorder, builder/validator parity.
  • EIP-8037 (2D Gas): Full clamp-and-spill reservoir model, cost_per_state_byte pinned to 1174 for devnet-6.
  • Engine API: execution-apis PR 786 FCU semantics (TooDeepReorg, NewHeadAlreadyCanonical payload building).
  • Tracing: prestateTracer for debug_traceTransaction / debug_traceBlockByNumber.

High Severity

1. Panic on None in NewHeadAlreadyCanonical branch (crates/networking/rpc/engine/fork_choice.rs, around the NewHeadAlreadyCanonical match arm)

get_block_header_by_hash returns Option<BlockHeader>. Using ? on it will propagate a Result error but will panic on None because Option::None cannot be propagated via ? without ok_or(). While the block was validated earlier, defensive handling is required:

// Current (panics on None):
let head_block = context.storage
    .get_block_header_by_hash(fork_choice_state.head_block_hash)?;

// Should be:
let head_block = context.storage
    .get_block_header_by_hash(fork_choice_state.head_block_hash)?
    .ok_or_else(|| RpcErr::Internal("head block header not found".into()))?;

2. prestateTracer misses read-only (warm) accounts (crates/vm/backends/levm/tracing.rs, find_touched_accounts)

The implementation iterates post_cache to find changed accounts. Accounts that are accessed (via EXTCODESIZE, BALANCE, SLOAD, etc.) but not modified never enter post_cache — they stay in initial_accounts_state. Geth's prestateTracer includes all accessed accounts, not just modified ones. This produces an incomplete prestate map for any tx that reads accounts without writing them. This is a semantic gap vs. geth.

3. apply_same_tx_selfdestruct_state_refund iterates accounts with a shrinking pool (crates/vm/levm/src/vm.rs)

When a tx selfdestructs multiple accounts, each iteration computes net_state_gas fresh but the loop also increments state_gas_refund_absorbed each iteration. This means the second account's refund is clamped against a smaller pool than the first, regardless of their combined entitlement. If EELS applies a single aggregate clamp to the total refund, this diverges from reference client behavior. Worth verifying against the EELS apply_same_tx_selfdestruct_state_refund implementation.


Medium Severity

4. Spurious -38006 TooDeepReorg on genesis-head FCU when finalized is nonzero (crates/networking/rpc/engine/fork_choice.rs, canonical_link_height computation)

When head.number == 0, saturating_sub(1) gives canonical_link_height = 0. The spec guard canonical_link_height < stored_finalized evaluates to 0 < stored_finalized, which is true if finalized is at any block above genesis. This triggers -38006 for a legitimate sync-from-scratch FCU(genesis) when the stored finalized is nonzero. A sync scenario where the CL sends FCU(genesis) after the node has been synced before could hit this.

5. L2 payload builder does not restore block_regular_gas_used / block_state_gas_used on apply_plain_transaction error (crates/l2/sequencer/block_producer/payload_builder.rs)

The L2 builder snapshots previous_block_regular_gas_used but only restores it in the invalid-message break branch, not in the apply_plain_transaction Err continue branch. If a tx passes the 2D check inside execute_tx but then fails with a state-gas overflow error (post-counter-update), the 2D counters are permanently inflated for subsequent txs in the same block. Contrast with the L1 builder (apply_tx_to_payload) where check_2d_gas_allowance runs before execution, so the Err path is safe. The L2 builder needs a matching pre-snapshot/restore pattern.

6. trace_block_prestate aborts on any tx execution error

If any tx in the block fails during replay, the function propagates the error and stops, leaving subsequent txs untraced. Geth continues tracing through failed txs (the prestate is still valid). The fix is to catch the error per-tx, record an empty or partial prestate, and continue iterating.


Low Severity / Maintainability

7. Three copies of EIP-7976/7981 floor gas logicdefault_hook.rs, utils.rs (VM), and utils.rs::intrinsic_gas_floor (standalone). The parity tests catch most divergences now, but future edits require triple-updating. A single canonical function would eliminate the risk.

8. test_inner_create_deposit_oog_discard is #[ignore]'d with a stale calibration — the comment assumes cpsb(1_000_000) = 1, but with the current 1174 pin the test would also fail because state_gas_storage_set = 37_568 causes the outer CALL to OOG before reaching CREATE. The ignore comment should note that the test is broken under the 1174 pin, not just "disabled because of calibration."

9. Missing test: EIP-4844 blob tx through intrinsic_gas_dimensions — the parity suite covers plain transfer, CREATE, calldata+access list, and EIP-7702 auth-list, but not blob transactions. The Amsterdam is_amsterdam_activated fast-return in transaction_intrinsic_gas bypasses blob checks; if intrinsic_gas_dimensions handles them differently from the legacy path this would be silently wrong.

10. Deeply nested mixed-success/failure call trees for state_gas_refund_pending — the credit_state_gas_refund accounting chain is correct at 1-hop and 2-hop depth (as tested), but the interaction of pending refunds across 3+ hop call trees with mixed success/failure at intermediate frames is not tested. The debug_assert_eq! guards catch most invariant violations in debug builds, but production builds would silently compute incorrect gas.


Positive Notes

  • The u16 → u32 widening is consistent across all encode/decode/validate paths and is well-guarded by RLP round-trip tests including the WIDE_IDX = u32::MAX / 2 boundary case.
  • check_2d_gas_allowance formula correctly mirrors EELS fork.py:560-578, including the saturating_sub guard for intrinsic-underfunded txs.
  • The parallel execution 2D check is correctly serialized in aggregation order, matching EELS serial semantics despite parallel execution.
  • intrinsic_state_gas_charged double-set guard with debug_assert_eq! is a nice belt-and-suspenders pattern.
  • BAL SYSTEM_ADDRESS filtering is correct and covered by parity tests in both directions.
  • The fe959e3bd fix (build payload on NewHeadAlreadyCanonical with attributes + set_synced()) is correct per spec and the regression it clears (Hive Cancun Engine, Rpc Compat, Devp2p) validates the fix.

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

@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown

🤖 Codex Code Review

  1. High: crates/blockchain/blockchain.rs still rejects any Osaka+ tx with gas_limit > 16_777_216, but the VM explicitly drops that rule from Amsterdam onward in crates/vm/levm/src/hooks/default_hook.rs. The builder path already assumes Amsterdam txs can exceed the regular cap and only reserves min(tx.gas_limit, TX_MAX_GAS_LIMIT_AMSTERDAM) in crates/blockchain/payload.rs. Result: valid Amsterdam reservoir-style txs can never enter the mempool.

  2. Medium: crates/blockchain/fork_choice.rs returns NewHeadAlreadyCanonical before the safe/finalized connectivity checks at crates/blockchain/fork_choice.rs, and crates/networking/rpc/engine/fork_choice.rs now treats that case as a full success and returns a head header for payload building. A disconnected safeHash / finalizedHash inside the finalized prefix can now get VALID (and potentially a payload ID) instead of InvalidForkChoiceState.

  3. Medium: The new prestate tracer is built from pre/post cache diffs in crates/vm/backends/levm/tracing.rs, and the slot filtering at crates/vm/backends/levm/tracing.rs only keeps newly loaded or mutated slots. Pure reads of already-cached state disappear from the output, so SLOAD / BALANCE / EXTCODEHASH-style read sets can be missing from both tx-level and block-level prestate traces. This needs an explicit access tracker, not state diffing.

Open question: crates/vm/backends/levm/mod.rs says system calls “do not use intrinsic gas”, but is_system_call is only consumed by validate_gas_allowance; prepare_execution still unconditionally runs validate_min_gas_limit and add_intrinsic_gas. If that comment reflects the intended semantics, the flag is only half-wired.

Static review only; I didn’t run the suite.


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

@greptile-apps

greptile-apps Bot commented May 7, 2026

Copy link
Copy Markdown

Greptile Summary

This PR implements EIP-7928 (Block Access List) and EIP-8037 (2D gas) for the bal-devnet-6 devnet, rolls up devnet-3→6 spec changes, adds prestateTracer support, and applies execution-apis PR 786 FCU semantics (finalized-prefix no-reorg restriction + -38006 TooDeepReorg). The implementation is complex and heavily commented, with extensive per-EIP test coverage.

  • EIP-8037 (2D gas): Full implementation with clamp-and-spill reservoir model, sub-frame ExceptionalHalt reclassification, REVERT cascade propagation, CREATE top-halt formula, and SSTORE 0\u2192N\u21920 refunds via credit_state_gas_refund.
  • EIP-7928 (BAL): Block-access-list index promoted from u16 to u32 throughout, shadow recorder for per-tx access validation in parallel execution, and SYSTEM_ADDRESS exclusion from BAL entries.
  • Fork choice: NewHeadAlreadyCanonical now builds a payload and marks the node synced; TooDeepReorg error gated on the finalized prefix per execution-apis PR 786.

Confidence Score: 3/5

The 2D gas accounting changes are intricate; the interaction between set_delegation's auth refund and state_gas_reservoir_initial needs verification before merging.

When set_delegation credits the reservoir for existing EIP-7702 authorities, it reduces both state_gas_used and intrinsic_state_gas_charged by the refund but leaves state_gas_reservoir_initial at its pre-refund value. The refund_sender formula subtracts the stale reservoir_initial, causing the regular dimension to be overcounted by one refund-unit per existing authority, inflating block_regular_gas_used and tightening the 2D budget for subsequent transactions. The fork-choice and payload-builder changes are clean and safe.

crates/vm/levm/src/utils.rs (set_delegation refund + reservoir_initial interaction) and crates/vm/levm/src/hooks/default_hook.rs (refund_sender regular-gas formula)

Important Files Changed

Filename Overview
crates/vm/levm/src/vm.rs Central EIP-8037 state: adds 12 new dimensional-gas fields, credit_state_gas_refund, finalize_execution top-level halt/revert wipe, and regular_gas formula rewrite. Complex but well-commented; the state_gas_reservoir_initial / set_delegation interaction is the main risk area.
crates/vm/levm/src/utils.rs set_delegation auth-refund logic and add_intrinsic_gas snapshot capture; state_gas_reservoir_initial is set before set_delegation runs, creating a dimensional overcounting issue for type-4 txs with existing authorities.
crates/vm/levm/src/hooks/default_hook.rs refund_sender formula rewritten for 2D: raw_consumed − intrinsic_state − reservoir_initial − spill + reclassified; apply_same_tx_selfdestruct_state_refund added; validate_min_gas_limit updated for EIP-7976/7981 floor tokens.
crates/vm/levm/src/opcode_handlers/system.rs CALL/CREATE/SELFDESTRUCT updated to use vm.state_gas_new_account (dynamic cpsb); handle_return_call/handle_return_create gain per-frame 2D gas snapshots and asymmetric REVERT/ExceptionalHalt handling.
crates/blockchain/fork_choice.rs Implements execution-apis PR 786 no-reorg restriction, TooDeepReorg gated on finalized prefix, and REORG_DEPTH_LIMIT=128 as implementation cap.
crates/vm/backends/levm/mod.rs check_2d_gas_allowance added for sequential and parallel paths; u16→u32 for BlockAccessIndex; SYSTEM_ADDRESS excluded from BAL entries; shadow recorder for per-tx access validation.
crates/blockchain/payload.rs Transaction processing extracted into apply_tx_to_payload; check_2d_gas_allowance run before BAL recording; block_regular_gas_used / block_state_gas_used tracked per-tx.
crates/vm/levm/src/call_frame.rs 7 new per-frame EIP-8037 snapshot fields added for clamp-and-spill state restoration; all initialized to 0 in new().
crates/vm/levm/src/gas_cost.rs COST_PER_STATE_BYTE promoted to cost_per_state_byte() function (pinned at 1174); EIP-7976/7981 helpers floor_tokens_in_access_list and total_cost_floor_per_token added.
crates/networking/rpc/engine/fork_choice.rs NewHeadAlreadyCanonical now calls set_synced and returns head header for payload building; TooDeepReorg returns -38006.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[VM.execute] --> B[prepare_execution]
    B --> B1[add_intrinsic_gas
sets intrinsic_state_gas_charged
sets state_gas_reservoir_initial]
    B1 --> B2[validate_type_4_tx - set_delegation
adds refund to reservoir
subtracts refund from state_gas_used
NOT updating state_gas_reservoir_initial]
    B2 --> C[snapshot reservoir_at_top_message_entry]
    C --> D{is_create?}
    D -- yes --> E[handle_create_transaction]
    D -- no --> F[run_execution]
    E --> G[finalize_execution]
    F --> G
    G --> G1{Amsterdam and not success?}
    G1 -- REVERT --> G2[refill reservoir with execution_portion]
    G1 -- ExceptionalHalt --> G3[restore reservoir to entry
reclassify gross_spill to regular]
    G1 -- success --> G4[apply_same_tx_selfdestruct_refund]
    G2 --> H[refund_sender]
    G3 --> H
    G4 --> H
    H --> H1[regular_gas = raw_consumed
- intrinsic_state_charged
- reservoir_initial stale after auth-refund
- spill + reclassified]
    H1 --> I[report.gas_used = max_regular + state_gas]
    I --> J[block_regular_gas_used += tx_regular_gas
block_state_gas_used += tx_state_gas]
Loading

Comments Outside Diff (1)

  1. crates/vm/levm/src/utils.rs, line 339-367 (link)

    P2 PR description contradicts final code for set_delegation

    The PR's "Notes for reviewers" states that commit 8045f5337 ("EIP-7702 set_delegation only credits state_gas_reservoir") reverts the state_gas_used/intrinsic_state_gas_charged subtractions and that "re-applying the subtraction is the bal-devnet-7 follow-up." However, commit 03e58d9ff later in this same PR re-applies both subtractions. The final HEAD contains the subtractions, which directly contradicts the description reviewers are given. The description's "Notes for reviewers" section should be updated to reflect the actual final state.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: crates/vm/levm/src/utils.rs
    Line: 339-367
    
    Comment:
    **PR description contradicts final code for `set_delegation`**
    
    The PR's "Notes for reviewers" states that commit `8045f5337` ("EIP-7702 set_delegation only credits state_gas_reservoir") reverts the `state_gas_used`/`intrinsic_state_gas_charged` subtractions and that "re-applying the subtraction is the bal-devnet-7 follow-up." However, commit `03e58d9ff` later in this same PR re-applies both subtractions. The final HEAD contains the subtractions, which directly contradicts the description reviewers are given. The description's "Notes for reviewers" section should be updated to reflect the actual final state.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
crates/vm/levm/src/utils.rs:354-361
**`state_gas_reservoir_initial` not updated alongside reservoir on auth refund**

`set_delegation` adds `refund` to `state_gas_reservoir` but leaves `state_gas_reservoir_initial` (captured moments earlier in `add_intrinsic_gas`) unchanged. The `refund_sender` formula is `regular_gas = raw_consumed − intrinsic_state_charged − reservoir_initial − spill + reclassified`. Because `intrinsic_state_charged` is reduced by `refund` but `reservoir_initial` is not, the result overcounts the regular gas dimension by `refund` (= `STATE_BYTES_PER_NEW_ACCOUNT × cpsb ≈ 131,488`) per existing authority. This flows into `block_regular_gas_used` via `apply_plain_transaction`, so a block with N existing-authority type-4 txs reports `N × refund` extra regular-dimension gas, tightening the 2D budget for subsequent transactions.

### Issue 2 of 2
crates/vm/levm/src/utils.rs:339-367
**PR description contradicts final code for `set_delegation`**

The PR's "Notes for reviewers" states that commit `8045f5337` ("EIP-7702 set_delegation only credits state_gas_reservoir") reverts the `state_gas_used`/`intrinsic_state_gas_charged` subtractions and that "re-applying the subtraction is the bal-devnet-7 follow-up." However, commit `03e58d9ff` later in this same PR re-applies both subtractions. The final HEAD contains the subtractions, which directly contradicts the description reviewers are given. The description's "Notes for reviewers" section should be updated to reflect the actual final state.

Reviews (1): Last reviewed commit: "fix(test): drop redundant module-level i..." | Re-trigger Greptile

Comment thread crates/vm/levm/src/utils.rs Outdated
Comment on lines +354 to +361
let refund = self.state_gas_new_account;
self.state_gas_reservoir = self
.state_gas_reservoir
.checked_add(state_refund)
.ok_or(InternalError::Overflow)?;
// Track as intrinsic state gas adjustment (matches EELS intrinsic_state_gas -= refund).
// Do NOT reduce state_gas_used here — that would inflate regular_gas in block accounting.
self.intrinsic_state_gas_refund = self
.intrinsic_state_gas_refund
.checked_add(state_refund)
.checked_add(refund)
.ok_or(InternalError::Overflow)?;
self.state_gas_used = self.state_gas_used.saturating_sub(refund);
self.intrinsic_state_gas_charged =
self.intrinsic_state_gas_charged.saturating_sub(refund);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 state_gas_reservoir_initial not updated alongside reservoir on auth refund

set_delegation adds refund to state_gas_reservoir but leaves state_gas_reservoir_initial (captured moments earlier in add_intrinsic_gas) unchanged. The refund_sender formula is regular_gas = raw_consumed − intrinsic_state_charged − reservoir_initial − spill + reclassified. Because intrinsic_state_charged is reduced by refund but reservoir_initial is not, the result overcounts the regular gas dimension by refund (= STATE_BYTES_PER_NEW_ACCOUNT × cpsb ≈ 131,488) per existing authority. This flows into block_regular_gas_used via apply_plain_transaction, so a block with N existing-authority type-4 txs reports N × refund extra regular-dimension gas, tightening the 2D budget for subsequent transactions.

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

Comment:
**`state_gas_reservoir_initial` not updated alongside reservoir on auth refund**

`set_delegation` adds `refund` to `state_gas_reservoir` but leaves `state_gas_reservoir_initial` (captured moments earlier in `add_intrinsic_gas`) unchanged. The `refund_sender` formula is `regular_gas = raw_consumed − intrinsic_state_charged − reservoir_initial − spill + reclassified`. Because `intrinsic_state_charged` is reduced by `refund` but `reservoir_initial` is not, the result overcounts the regular gas dimension by `refund` (= `STATE_BYTES_PER_NEW_ACCOUNT × cpsb ≈ 131,488`) per existing authority. This flows into `block_regular_gas_used` via `apply_plain_transaction`, so a block with N existing-authority type-4 txs reports `N × refund` extra regular-dimension gas, tightening the 2D budget for subsequent transactions.

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

edg-l added 5 commits May 8, 2026 10:10
Brings ethrex up to bal-devnet-4 fixture spec. Rolls up EIP-7928,
EIP-8037, EIP-7976, EIP-7981, EIP-7708 and misc BAL validation fixes
into one change set.

BAL (EIP-7928)
- Widen BlockAccessIndex and related recorder/index fields to u32.
- Shadow BAL recorder on per-tx tx_dbs in the parallel validator:
  diff touched_addresses / storage_reads against header BAL to catch
  missing pure-access entries and missing storage_reads.
- Fall back to pre-state code_hash in validate_tx_execution PART B
  when the BAL has no code_changes entry (missing_code_change).
- Stop whitelisting SYSTEM_ADDRESS from unaccessed_pure_accounts via
  system_seed / current_accounts_state scrubs; user-tx touches still
  remove it via the per-tx tracked_accounts path.

EIP-8037 (state gas 2D accounting)
- Dynamic cost_per_state_byte(block_gas_limit), Amsterdam only.
- Two-counter reservoir: state_gas_spill_outstanding +
  state_gas_credit_against_drain for correct revert math across
  nested sub-calls (PR #2733 clamp-and-spill).
- Per-tx 2D inclusion check (PR #2703) in sequential + parallel
  paths: reject with GAS_ALLOWANCE_EXCEEDED when tx.gas worst-case
  exceeds remaining block regular/state budget.
- intrinsic_state_gas immutable across the tx (PR #2711) and
  subtracted separately when deriving block-dimensional regular gas.
- CREATE collision/early/child failure refunds account state gas.
- Same-tx SELFDESTRUCT refunds state gas clamped against
  execution-only state gas (PR #2707), not total state_gas_used.
- Revert-path reservoir refill uses the PR #2733 X - Z formula.
- Top-level reservoir reset on tx failure (PR #2689).
- Zero gas_remaining on precompile exceptional halt so block
  accounting sees the full intrinsic.

Calldata / access-list floors
- TOTAL_COST_FLOOR_PER_TOKEN 10 -> 16 under Amsterdam (EIP-7976).
- Access-list data bytes fold into floor-token count (EIP-7981).

EIP-7708
- Lex-ordered burn logs, no coinbase priority-fee log, SELFDESTRUCT-
  destination coalescing.

Tests
- New levm tests for EIP-7976/7981, EIP-8037 refund/code-deposit/
  top-level-failure paths.
- Skip 6 zkevm@v0.3.0 EIP-8025 fixtures filled against bal@v5.6.1
  (re-enable once zkevm@v0.4.x ships).

Hive consume-engine amsterdam: 1339 pass, 3 remaining (withdrawal
missing-entry cases addressed by PR #6463, cherry-pick pending).
Addresses miss-slot risks found in the builder/validator parity audit of
the bal-devnet-4 rollup. Three builder-side paths could produce blocks
the validator rejects, plus minor hardening.

- Mempool intrinsic gas was using `TX_CREATE_GAS_COST = 53000`
  unconditionally for CREATE. Under Amsterdam the VM charges the
  `(regular, state)` split derived from `intrinsic_gas_dimensions`
  (`REGULAR_GAS_CREATE + STATE_BYTES_PER_NEW_ACCOUNT * cpsb`). Route
  through the shared helper for Amsterdam+ so admission matches VM charge.

- Payload builder (`fill_transactions`) had no EIP-8037 PR #2703 per-tx
  2D inclusion check. A tx passing execution in the builder could still
  fail the check in the validator's aggregation loop and invalidate the
  block. Expose `check_2d_gas_allowance` as pub and call it before any
  BAL touches so rejected txs contribute nothing.

- L2 payload builder recorded sender/recipient BAL touches before
  executing, with no checkpoint/restore for the `undo_last_tx` path
  (invalid L2 out-message) or apply-tx error. Mirror the L1 builder:
  take a `bal_checkpoint` after `set_bal_index`, restore on both
  rejection paths.

- `execute_block_parallel` now computes `is_amsterdam` locally and
  explicitly gates the 2D inclusion loop, keeping the Amsterdam-only
  invariant checkable rather than implicit in the caller.

- `check_2d_gas_allowance` doc now explains why our
  `block_regular_gas_used` aggregates `max(raw_regular, floor)` at
  tx-report time (matching EELS `block_output.block_gas_used`).

- Post-exec 2D overflow rollback in `apply_plain_transaction` replaces
  the unchecked `-=` on `cumulative_gas_spent` with `saturating_sub` +
  `debug_assert`.

- Missing-code-change per-tx BAL validation: when a BAL account has no
  `code_changes` entry (`seeded_pos == 0`), fall back to the
  `system_seed` / store pre-state code_hash. Fixes the remaining
  `missing_code_change` Hive test.

Hive consume-engine amsterdam: 1340 pass, 2 remaining (withdrawal
missing-entry cases addressed by PR #6463).
Regression guards for builder/validator drift on Amsterdam blocks. Both
paths share the VM core but diverge in plumbing (mempool admission,
shadow BAL recorder, 2D inclusion check, BAL checkpoint/restore, coinbase
and SYSTEM_ADDRESS filters), and a disagreement means a missed slot on
devnet that a green ef-tests run cannot catch — ef-tests only consume
blocks, never produce them.

Two groups of tests:

Positive parity (9). Builder produces a legitimate block, validator
pipeline (parallel, BAL-seeded) must accept. Covers: empty block with
only pre-exec system calls; plain transfer; CREATE tx (Amsterdam
intrinsic split); SSTORE state gas; BALANCE of untouched account
(pure-access BAL entry); calldata floor (EIP-7976); access-list floor
(EIP-7981); user-tx touch of SYSTEM_ADDRESS; multi-tx multi-sender
aggregation.

Negative parity (5). Builder produces a legitimate block, the test
corrupts the BAL (drops / mutates / appends entries), re-hashes the
header, and the validator must reject. Each scenario mirrors one of the
Hive test_bal_invalid_* cases fixed in session 3:

  parity_reject_missing_pure_access_account
  parity_reject_surplus_system_address
  parity_reject_missing_storage_read
  parity_reject_missing_storage_change
  parity_reject_missing_code_change

If one of the negative tests ever flips to "accept" the corresponding
BAL-validation check has regressed; treat as P0.
Follow-up to PR #6518 addressing the test-gap list documented in the
session-3 review. Covers every remaining item in TODO.md except the
upstream zkevm@v0.4.x fixture re-enable (tracked externally).

Tests (10 new):

- `test_cpsb_clamp_to_one_for_tiny_gas_limit`,
  `test_cpsb_30m_bin_boundary` — cpsb quantization boundaries. Guards
  against an off-by-one in the `if quantized > CPSB_OFFSET` branch and
  against bin boundary regressions in the 5M-30M range.

- `test_change_variants_rlp_roundtrip_index_above_u16_max` — RLP
  round-trip for all 4 BAL change variants at index 70_000, guarding
  against an accidental revert to the pre-devnet-4 `u16` type that
  would silently truncate high indices.

- `amsterdam_create_intrinsic_matches_vm_dimensions` — mempool
  admission for Amsterdam CREATE txs must match the VM's `(regular,
  state)` split (TX_BASE + REGULAR_GAS_CREATE +
  STATE_BYTES_PER_NEW_ACCOUNT * cpsb), not the legacy 53000.

- `test_intrinsic_parity_plain_transfer` /
  `test_intrinsic_parity_create_tx` /
  `test_intrinsic_parity_with_calldata_and_access_list` /
  `test_intrinsic_parity_eip7702_auth_list` — parity between the
  standalone `intrinsic_gas_dimensions` helper (used by mempool and
  payload builder) and `VM::get_intrinsic_gas` (used during execution).
  Run across Prague / Osaka / Amsterdam at 30M and 120M block gas
  limits.

- `test_call_to_empty_account_with_value_retains_parent_state_gas` —
  EIP-8037 CALL-to-empty-with-value charges new-account state gas in
  the caller's frame, retained across successful parent continuation.
  Pairs with the existing `test_child_charge_then_revert_returns_state_gas_to_parent`
  for the revert direction.

Code polish:

- Clarifying comment on the `frame_outstanding_delta` invariant in
  `credit_state_gas_refund` (`crates/vm/levm/src/vm.rs`). The
  subtraction is fragile — documenting why it must read
  `state_gas_spill_outstanding` and not `state_gas_spill`.

- `debug_assert!` guards on tx count vs `u32::MAX` at each block-exec
  entry (`execute_block`, `execute_block_pipeline`), keeping the
  EIP-7928 `BlockAccessIndex` invariant explicit rather than implicit
  in the ~10 downstream `u32::try_from(...).unwrap_or(u32::MAX)` sites.

Docs:

- `docs/roadmaps/forks-roadmap.md` — EIP-7976 / EIP-7981 flipped
  🔴→✅, EIP-8037 status line expanded (dynamic cpsb, clamp-and-spill,
  2D inclusion, same-tx SELFDESTRUCT refund), priority note updated
  for bal-devnet-4 + PR #6518.

All 478 tests pass. No behavior changes — these are regression guards
and documentation for the bal-devnet-4 work landed in PR #6518.
Two bot reviews (Codex, Claude) + one code reviewer (Greptile) flagged 7
issues. All 7 verified real and fixed in-PR per user request.

Critical (L2 consensus/liveness):

- `crates/l2/sequencer/block_producer/payload_builder.rs` now enforces
  the EIP-8037 PR #2703 per-tx 2D inclusion check against the L2's
  `configured_block_gas_limit` before executing each tx, matching the
  L1 builder. Without this, the L2 builder could reject valid txs or
  accept txs that violate one dimension of the block cap.

- `fill_transactions` now snapshots and restores
  `block_regular_gas_used` / `block_state_gas_used` around
  `apply_plain_transaction` on the `undo_last_tx` rollback path
  (invalid L2 out-message). Previously those two counters stayed
  inflated after a rejected tx, polluting `gas_used()` and the final
  header `gas_used`.

High (mempool DoS avenue):

- `crates/blockchain/mempool.rs::transaction_intrinsic_gas` now
  enforces `max(intrinsic_regular + intrinsic_state, floor)` for
  Amsterdam+, matching the VM's `validate_min_gas_limit` check.
  Previously a tx with mostly zero calldata could pass mempool
  admission at the weighted EIP-2028 cost (400 gas for 100 zero
  bytes) but fail the VM's 6400-gas unweighted floor at block
  inclusion, polluting the pool.

  New standalone helper `intrinsic_gas_floor(tx, fork)` added in
  `crates/vm/levm/src/utils.rs` mirroring `VM::get_min_gas_used` so
  the mempool / payload builder can compute the floor without a VM
  instance. Re-exported from `ethrex-vm`.

Medium:

- `crates/vm/backends/levm/mod.rs` withdrawal index computation
  switched from `.map(|n| n + 1)` to `.map(|n| n.saturating_add(1))`.
  The prior form wraps to 0 in release builds when `n == u32::MAX`
  (the `debug_assert` only fires in debug).

- `crates/vm/levm/src/opcode_handlers/system.rs` adds `debug_assert!`
  at the two reservoir-revert sites verifying
  `outstanding_delta >= credit_against_drain_delta`. If that invariant
  is ever violated, the `saturating_sub` silently mischarges the
  block's regular dimension; a loud debug panic is preferable.

Style:

- `crates/vm/levm/src/gas_cost.rs::access_list_bytes` — replace
  `keys.len() as u64` with `u64::try_from(...).unwrap_or(u64::MAX)`
  for consistency with the rest of the codebase.

- `crates/vm/levm/src/hooks/default_hook.rs::refund_sender` — rename
  the currently-unused `gas_used_pre_refund` parameter to
  `_gas_used_pre_refund` at the signature and drop the interior
  `let _ =` that was silencing it. Expanded doc explains it's kept
  in the signature for future reintroduction.

All 478 tests pass; no behavior changes except the three
intentional ones (mempool floor, L2 2D check, L2 counter rollback)
plus the already-exercised saturation edge.
edg-l added 7 commits May 8, 2026 10:10
…this branch

`make -C tooling/ef_tests/blockchain test` now invokes `test-stateless-zkevm`
instead of `test-stateless`. Reason: zkevm@v0.3.3 fixtures are filled against
bal@v5.6.1, which is out of sync with this branch's bal-devnet-6+ (and
bal-devnet-7-prep set_delegation re-application) gas accounting. PR #6527
on main broadened test-stateless to extract the entire for_amsterdam tree
from the zkevm bundle and run all of it under --features stateless; that
scope trips ~549 fixtures on this branch with GasUsedMismatch /
ReceiptsRootMismatch / BlockAccessListHashMismatch.

Re-broaden once the zkevm bundle is regenerated against the current bal
spec.

docs/known_issues.md updated to surface this in the PR sticky comment +
job summary.

Verified locally end-to-end: make test → 8737 + 93 passed; 0 failed.
The narrowing in the Makefile is repo-wide (lands on main once this PR
merges), not branch-only. Drop the misleading "on this branch" wording
and reframe the reason in terms of the current bal spec rather than
this branch's state.
- Preamble no longer says "on the current branch" — the doc lives in-tree
  and applies wherever it's checked out.
- Comment heading drops "on this branch" likewise.
- Bal-devnet-6 section drops the "Tracking via PR #6574" line, which
  pointed back at the PR introducing this doc and stops being meaningful
  the moment that PR merges. The resolution-path bullets already cover
  what's needed to re-enable.
The Paris withdrawal Block Re-Org hive tests assert the old "accept deep
reorg" engine-API behaviour. Under execution-apis PR #786, the EL must
reject FCUs whose canonical_link_height < stored_finalized with the new
-38006 TooDeepReorg error, and ethrex implements that. The hive engine
simulator has not been updated to PR #786 semantics, so it treats the
spec-correct rejection as a failure.

Adds the 8 affected test names to KNOWN_FLAKY_TESTS in
check-hive-results.sh (substring match), and documents the exclusion in
docs/known_issues.md so it surfaces in the sticky PR comment + job
summary. Re-enable once hive catches up.
Each section now shows a one-paragraph summary by default; affected test
names, bucket tables, resolution paths, and full lists move into
collapsibles. Visible-by-default content drops from ~83 to ~32 lines, so
the sticky PR comment and job summary read at a glance instead of
flooding the page.
…sults check

The 8 Engine withdrawal Block Re-Org (Paris) entries added in 4d217db
aren't flaky — they're spec-correct rejections under execution-apis #786
(`-38006 TooDeepReorg`) that hive hasn't caught up to. Group both
categories under a neutral name and document the split in the array
comment so the distinction is preserved.

Renames the array, the jq-filter variable, and the log-line wording. No
behaviour change.
…me-engine

The Hive Consume Engine Amsterdam job runs the same snobal-devnet-6
fixtures we already skip in the blockchain runner's SKIPPED_BASE, just
routed through the eels/consume-engine simulator — so it surfaces the
same 54 failures across 32 EELS test functions on this branch.

Add a third "(3) bal-devnet-6 fixture-vs-impl mismatch" category to
KNOWN_EXCLUDED_TESTS in check-hive-results.sh. Substrings anchored on
`[fork_Amsterdam` so legacy Prague/Osaka variants still run, mirroring
the SKIPPED_BASE pattern. docs/known_issues.md gains a parallel section
so the sticky PR comment + job summary list both surfaces.

Re-enable once fixtures bump to snobal-devnet-7 or the bal-devnet-7-prep
subtraction is reverted.
@edg-l edg-l force-pushed the bal-devnet-6-pr branch from 88b5e08 to 4664137 Compare May 8, 2026 09:40
edg-l added 5 commits May 8, 2026 13:22
…subtraction

The top-halt reclassification formula subtracted both
`min(credit_against_drain, regular_gas_reclassified)` and
`regular_gas_reclassified` from the gross spill, double-counting the
already-reclassified amount whenever a deeper halt had reclassified its
subtree's spill. Manifested on bal-devnet-6 FullSync as `gas_used`
mismatch on block 597 (Δ = 131,488 = 1·STATE_NEW) for txs with nested
CREATEs that all halt.

Drain credits already affect `state_gas_refund_absorbed` (reduces net
state-gas at finalize) and refill `state_gas_reservoir` via
`credit_state_gas_refund`, so they have no further role at top-halt
reclassification. The simpler rule is: every byte of gross spill not
already reclassified at a deeper halt boundary moves into the regular
dimension here.

`test_top_halt_phantom_drain_does_not_cancel_real_spill` still passes
(the cap was already pinning drain_cap to 0 in that case via the
`min(_, regular_gas_reclassified)` clamp). All 30 EIP-8037 tests green.
…tests

The snobal-devnet-6 ef-test suite (blockchain runner) covers the
behavioral surface these unit tests were guarding: transfer/burn logs
(tests/amsterdam/eip7708_eth_transfer_logs/), calldata + access-list
floor (eip7976/eip7981), SSTORE 0→N→0 refunds (state_gas_sstore),
top-level halt/revert/OOG reservoir reset (state_gas_reservoir), and
CREATE code-deposit OOG paths (state_gas_create). None of these
ethrex scenarios overlap with the 74 fixtures in SKIPPED_BASE.

Kept:
- eip7708: source-level constants vs keccak preimages (fixtures embed
  the hashes directly, can't validate the derivation)
- eip8037_tests: intrinsic_gas_dimensions ↔ VM::get_intrinsic_gas
  parity (ethrex-internal two-path concern, not in EELS)
- eip8037_top_level_failure_tests: phantom-drain and partial-credit
  divergence guards that pin gas_used on halt paths where ethrex's
  reclassification formula has historically drifted by one state-gas
  charge
Punts the parity work to a separate follow-up branch. Removes the
hand-written suite (builder_validator_parity_tests.rs, 14 tests over
~1080 LOC) and the cfg-gated `builder-parity` feature that exercised
build→validate parity across every Amsterdam blockchain ef-test
fixture (test_runner.rs run_builder_parity + collect_header_mismatches,
~170 LOC), the Makefile target, and the Cargo feature flag.

Default `make test` runtime and CI signal are unchanged — the feature
was off by default. Will be reintroduced from scratch in a dedicated
parity PR once devnet-7 fixtures settle.
Prior commit (2496c2b) trimmed `test/tests/levm/eip7708_tests.rs` to
a single constants check, on the assumption the file was fully new in
this PR. It wasn't — 25 of the 28 tests had been on `main` for a while
(landed in PR #6322, well before this branch diverged). Only 3 tests
were genuinely added on this PR:
  - test_burn_logs_emitted_in_lex_ascending_order_three_accounts
  - test_coinbase_priority_fee_does_not_emit_transfer_log
  - test_multi_selfdestruct_dest_emits_single_burn_log_with_combined_balance

Those three are covered by the EELS burn/transfer-logs ef-tests we now
run via the blockchain runner, so dropping them is correct. The other
25 belong to main and should not have been touched here. Restored to
main verbatim and added the new `Environment::is_system_call` field
introduced by this PR.
…zed prefix

Hive `Withdrawals Block Re-Org (Paris)` tests were failing with `-38006`
because ethrex was returning `TooDeepReorg` whenever an FCU's canonical
link was below `stored_finalized`. Per execution-apis PR 786 point 6 the
limit for `-38006` is "specific to the client software" — Erigon,
Nethermind, Besu and geth all scope it to their state-retention/unwind
capability, not to a finalized-crossing policy. ethrex's stricter reading
caused legitimate CL-driven reorgs to be rejected.

CLMock's `DefaultSlotsToFinalized = 2` puts `stored_finalized` two blocks
behind head during canonical chain construction. Hive then asks for an
8- or 10-block sidechain reorg; canonical_link sits below finalized, the
old check fired with `Reorg depth 8 exceeds the client's limit of 2`,
and the test asserted Valid → FAIL.

Spec compliance preserved:
- Point 2 (skip when head is canonical ancestor of finalized) still
  handled by the `NewHeadAlreadyCanonical` branch at fork_choice.rs:81.
- Point 5 (`-38002 Invalid forkchoice state` for disconnected safe/
  finalized) unaffected.
- Point 6 (`-38006 Too deep reorg`) still returned when
  `reorg_depth > REORG_DEPTH_LIMIT (128)` — ethrex's actual state-history
  cap. We now match the industry-wide reading: trust the CL's fork
  choice and only refuse when we physically cannot unwind.

Also drops the 8 hive Block-Re-Org entries from `KNOWN_EXCLUDED_TESTS`
in `.github/scripts/check-hive-results.sh` and the matching section in
`docs/known_issues.md`. Smoke tests stay green (6 passing including
`unfinalized_reorg_deeper_than_32_is_allowed`).
bal-devnet-6 EELS spec (`devnets/bal/6` `eoa_delegation.py`) only does
`message.state_gas_reservoir += STATE_BYTES_PER_NEW_ACCOUNT × cpsb`
on each existing-authority refund. Docstring: "no mutation of
intrinsic_state_gas".

Drops the two extra subtractions previously added in `cefdf69de` that
anticipated EELS PR #2711 / #2816 (the bal-7 `state_refund` channel).
For bal-6 the block-level `state_gas_used` intentionally stays
"inflated" by the auth refund — the refund is sender-side only in this
devnet. Block-accounting subtraction lands in bal-devnet-7 via the
separate `state_refund` channel.

Unblocks the snobal-devnet-6 EIP-7702 fixtures previously allowlisted
under "bal-devnet-6 known-failing fixtures (Amsterdam fork only)" in
tooling/ef_tests/blockchain/tests/all.rs.
Comment thread crates/vm/levm/src/utils.rs

@ElFantasma ElFantasma left a comment

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.

Four findings inline — all minor. None blocking.

Body finding (not inline): the 74-fixture skip list in tools/ef_tests/blockchain/tests/all.rs + docs/known_issues.md is the right contract, but worth wiring a tracking issue so the skip list shrinks as snobal-devnet-7 fixtures land — easy to forget once devnet attention moves on.

Comment thread crates/blockchain/fork_choice.rs
Comment thread crates/vm/levm/src/hooks/default_hook.rs
Comment thread crates/vm/levm/src/hooks/default_hook.rs
Comment thread crates/vm/levm/src/opcode_handlers/system.rs
@edg-l

edg-l commented May 12, 2026

Copy link
Copy Markdown
Contributor Author

Four findings inline — all minor. None blocking.

Body finding (not inline): the 74-fixture skip list in tools/ef_tests/blockchain/tests/all.rs + docs/known_issues.md is the right contract, but worth wiring a tracking issue so the skip list shrinks as snobal-devnet-7 fixtures land — easy to forget once devnet attention moves on.

The skip list is alredy gone in the bal-devnet-7 branch

@edg-l edg-l requested review from ElFantasma and iovoid May 12, 2026 07:37
@edg-l edg-l enabled auto-merge May 13, 2026 11:13
@edg-l edg-l added this pull request to the merge queue May 13, 2026
Merged via the queue into main with commit 80b5951 May 13, 2026
84 checks passed
@edg-l edg-l deleted the bal-devnet-6-pr branch May 13, 2026 13:07
@github-project-automation github-project-automation Bot moved this from In Review to Done in ethrex_l1 May 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L1 Ethereum client

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants