Skip to content

fix(l1): add reverse check in BAL withdrawal validation#6463

Merged
edg-l merged 1 commit into
mainfrom
fix/bal-withdrawal-reverse-check
Apr 23, 2026
Merged

fix(l1): add reverse check in BAL withdrawal validation#6463
edg-l merged 1 commit into
mainfrom
fix/bal-withdrawal-reverse-check

Conversation

@edg-l
Copy link
Copy Markdown
Contributor

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

Summary

  • validate_bal_withdrawal_index only checked the BAL->DB direction (every BAL claim matches actual state). It did not verify the reverse: that every account modified during the withdrawal/request phase has a corresponding BAL entry.
  • A malicious block builder could omit a withdrawal recipient from the BAL. Since bal_to_account_updates derives the state root entirely from BAL entries, the omitted account's withdrawal balance would be excluded from the state trie. The BAL-derived state root would match the tampered header, passing all validation.
  • This adds Part B (DB->BAL) to the withdrawal validation, mirroring the bidirectional check already present in validate_tx_execution.

Perf notes

  • Withdrawal/request phase typically touches <16 accounts, so the reverse iteration is negligible.
  • Pre-state lookups first check the last BAL entry (in-memory), only falling back to db.store when no BAL entry exists for that field.

Test plan

  • All 1963 Amsterdam EF tests pass
  • EEST test for missing withdrawal account in BAL (PR pending in execution-spec-tests)

@github-actions github-actions Bot added the L1 Ethereum client label Apr 10, 2026
@edg-l edg-l moved this to In Progress in ethrex_l1 Apr 10, 2026
@github-actions
Copy link
Copy Markdown

🤖 Kimi Code Review

Overall Assessment: The PR implements critical bidirectional validation for Block Access Lists (BAL) to prevent malicious builders from omitting state changes that occur during the withdrawal/request phase. The logic is sound, but missing storage validation in Part B is a potential vulnerability if withdrawals can modify storage (e.g., via system contracts).

Critical Issues

1. Missing Storage Validation in Part B (Lines 1582-1689)

Part B validates that every modified account has corresponding BAL entries for balance, nonce, and code, but storage changes are not validated. If the withdrawal/request phase modifies storage (e.g., through EIP-7002/EIP-7251 system contracts), a malicious builder could omit these changes from the BAL.

Recommendation: Add storage validation to Part B:

// Check if any storage slots were modified but not recorded in BAL
if let Some(bal_acct) = index.addr_to_idx.get(addr) {
    let bal_acct = &bal.accounts()[*bal_acct];
    // Verify all storage modifications at withdrawal_idx are recorded
    for (slot, value) in &account.storage {
        if !bal_acct.storage_changes.iter().any(|c| c.index == withdrawal_idx && c.slot == *slot && c.value == *value) {
            // Check if this slot was actually modified vs pre-state
            // ...
        }
    }
}

Medium Issues

2. Performance: Full State Iteration (Line 1583)

Iterating over db.current_accounts_state (potentially thousands of accounts) for every block adds O(n) overhead to validation. While necessary for security, ensure this only runs during BAL validation mode, not standard execution.

3. Silent Error Handling (Lines 1597, 1623, 1641, 1680)

Multiple uses of .ok() and unwrap_or_default() treat DB errors as "account not found":

  • If the underlying store fails (disk error, corruption), validation treats it as a default/empty account
  • This could allow a corrupted database to pass validation incorrectly

Suggestion: Distinguish between "account not found" (None) and store errors (Err):

let pre = match db.store.get_account_state(*addr) {
    Ok(Some(acct)) => (acct.balance, acct.nonce, acct.code_hash),
    Ok(None) => (U256::ZERO, 0, *EMPTY_KECCACK_HASH),
    Err(e) => return Err(EvmError::Store(e)),
};

Minor Issues

4. Code Duplication (Lines 1612-1686)

The pattern for fetching "last BAL value or pre-state" is repeated for balance, nonce, and code. Consider extracting a helper function to reduce duplication and potential inconsistencies.

5. Inconsistent Code Hash Handling (Lines 1669-1675)

The code recomputes the keccak hash of BAL code changes, but compares against account.info.code_hash which may be cached. Ensure that empty code consistently maps to EMPTY_KECCACK_HASH in both BAL and account state.

6. Panic Risk (Line 1617)

bal.accounts()[bal_acct_idx] uses unchecked indexing. While bal_acct_idx comes from the index and should be valid, consider using .get() for defense in depth:

let Some(acct) = bal.accounts().get(bal_acct_idx) else {
    return Err(EvmError::Custom(format!("BAL index corruption: invalid index {bal_acct_idx}")));
};

Positive Feedback

  • The bidirectional validation (Part A + Part B) correctly addresses the security requirement that BAL must be complete and accurate
  • Error messages are descriptive and include relevant debugging information (addresses, indices, values)
  • The use of is_unmodified() check at line 1584 efficiently skips untouched accounts

Summary

Approve with changes: Add storage validation to Part B to ensure complete state coverage, and consider handling store errors explicitly rather than silently defaulting.


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

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 10, 2026

Lines of code report

Total lines added: 163
Total lines removed: 0
Total lines changed: 163

Detailed view
+---------------------------------------+-------+------+
| File                                  | Lines | Diff |
+---------------------------------------+-------+------+
| ethrex/crates/vm/backends/levm/mod.rs | 2210  | +163 |
+---------------------------------------+-------+------+

@github-actions
Copy link
Copy Markdown

🤖 Codex Code Review

  1. crates/vm/backends/levm/mod.rs:1593 and crates/vm/backends/levm/mod.rs:1619: the new reverse validation only checks balance / nonce / code_hash, but not storage. That leaves a consensus hole in the post-tx phase: extract_all_requests_levm() runs EIP-7002 / EIP-7251 system-contract calls, which are storage-backed queues, so a malformed BAL can omit storage updates at withdrawal_idx and still pass validation as long as account info is unchanged. The same gap exists in the absent from BAL entirely branch, which compares only (balance, nonce, code_hash) and will miss storage-only mutations on an omitted account. The tx-path validator already has the right pattern for reverse storage checks at crates/vm/backends/levm/mod.rs:1442; the withdrawal-path needs an equivalent seeded-vs-post storage comparison, and the absent-account branch needs to reject storage changes too.

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

@github-actions
Copy link
Copy Markdown

🤖 Claude Code Review

Here is my review of PR #6463:


Review: fix(l1): add reverse check in BAL withdrawal validation

Summary

This PR closes a real security vulnerability: a malicious block builder could omit a withdrawal recipient from the BAL, and since bal_to_account_updates derives the state root exclusively from BAL entries, the omitted account's balance change would be excluded from the trie — allowing a tampered state root to pass validation. Adding Part B (DB→BAL) correctly closes that attack surface.

The fix is logically sound and mirrors the structure of the already-existing validate_tx_execution bidirectional check. A few concerns below.


Security / Correctness

Storage changes are not reverse-checked in Part B

validate_tx_execution Part B (lines 1442–1469) iterates over account.storage and verifies that any storage mutation without a BAL entry at the current index matches the seeded value. The new Part B in validate_bal_withdrawal_index (lines 1621–1689) checks balance, nonce, and code — but omits the storage check entirely.

The withdrawal phase runs extract_all_requests_levm (system calls on predeploys) before process_withdrawals. Those system calls can write storage. A builder who omits a storage_change entry at withdrawal_idx for a predeploy account would not be caught by Part B, even if that account IS in the BAL (the bal_acct_idx branch). Part A only validates entries that the BAL claims exist; Part B is where the omission would need to be caught.

This is the same category of vulnerability that this PR is fixing, applied to storage rather than balance. Adding the same pattern as validate_tx_execution lines 1443–1469 would close it.


DB error silenced at the "absent from BAL" path

Lines 1597–1602:

let pre = db
    .store
    .get_account_state(*addr)
    .ok()                               // DB errors discarded here
    .map(|a| (a.balance, a.nonce, a.code_hash))
    .unwrap_or_default();               // defaults to (0, 0, EMPTY_HASH)

If get_account_state fails (network partition, corruption, etc.), the pre-state silently defaults to zero, meaning a modified account can incorrectly pass the pre != post check. The same pattern exists in validate_tx_execution (line 1346) so this is consistent, but it's a silent failure mode worth documenting or propagating as an error in security-critical validation code.


Correctness Assumptions

.last() assumes no BAL entries between last_tx_idx and withdrawal_idx

Lines 1622–1631 (and analogous nonce/code blocks): the seeded balance is computed as acct.balance_changes.last(). In validate_tx_execution, the equivalent uses partition_point(|c| c.block_access_index <= seed_idx) for precision. The .last() approach is correct only if no entries exist between last_tx_idx and withdrawal_idx = last_tx_idx + 1, which should be true given the BAL structure — but this implicit assumption isn't documented. A brief comment would help maintainability.


Minor

Code mismatch error lacks diagnostic values

Lines 1683–1688: the code mismatch error reports that code changed but doesn't log the actual vs expected hash values:

return Err(EvmError::Custom(format!(
    "BAL validation failed for withdrawal: account {addr:?} code \
     changed during withdrawal/request phase but BAL has no \
     code change at index {withdrawal_idx}"
)));

The balance and nonce errors log their values (account.info.balance, seeded). Adding the hashes to the code error message would make failures easier to diagnose.

Storage not checked for accounts absent from BAL entirely

Lines 1593–1614 handle accounts that have no BAL entry at all — they only compare (balance, nonce, code_hash), not storage. This is symmetric with the gap above (Item 1), and worth closing with the same fix.


Verdict

The fix correctly addresses the stated vulnerability for the balance/nonce/code dimension. Two structural gaps remain: (a) storage mutations during the withdrawal/request phase are not reverse-checked in Part B, and (b) the "absent from BAL" path also skips storage. Both are the same family of omission as the original bug. The test plan correctly notes the EEST test is pending — coverage of these storage cases should be included there.


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

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

🤖 Kimi Code Review

The PR implements critical bidirectional validation for Block Access Lists (BAL) during the withdrawal/request phase. This prevents a consensus vulnerability where malicious builders could omit withdrawal recipients from the BAL, causing state root mismatches.

Security & Correctness

Correctness: The bidirectional check (Part A: BAL→DB, Part B: DB→BAL) correctly ensures that:

  1. Every BAL claim matches the actual DB state (existing)
  2. Every account modified during withdrawal has a corresponding BAL entry (new)

Line 1588-1608: The warm-access artifact detection is sound—comparing pre-state vs post-state prevents false positives on accounts that were merely touched but not mutated during the phase.

Line 1628-1639, 1654-1665, 1680-1696: The fallback logic to db.store.get_account_state when BAL has no changes at withdrawal_idx correctly handles the edge case where the last BAL entry or the original store state should be the valid baseline.

Suggestions

1. Avoid redundant store lookups (Performance)
db.store.get_account_state(*addr) is called multiple times for the same address across balance, nonce, and code checks. Consider fetching once and caching:

// Instead of three separate lookups:
let pre_state = db.store.get_account_state(*addr).ok();
let pre_balance = pre_state.as_ref().map(|a| a.balance).unwrap_or_default();
let pre_nonce = pre_state.as_ref().map(|a| a.nonce).unwrap_or_default();
let pre_code_hash = pre_state.as_ref().map(|a| a.code_hash).unwrap_or(*EMPTY_KECCACK_HASH);

2. Code deduplication (Maintainability)
The pattern for checking balance/nonce/code is identical. Consider extracting a helper to reduce duplication, though the current explicitness is acceptable for critical validation logic.

3. Error message clarity
Line 1599: Include the actual vs expected values in the error message to aid debugging:

return Err(EvmError::Custom(format!(
    "BAL validation failed: account {addr:?} modified during withdrawal \
     (pre: {pre:?}, post: {post:?}) but absent from BAL"
)));

4. Hash calculation consistency
Line 1684-1689: Ensure ethrex_common::utils::keccak matches the EVM's keccak256 implementation exactly. While likely correct, consensus-critical hash operations should use the same primitive throughout the codebase.

Verdict

The security fix is correctly implemented and addresses a legitimate attack vector. The logic for distinguishing warm accesses from genuine mutations is sound. Minor performance optimizations (caching store lookups) are recommended but not blocking.


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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 10, 2026

Greptile Summary

This PR adds the missing DB→BAL reverse check (Part B) to validate_bal_withdrawal_index, fixing a vulnerability where a malicious block builder could omit a withdrawal recipient from the BAL and produce a valid-looking (but wrong) BAL-derived state root.

  • The new Part B checks balance, nonce, and code but omits storage, unlike the analogous Part B in validate_tx_execution (lines 1442–1469). System calls executed during the withdrawal/request phase (EIP-7002, EIP-7251 predeploys) write storage; those mutations are not covered by the new check and could still be silently omitted from the BAL.

Confidence Score: 4/5

Safe to merge for the balance-omission fix, but leaves a storage-mutation gap in Part B that mirrors the original vulnerability class for system-call predeploys.

The primary vulnerability (omitted withdrawal recipient balance) is correctly fixed and all 1963 EF tests pass. However, the new Part B diverges from the validate_tx_execution template by skipping the storage-slot check, which means storage writes by EIP-7002/EIP-7251 system calls during the withdrawal/request phase could still be omitted from the BAL undetected. This is a P1 gap in the stated goal of a bidirectional check.

crates/vm/backends/levm/mod.rs — specifically the Part B storage check absence in validate_bal_withdrawal_index (around line 1664–1689)

Important Files Changed

Filename Overview
crates/vm/backends/levm/mod.rs Adds Part B (DB→BAL) reverse check to validate_bal_withdrawal_index, correctly patching the omitted-withdrawal-recipient vulnerability; however, the new Part B omits the storage-slot check that the equivalent validate_tx_execution Part B performs, leaving system-call storage mutations (EIP-7002, EIP-7251 predeploys) uncovered.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["validate_bal_withdrawal_index(db, bal, withdrawal_idx, index)"] --> B

    subgraph PartA["Part A: BAL → DB"]
        B["For each acct in bal.accounts()"] --> C{Has change at withdrawal_idx?}
        C -- balance/nonce/code/storage --> D[Check DB matches BAL claim]
        D --> E{Match?}
        E -- no --> F[Return Err: mismatch]
        E -- yes --> B
    end

    PartA --> G

    subgraph PartB["Part B: DB → BAL (new)"]
        G["For each (addr, account) in current_accounts_state"] --> H{is_unmodified?}
        H -- yes --> G
        H -- no --> I{addr in BAL index?}
        I -- no --> J[Compare pre-state vs post-state balance/nonce/code_hash only]
        J --> K{Changed?}
        K -- yes --> L[Return Err: absent from BAL]
        K -- no --> G
        I -- yes --> M[Check balance vs last BAL entry]
        M --> N[Check nonce vs last BAL entry]
        N --> O[Check code vs last BAL entry]
        O --> P["⚠️ Storage NOT checked"]
        P --> G
    end

    PartB --> Q[Ok]
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: crates/vm/backends/levm/mod.rs
Line: 1664-1689

Comment:
**Storage mutations missing from Part B**

Part B checks balance, nonce, and code, but does not verify storage slot changes — unlike `validate_tx_execution`'s Part B (lines 1442–1469) which iterates `account.storage` and checks every slot against its BAL entry. During the withdrawal/request phase, system calls (EIP-7002, EIP-7251) write to storage in their predeploy contracts. A block builder could omit those storage changes from the BAL; the BAL-derived state root would then exclude those slot mutations, but this check would not catch the omission.

The analogous pattern from `validate_tx_execution` Part B is:

```rust
// Storage: for each slot written during withdrawal/request phase,
//          verify a corresponding BAL entry exists.
for (key_h256, &value) in &account.storage {
    let slot_u256 = U256::from_big_endian(key_h256.as_bytes());
    let pos = acct
        .storage_changes
        .partition_point(|sc| sc.slot < slot_u256);
    if pos < acct.storage_changes.len() && acct.storage_changes[pos].slot == slot_u256 {
        let sc = &acct.storage_changes[pos];
        if !has_exact_change_storage(&sc.slot_changes, withdrawal_idx) {
            let seeded = sc
                .slot_changes
                .last()
                .map(|c| c.post_value)
                .unwrap_or_default();
            if value != seeded {
                return Err(EvmError::Custom(format!(
                    "BAL validation failed for withdrawal: account {addr:?} storage slot \
                     {slot_u256} changed during withdrawal/request phase ({value}) but BAL \
                     has no change at index {withdrawal_idx} (last_bal={seeded})"
                )));
            }
        }
    }
    // Slot not in BAL storage_changes: loaded from store, skip.
}
```

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

Reviews (1): Last reviewed commit: "fix(l1): add reverse check in BAL withdr..." | Re-trigger Greptile

Comment on lines +1664 to +1689
// Code
if !has_exact_change_code(&acct.code_changes, withdrawal_idx) {
let seeded_hash = acct
.code_changes
.last()
.map(|c| {
if c.new_code.is_empty() {
*EMPTY_KECCACK_HASH
} else {
ethrex_common::utils::keccak(&c.new_code)
}
})
.unwrap_or_else(|| {
db.store
.get_account_state(*addr)
.map(|a| a.code_hash)
.unwrap_or(*EMPTY_KECCACK_HASH)
});
if account.info.code_hash != seeded_hash {
return Err(EvmError::Custom(format!(
"BAL validation failed for withdrawal: account {addr:?} code \
changed during withdrawal/request phase but BAL has no \
code change at index {withdrawal_idx}"
)));
}
}
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 Storage mutations missing from Part B

Part B checks balance, nonce, and code, but does not verify storage slot changes — unlike validate_tx_execution's Part B (lines 1442–1469) which iterates account.storage and checks every slot against its BAL entry. During the withdrawal/request phase, system calls (EIP-7002, EIP-7251) write to storage in their predeploy contracts. A block builder could omit those storage changes from the BAL; the BAL-derived state root would then exclude those slot mutations, but this check would not catch the omission.

The analogous pattern from validate_tx_execution Part B is:

// Storage: for each slot written during withdrawal/request phase,
//          verify a corresponding BAL entry exists.
for (key_h256, &value) in &account.storage {
    let slot_u256 = U256::from_big_endian(key_h256.as_bytes());
    let pos = acct
        .storage_changes
        .partition_point(|sc| sc.slot < slot_u256);
    if pos < acct.storage_changes.len() && acct.storage_changes[pos].slot == slot_u256 {
        let sc = &acct.storage_changes[pos];
        if !has_exact_change_storage(&sc.slot_changes, withdrawal_idx) {
            let seeded = sc
                .slot_changes
                .last()
                .map(|c| c.post_value)
                .unwrap_or_default();
            if value != seeded {
                return Err(EvmError::Custom(format!(
                    "BAL validation failed for withdrawal: account {addr:?} storage slot \
                     {slot_u256} changed during withdrawal/request phase ({value}) but BAL \
                     has no change at index {withdrawal_idx} (last_bal={seeded})"
                )));
            }
        }
    }
    // Slot not in BAL storage_changes: loaded from store, skip.
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/vm/backends/levm/mod.rs
Line: 1664-1689

Comment:
**Storage mutations missing from Part B**

Part B checks balance, nonce, and code, but does not verify storage slot changes — unlike `validate_tx_execution`'s Part B (lines 1442–1469) which iterates `account.storage` and checks every slot against its BAL entry. During the withdrawal/request phase, system calls (EIP-7002, EIP-7251) write to storage in their predeploy contracts. A block builder could omit those storage changes from the BAL; the BAL-derived state root would then exclude those slot mutations, but this check would not catch the omission.

The analogous pattern from `validate_tx_execution` Part B is:

```rust
// Storage: for each slot written during withdrawal/request phase,
//          verify a corresponding BAL entry exists.
for (key_h256, &value) in &account.storage {
    let slot_u256 = U256::from_big_endian(key_h256.as_bytes());
    let pos = acct
        .storage_changes
        .partition_point(|sc| sc.slot < slot_u256);
    if pos < acct.storage_changes.len() && acct.storage_changes[pos].slot == slot_u256 {
        let sc = &acct.storage_changes[pos];
        if !has_exact_change_storage(&sc.slot_changes, withdrawal_idx) {
            let seeded = sc
                .slot_changes
                .last()
                .map(|c| c.post_value)
                .unwrap_or_default();
            if value != seeded {
                return Err(EvmError::Custom(format!(
                    "BAL validation failed for withdrawal: account {addr:?} storage slot \
                     {slot_u256} changed during withdrawal/request phase ({value}) but BAL \
                     has no change at index {withdrawal_idx} (last_bal={seeded})"
                )));
            }
        }
    }
    // Slot not in BAL storage_changes: loaded from store, skip.
}
```

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

@github-actions
Copy link
Copy Markdown

🤖 Codex Code Review

  1. High: the new reverse validation still allows storage-only request/withdrawal mutations to be omitted for accounts that are already present in the BAL. In crates/vm/backends/levm/mod.rs:1619, Part B only checks balance/nonce/code when there is no exact withdrawal_idx entry. But this phase runs extract_all_requests_levm before withdrawals (crates/vm/backends/levm/mod.rs:323), and those Prague system calls are stateful. If a request predeploy writes storage on an account already listed in BAL and the builder omits the corresponding storage_changes at withdrawal_idx, Part A sees nothing and Part B still passes because storage is never compared against the seeded pre-withdrawal value. That means the BAL-derived state root can still exclude a real storage mutation.

  2. High: the “account absent from BAL entirely” branch also misses storage-only mutations. In crates/vm/backends/levm/mod.rs:1593, the code only compares (balance, nonce, code_hash) from store vs post-state. If the withdrawal/request phase mutates storage without changing those three fields, pre == post and validation succeeds even though the storage trie changed. This leaves the exact same consensus hole for any storage-only mutation on an otherwise unchanged account.

Direction looks right, but I would not merge this until storage is included in the reverse check for both branches.


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

@github-actions
Copy link
Copy Markdown

🤖 Claude Code Review

Now I have all the context I need for the review. Here's my analysis:


Review: fix(l1): add reverse check in BAL withdrawal validation

Summary

This PR addresses a real security gap: validate_bal_withdrawal_index only checked BAL→DB (claims match reality) but not DB→BAL (all mutations have corresponding claims). A malicious block builder could omit a withdrawal recipient from the BAL, causing bal_to_account_updates to derive an incorrect state root. The fix is correct in principle and mirrors the existing bidirectional pattern in validate_tx_execution.


Security Concern: Part B lacks storage reverse-check

This is the most significant issue with the PR as written.

validate_tx_execution Part B iterates account.storage for each in-BAL account and validates that any slot present in the BAL's storage_changes structure does in fact have a change recorded at bal_idx if the slot was modified. The new withdrawal Part B has no equivalent loop.

The relevant system calls during the withdrawal/request phase (extract_all_requests_levm) write deterministic storage entries:

  • EIP-4788: writes timestamp → beacon_root and timestamp % BUFFER_LEN → timestamp
  • EIP-2935: writes current block hash to the history contract
  • EIP-7002 / EIP-7251: pop requests from storage queues

These predeploy contracts are almost certainly already present in the BAL from prior transaction accesses. That means they won't fall into the "absent from BAL entirely" branch — they'll take the index.addr_to_idx.get(addr) path. But Part B then only checks balance/nonce/code for such accounts, not storage.

A malicious block builder could record the predeploy in the BAL but omit the storage writes at withdrawal_idx. Part A won't flag this (it only verifies BAL claims, not omissions), and the new Part B won't catch it either. The BAL-derived state root would be missing the storage update.

Suggested addition at the end of the per-account block (after the code check), mirroring validate_tx_execution Part B:

// Storage: reverse-check that each slot in execution state
// has a corresponding BAL claim if it was modified.
for (key_h256, &value) in &account.storage {
    let slot_u256 = U256::from_big_endian(key_h256.as_bytes());
    let pos = acct
        .storage_changes
        .partition_point(|sc| sc.slot < slot_u256);
    if pos < acct.storage_changes.len()
        && acct.storage_changes[pos].slot == slot_u256
    {
        let sc = &acct.storage_changes[pos];
        if !has_exact_change_storage(&sc.slot_changes, withdrawal_idx) {
            let seeded = sc
                .slot_changes
                .last()
                .map(|c| c.post_value)
                .unwrap_or_else(|| {
                    db.store
                        .get_storage_value(*addr, *key_h256)
                        .unwrap_or_default()
                });
            if value != seeded {
                return Err(EvmError::Custom(format!(
                    "BAL validation failed for withdrawal: account {addr:?} \
                     storage slot {slot_u256} changed during withdrawal/request \
                     phase ({value}) but BAL has no change at index {withdrawal_idx}"
                )));
            }
        }
    }
    // Slot not in BAL storage_changes at all: skip (same known gap as tx validation)
}

Minor: unwrap_or_default() for the absent-from-BAL tuple

In the "absent from BAL entirely" branch (around line 1600):

let pre = db
    .store
    .get_account_state(*addr)
    .ok()
    .map(|a| (a.balance, a.nonce, a.code_hash))
    .unwrap_or_default();

If get_account_state returns Err, the tuple (U256, u64, H256) defaults to (0, 0, H256::zero()). But AccountState::default() has code_hash = EMPTY_KECCACK_HASH, not H256::zero(). In production, DynVmDatabase::get_account_state returns Ok(AccountState::default()) for non-existent addresses (via .unwrap_or_default() at the Option level), so this is not a live bug. But test databases or mocks that return Err(DatabaseError::NotFound) would get a false mismatch on code_hash.

The same pattern exists in validate_tx_execution, so this isn't introduced by this PR. But since the new code is already calling db.store.get_account_state, it could use the more defensive form:

let pre_state = db
    .store
    .get_account_state(*addr)
    .unwrap_or_default();   // AccountState::default() has code_hash = EMPTY_KECCACK_HASH
let pre = (pre_state.balance, pre_state.nonce, pre_state.code_hash);

Minor: index access can panic

let acct = &bal.accounts()[bal_acct_idx];

bal_acct_idx comes from an index built against the same bal, so this is safe in practice. The same pattern is used in validate_tx_execution. If the index is ever built from a different bal or the BAL is mutated between index construction and validation, this panics instead of returning an error. A .get(bal_acct_idx).ok_or(...) would be more resilient, but this is consistent with existing style.


What's correct

  • The fix correctly addresses the stated attack: omitting a withdrawal recipient from the BAL no longer passes validation, because Part B detects the balance change without a corresponding BAL entry at withdrawal_idx.
  • Using .last() for the seeded value (rather than partition_point against a seed index as in tx validation) is correct: the withdrawal phase runs after all transactions, so the last BAL entry is always the most recent pre-withdrawal value.
  • The Destroyed/DestroyedModified account statuses are correctly included in Part B (they're not Unmodified).
  • Error messages include sufficient context (address, value, index) for diagnosis.
  • Code structure cleanly mirrors the existing validate_tx_execution pattern.

Bottom line

The balance/nonce/code coverage fix is correct and closes the described attack vector. The missing storage reverse-check for accounts in the BAL is the key thing to address before merge — system calls during request extraction make deterministic storage writes, and without the reverse check, those writes can be silently omitted from the BAL.


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

@edg-l edg-l force-pushed the fix/bal-withdrawal-reverse-check branch from e4894d6 to c67cdb0 Compare April 10, 2026 14:43
Comment thread crates/vm/backends/levm/mod.rs
@github-project-automation github-project-automation Bot moved this from In Review to In Progress in ethrex_l1 Apr 13, 2026
Comment thread crates/vm/backends/levm/mod.rs Outdated
@edg-l edg-l requested review from ElFantasma and iovoid April 14, 2026 08:19
@edg-l edg-l moved this from In Progress to In Review in ethrex_l1 Apr 14, 2026
Comment thread crates/vm/backends/levm/mod.rs Outdated
@github-project-automation github-project-automation Bot moved this from In Review to In Progress in ethrex_l1 Apr 15, 2026
@edg-l edg-l force-pushed the fix/bal-withdrawal-reverse-check branch from 7c830ac to 0e521fb Compare April 23, 2026 07:55
@edg-l edg-l requested a review from iovoid April 23, 2026 08:00
@edg-l edg-l moved this from In Progress to In Review in ethrex_l1 Apr 23, 2026
Add Part B to validate_bal_for_withdrawals_and_requests: for every
account/slot mutated during the withdrawal/request phase, verify a
corresponding BAL entry exists. Closes the gap where a builder could
omit entries from the BAL without being caught.

- Covers balance, nonce, code and storage changes.
- Falls back to the store when seeding from the last BAL entry.
- Propagates db errors instead of defaulting to zero on failure.
@edg-l edg-l force-pushed the fix/bal-withdrawal-reverse-check branch from b5c3cc2 to f5548dd Compare April 23, 2026 08:26
edg-l added a commit that referenced this pull request Apr 23, 2026
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).
edg-l added a commit that referenced this pull request Apr 23, 2026
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).
@edg-l edg-l added this pull request to the merge queue Apr 23, 2026
edg-l added a commit that referenced this pull request Apr 23, 2026
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).
Merged via the queue into main with commit a6bd4f9 Apr 23, 2026
54 checks passed
@edg-l edg-l deleted the fix/bal-withdrawal-reverse-check branch April 23, 2026 12:54
@github-project-automation github-project-automation Bot moved this from In Review to Done in ethrex_l1 Apr 23, 2026
edg-l added a commit that referenced this pull request Apr 23, 2026
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).
edg-l added a commit that referenced this pull request Apr 23, 2026
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).
avilagaston9 pushed a commit that referenced this pull request Apr 27, 2026
Brings in main commits since the prior merge: #6516 EIP-8025 compliance
(Electra-aligned ExecutionRequests typed container in NewPayloadRequest,
MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD corrected from 1 to 2,
to_encoded_requests() helper for EIP-7685 bytes, removal of
ExecutionPayloadHeader/NewPayloadRequestHeader, new byte-oriented
execution_program entrypoint that decodes the wire format internally and
returns valid: false instead of erroring on post-decode failures), #6463
BAL withdrawal reverse check (DB->BAL direction so a malicious builder
can't omit a withdrawal recipient from the BAL), #6505 Kademlia k-bucket
revert (PeerTableServer::spawn no longer takes a node_id), plus snap-sync
observability + dashboards (#6470), pivot-update crash fix (#6475),
weighted peer selection (#6428), txpool_contentFrom/txpool_inspect RPC
(#6446), block-by-block exec fallback (#6464), Amsterdam EELS branch pin
(#6495), and rollup store SQLite v9->v10 migration (#6514).

Conflict resolutions:
- crates/common/types/stateless_ssz.rs: this branch had already moved
  the EIP-8025 SSZ types out of crates/common/types/eip8025_ssz.rs into
  stateless_ssz.rs and tucked the native-rollup containers below them.
  Kept that layout, applied #6516's content updates to the EIP-8025
  section (renamed spec-limit constants, ExecutionRequests typed
  container with to_encoded_requests, dropped header types and their
  tests), pulled in the EncodedRequests import, and kept both the new
  test_execution_requests_to_encoded_bytes and the branch's stateless
  round-trip tests.
- crates/guest-program/src/l1/program.rs: adopted #6516's new
  execution_program(bytes: &[u8], crypto) API with the internal
  decode_eip8025 call, the validate_eip8025_execution helper, and the
  decode-failure test. Rewrote all `eip-8025` feature gates as
  `experimental-devnet` and all `eip8025_ssz::` paths as
  `stateless_ssz::` to match this branch's renames.
- crates/guest-program/bin/{sp1,risc0,zisk,openvm}/src/main.rs: applied
  #6516's simplification (drop decode_eip8025 import, pass &input
  straight to execution_program) under the experimental-devnet feature
  gate. Also flipped the rkyv::rancor::Error import gate from the old
  `eip-8025` name to `experimental-devnet` so the non-devnet build still
  has the import it needs.
- crates/prover/src/backend/exec.rs: kept #6516's updated comment ("raw
  input bytes" instead of "(NewPayloadRequest, ExecutionWitness)") under
  the experimental-devnet feature gate.

Auto-merged regions checked: crates/vm/backends/levm/mod.rs picked up
all of #6463's Part B (DB->BAL) reverse check intact, and
cmd/ethrex/l2/initializers.rs picked up #6505's PeerTableServer::spawn
signature change. Verified cargo fmt --all clean, cargo check --workspace
clean, cargo check --workspace --tests clean, and cargo check -p
ethrex-guest-program --features experimental-devnet --tests clean.
edg-l added a commit that referenced this pull request May 6, 2026
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).
edg-l added a commit that referenced this pull request May 6, 2026
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).
edg-l added a commit that referenced this pull request May 6, 2026
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).
edg-l added a commit that referenced this pull request May 6, 2026
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).
edg-l added a commit that referenced this pull request May 6, 2026
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).
edg-l added a commit that referenced this pull request May 6, 2026
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).
edg-l added a commit that referenced this pull request May 7, 2026
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).
edg-l added a commit that referenced this pull request May 7, 2026
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).
edg-l added a commit that referenced this pull request May 7, 2026
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).
edg-l added a commit that referenced this pull request May 7, 2026
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).
edg-l added a commit that referenced this pull request May 8, 2026
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).
edg-l added a commit that referenced this pull request May 8, 2026
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).
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.

4 participants