Skip to content

perf(l1): bal optimistic merkleization on validation path#6655

Open
edg-l wants to merge 1 commit into
bal-devnet-7-prfrom
perf/bal-optimistic-merkleization
Open

perf(l1): bal optimistic merkleization on validation path#6655
edg-l wants to merge 1 commit into
bal-devnet-7-prfrom
perf/bal-optimistic-merkleization

Conversation

@edg-l
Copy link
Copy Markdown
Contributor

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

Summary

Decouple BAL-merkleization from EVM execution on the engine_newPayload validation path. Synthesize per-field state deltas from the input BlockAccessList before thread::scope and run merkle stages B/C/D in parallel with execution + warming. validate_block_access_list_hash post-execution remains the correctness gate; on mismatch the optimistic merkle output is discarded by the existing ? error flow.

Closes #6584. Validation path only (builder still streams). Amsterdam+ only.

Design

  • New BalSynthesisItem { address, balance, nonce, code_hash, code, added_storage } in crates/common/types/block_access_list.rs carries per-field optionals — no Option<AccountInfo> blob, so a balance-only ETH-transfer recipient doesn't fabricate zero-nonce / EMPTY_KECCACK_HASH deltas that would corrupt the trie.
  • BalStateWorkItem refactored to per-field optionals so the streaming flow and the synthesis flow lower into the same shape.
  • handle_merkleization_balhandle_merkleization_bal_from_updates(prepared: FxHashMap<Address, BalSynthesisItem>, parent_header). Stage A (channel drain) deleted from the BAL path.
  • execute_block_pipeline synthesizes optimistic_updates + optimistic_witness before thread::scope. No channel + no drain thread on the BAL path.
  • EVM side: LEVM::execute_block_pipeline / execute_block_parallel and Evm::execute_block_pipeline now take Option<Sender>. The validation-with-BAL caller passes None and the EVM skips its own bal_to_account_updates + merkleizer.send (levm/mod.rs:1034-1038) — that was redundant work (and did pre-state lookups we no longer need).
  • Witness accumulator built once at the call site from the synthesis map. generate_witness_from_account_updates only needs address + added_storage.keys(); the bulk of the witness comes from DatabaseLogger.state_accessed.
  • merkle_start_instant captured so the pipeline metric line shows start_delay (how much of execution the merkleizer overlapped with).

Perf refinements (inside handle_merkleization_bal_from_updates)

  • Parallel state-trie pre-warm via accounts.par_iter() before Stage B — warms RocksDB OS pages + trie node arena for the addresses Stage B/C are about to touch. Replaces what the old streaming bal_to_account_updates was doing via store.prefetch_accounts.
  • Hashed-key sort on item.added_storage before the per-slot insert loop so trie inserts walk node-arena paths in order instead of bucket order.

Correctness

  • removed / removed_storage intentionally not inferred from BAL. Stage B's value.is_zero() + Stage C's EIP-161 normalization collapse accounts identically to the streaming path. Verified for plain selfdestruct (EIP-6780 only emits record_balance_change(to, 0) on same-tx-created accounts; pre-existing contracts degrade to a balance transfer).
  • Accounts appearing only via storage_reads are omitted from the synthesized map (no state delta to apply; witness path captures their reads from DatabaseLogger.state_accessed).

Tests

  • 13 unit tests for synthesize_bal_updates in block_access_list.rs: read-only skip, pure storage write, balance-only / nonce-only / code-only (regression cases for the partial-info bug avoided by the per-field design), last-wins on each delta vec, zero storage retained, defensive empty slot_changes, account creation, EIP-6780 selfdestruct collapse.
  • The existing two-pass parallel runner in tooling/ef_tests/blockchain/test_runner.rs:192 exercises this code path on every Amsterdam fixture (pass 2 calls add_block_pipeline(block, Some(bal))).

Follow-ups (not in this PR)

  • Parallelize synthesize_bal_updates itself via bal.accounts().par_iter(). The keccak(new_code) of each code-changed account is the serial-on-main-thread hotspot.
  • Stage B → Stage C is a hard serial barrier (per-account pipelining would let Stage C start consuming accounts as soon as their storage root is ready).

Test plan

  • cargo test -p ethrex-common synthesize_tests (13/13)
  • cargo fmt --check
  • cargo clippy -p ethrex-blockchain --no-deps -- -D warnings (clean; pre-existing MAX_BLOB_TX_SIZE warning is feature-gated by c-kzg on main)
  • Hive bal group
  • EF blockchain tests (tooling/ef_tests/blockchain) — exercises the new path via the two-pass parallel runner on every Amsterdam fixture

@github-actions github-actions Bot added L1 Ethereum client performance Block execution throughput and performance in general labels May 14, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 14, 2026

⚠️ 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.

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-7 semantics 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.

@edg-l edg-l moved this to In Progress in ethrex_l1 May 14, 2026
@edg-l edg-l changed the title perf(l1): BAL optimistic merkleization on validation path perf(l1): bal optimistic merkleization on validation path May 14, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 14, 2026

Lines of code report

Total lines added: 285
Total lines removed: 0
Total lines changed: 285

Detailed view
+-------------------------------------------------+-------+------+
| File                                            | Lines | Diff |
+-------------------------------------------------+-------+------+
| ethrex/crates/blockchain/blockchain.rs          | 2553  | +19  |
+-------------------------------------------------+-------+------+
| ethrex/crates/common/types/block_access_list.rs | 1413  | +261 |
+-------------------------------------------------+-------+------+
| ethrex/crates/common/types/mod.rs               | 31    | +1   |
+-------------------------------------------------+-------+------+
| ethrex/crates/vm/backends/levm/mod.rs           | 2419  | +4   |
+-------------------------------------------------+-------+------+

@edg-l edg-l force-pushed the perf/bal-optimistic-merkleization branch 3 times, most recently from cea052f to 815f446 Compare May 14, 2026 12:02
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 14, 2026

Benchmark Block Execution Results Comparison Against Main

Command Mean [s] Min [s] Max [s] Relative
base 65.928 ± 0.349 65.646 66.840 1.00
head 66.258 ± 0.445 65.726 67.196 1.01 ± 0.01

@edg-l edg-l force-pushed the perf/bal-optimistic-merkleization branch 4 times, most recently from ce666ea to 01db258 Compare May 14, 2026 14:40
Decouple merkleization from EVM execution when the validation path
receives a BAL: synthesize per-field deltas from the input
BlockAccessList pre-execution and run merkle stages B/C/D in parallel
with execution + warming. validate_block_access_list_hash remains the
post-execution correctness gate.

Closes #6584.
@edg-l edg-l force-pushed the perf/bal-optimistic-merkleization branch from 01db258 to 43ae7c5 Compare May 14, 2026 14:43
@edg-l edg-l marked this pull request as ready for review May 15, 2026 14:52
@edg-l edg-l requested a review from a team as a code owner May 15, 2026 14:52
@ethrex-project-sync ethrex-project-sync Bot moved this from In Progress to In Review in ethrex_l1 May 15, 2026
@github-actions
Copy link
Copy Markdown

🤖 Codex Code Review

Findings

  1. High: blockchain.rs:478 builds optimistic_witness with only address and added_storage. That vector is later fed into blockchain.rs:1943, but the replay logic in store.rs:1869 only mutates nonce/balance/code hash when update.info is present and only removes accounts when update.removed is set. On the BAL path, cached witnesses will therefore miss balance-only, nonce-only, code-only, account-creation, and removal transitions when --precompute-witnesses is enabled. I would keep the synthesized BAL map for merkleization, but build witness updates from the prestate-aware bal_to_account_updates path (or another full AccountUpdate builder), not from this storage-only skeleton.

  2. Low: blockchain.rs:924 uses par_iter() directly, which runs on Rayon’s global pool, while the existing streaming merkle path is explicitly bounded by blockchain.rs:699 self.merkle_pool. The new BAL prewarm can therefore spill extra work onto the global pool and oversubscribe CPU/DB readers on large BALs, which is the opposite of what you want on engine_newPayload. Running that prewarm inside self.merkle_pool.install(...) or folding it into the scoped workers would keep resource usage predictable.

No other consensus/gas-accounting problems stood out in the touched logic. I did not run the test suite; this is a static review of the diff.


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

@github-actions
Copy link
Copy Markdown

🤖 Claude Code Review

Now I have a complete picture of the PR. Let me write the review.


PR Review: perf(l1): BAL optimistic merkleization on validation path

Overall: The design is sound and the implementation is careful. Decoupling synthesis from execution is a legitimate optimization, the invariants are well-maintained, and the 13 unit tests cover the critical edge cases. A few items below deserve attention before merging.


Correctness

EIP-161 normalization — correct. Stage C at blockchain.rs:1123-1130 already handles the removed: false hardcode safely:

// EIP-161: remove empty accounts (zero nonce, zero balance, empty code, empty storage)
if account_state != AccountState::default() {
    state_trie.insert(...)?;
} else {
    state_trie.remove(path)?;
}

A selfdestructed account (BAL records balance = 0, no nonce/code change) will be compared against AccountState::default() using pre-state nonce/code. If it was a fresh same-tx creation, the empty check removes it; if it had pre-existing nonce/code, the trie keeps the entry with balance=0. This matches the streaming path behavior as claimed.

Witness correctness — correct. generate_witness_from_account_updates at blockchain.rs:1579-1588 only reads account_update.address and account_update.added_storage.keys() — it does not consume info, code, or removed. The synthesized witness (address + added_storage) is sufficient and matches the actual usage.

Channel invariant — correct. The invariant optimistic_updates.is_some() ↔ rx_for_merkle.is_none() is established cleanly via bal.map(...) vs if bal.is_some(). Both expect() calls are logically justified.


Issues

1. duration_since may panic in the metrics path (blockchain.rs:2150-2153)

let merkle_start_delay_ms = merkle_start_instant
    .duration_since(exec_merkle_start)   // panics if exec_merkle_start > merkle_start_instant
    .as_secs_f64()
    * 1000.0;

Instant::duration_since panics on some platforms if the argument is later than self. Although thread scheduling virtually guarantees merkle_start_instant >= exec_merkle_start, a panic inside the logging function would crash a validator node over a metric. Compare with blockchain.rs:2093 which already uses saturating_duration_since for overlap:

.saturating_duration_since(exec_end_instant)

Use saturating_duration_since here as well for consistency and safety.

2. Pre-warm opens one state_trie per account (blockchain.rs, the new parallel pre-warm block)

accounts
    .par_iter()
    .try_for_each(|(hashed_address, _)| -> Result<(), StoreError> {
        let state_trie = self.storage.open_state_trie(parent_state_root)?;  // one per item
        let _ = state_trie.get(hashed_address.as_bytes())?;
        Ok(())
    })?;

For a block with, say, 400 touched accounts, this creates 400 trie handles and immediately discards them. Stage B opens exactly 16 (one per worker). If open_state_trie does anything heavier than wrapping a pointer — e.g. looking up the root node in RocksDB — this loop creates gratuitous work that may partially negate the warm-up benefit. Consider par_chunks or a per-thread trie via rayon's local storage to amortize the open across all accounts a given thread processes.

3. Redundant address field in BalSynthesisItem (block_access_list.rs:1594)

pub struct BalSynthesisItem {
    pub address: Address,    // duplicated — always equals the FxHashMap key
    ...
}

The field is set during synthesis but never read from the struct; callers always use the map key addr. Removing it would avoid the duplication and make the struct's role clearer (it's a field-delta bag, not a self-describing entity). Not a bug, but worth cleaning up.


Minor notes

  • The debug_assert! + immediate continue guard for empty slot_changes is the right pattern; the test at synthesize_skips_when_slot_changes_empty correctly annotates the dual debug/release behaviour with #[cfg_attr(debug_assertions, should_panic(...))].

  • The greedy bin-packing weight comment (weight >= 1 invariant) is still accurate for the new synthesis path since removed / removed_storage are gone and weight is purely max(1, added_storage.len()).

  • The [Instant; 7] type alias in BlockExecutionPipelineResult vs [Instant; 8] in log_block_execution_pipeline_info (which appends stored_instant at the call site) is intentional and consistent.


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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 15, 2026

Greptile Summary

This PR decouples BAL-merkleization from EVM execution on the engine_newPayload validation path by synthesizing per-field state deltas from the input BlockAccessList before thread::scope, then running merkle stages B/C/D in parallel with EVM execution. The correctness gate (validate_block_access_list_hash) remains post-execution; a hash mismatch discards the optimistic result via ? propagation.

  • BalSynthesisItem replaces the old Option<AccountInfo> blob with per-field optionals, preventing zero-fabrication for unchanged fields.
  • handle_merkleization_bal is replaced by handle_merkleization_bal_from_updates, eliminating Stage A (channel drain); the EVM skips bal_to_account_updates + send when merkleizer = None on the validation path.
  • Parallel state-trie pre-warm and hashed-key-sorted storage inserts are added to improve cache locality for Stage B/C.

Confidence Score: 4/5

Safe to merge for the validation path; correctness is gated by the existing post-execution BAL hash check and the state-root comparison against the block header, so an incorrect optimistic trie results in block rejection rather than silent state corruption.

The redesign is mechanically sound: per-field optionals avoid zero-fabrication for unchanged account fields, EIP-161 normalization in Stage C correctly handles account deletion without needing an explicit removed flag, and generate_witness_from_account_updates only reads address plus added_storage.keys() so the stripped optimistic_witness is complete for its purpose. Minor structural issues remain: BalSynthesisItem.address is stored but never read after construction, BalStateWorkItem.removed is an always-false dead branch, and the parallel pre-warm opens one state_trie handle per account rather than per thread.

The pre-warm block in handle_merkleization_bal_from_updates (blockchain.rs) and the BalSynthesisItem struct definition (block_access_list.rs) deserve a second look.

Important Files Changed

Filename Overview
crates/blockchain/blockchain.rs Core change: pre-scope BAL synthesis, optional channel, handle_merkleization_bal_from_updates with parallel pre-warm; BalStateWorkItem.removed is now always false (dead branch) and timing array expanded from 6 to 7 instants in the pipeline result.
crates/common/types/block_access_list.rs New BalSynthesisItem struct and synthesize_bal_updates function with 13 unit tests; BalSynthesisItem.address field is redundant (duplicates the map key).
crates/vm/backends/levm/mod.rs merkleizer: Sender to Option<Sender>; BAL path skips bal_to_account_updates + send; sequential path uses expect() to enforce the invariant that non-BAL callers always provide a Sender.
crates/vm/backends/mod.rs One-line signature change: merkleizer: Sender to Option<Sender> to match LEVM.
crates/common/types/mod.rs Re-exports BalSynthesisItem and synthesize_bal_updates from block_access_list.

Sequence Diagram

sequenceDiagram
    participant Main as Main Thread
    participant Exec as EVM Exec Thread
    participant Merkle as Merkleizer Thread
    participant Warm as Warmer Thread

    Note over Main: exec_merkle_start captured
    Main->>Main: synthesize_bal_updates(bal)
    Main->>Main: build optimistic_witness
    Note over Main: thread::scope entered

    par Parallel threads
        Main->>Exec: "spawn merkleizer=None on BAL path"
        Main->>Merkle: "spawn prepared=optimistic_updates"
        Main->>Warm: spawn
    end

    Note over Merkle: merkle_start_instant captured
    Merkle->>Merkle: par_iter pre-warm open_state_trie per account
    Merkle->>Merkle: Stage B parallel storage root computation
    Merkle->>Merkle: Stage C per-shard state trie updates EIP-161
    Merkle->>Merkle: Stage D finalize root state_trie_hash
    Note over Merkle: merkle_end_instant captured

    Exec->>Exec: execute_block_parallel skip bal_to_account_updates
    Exec->>Exec: validate_block_access_list_hash
    Note over Exec: exec_end_instant captured

    Main->>Main: accumulated_updates optimistic_witness OR streaming_witness
    Main->>Main: verify state_trie_hash matches block header
    Note over Main: exec_merkle_end_instant captured
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
crates/common/types/block_access_list.rs:1600-1608
The `address` field duplicates the map key in `FxHashMap<Address, BalSynthesisItem>`. In `handle_merkleization_bal_from_updates` the key `addr` is always used instead of `item.address`, and in `optimistic_witness` `*addr` is used too — so this field is written but never read. Removing it saves 20 bytes per account in the synthesized map.

```suggestion
#[derive(Debug, Clone, Default)]
pub struct BalSynthesisItem {
    pub balance: Option<U256>,
    pub nonce: Option<u64>,
    pub code_hash: Option<H256>,
    pub code: Option<Code>,
    pub added_storage: FxHashMap<H256, U256>,
}
```

### Issue 2 of 3
crates/blockchain/blockchain.rs:319-326
On the BAL synthesis path `removed` is hardcoded to `false` and `handle_merkleization_bal_from_updates` never sets it. The `if item.removed { account_state = AccountState::default(); }` branch in the Stage C worker is therefore unreachable on this path. Dropping the field from the struct would make this invariant visible and avoid confusion for future readers.

```suggestion
struct BalStateWorkItem {
    hashed_address: H256,
    nonce: Option<u64>,
    balance: Option<U256>,
    code_hash: Option<H256>,
    /// Pre-computed storage root from Stage B, or None to keep existing.
    storage_root: Option<H256>,
```

### Issue 3 of 3
crates/blockchain/blockchain.rs:921-932
**Pre-warm opens one `state_trie` handle per account**

The rayon closure calls `open_state_trie(parent_state_root)` for each of the N accounts, creating N independent trie handles before Stage B opens another 16 (one per worker). If `open_state_trie` acquires any internal lock, allocates a per-handle arena, or does RocksDB snapshot work, N parallel opens could serialise or spike allocation far beyond what Stage B already requires. Worth verifying that `open_state_trie` is a lightweight read-view wrapper, or considering a single trie opened per rayon thread rather than per item.

Reviews (1): Last reviewed commit: "perf(l1): BAL optimistic merkleization o..." | Re-trigger Greptile

Comment on lines +1600 to +1608
#[derive(Debug, Clone, Default)]
pub struct BalSynthesisItem {
pub address: Address,
pub balance: Option<U256>,
pub nonce: Option<u64>,
pub code_hash: Option<H256>,
pub code: Option<Code>,
pub added_storage: FxHashMap<H256, U256>,
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The address field duplicates the map key in FxHashMap<Address, BalSynthesisItem>. In handle_merkleization_bal_from_updates the key addr is always used instead of item.address, and in optimistic_witness *addr is used too — so this field is written but never read. Removing it saves 20 bytes per account in the synthesized map.

Suggested change
#[derive(Debug, Clone, Default)]
pub struct BalSynthesisItem {
pub address: Address,
pub balance: Option<U256>,
pub nonce: Option<u64>,
pub code_hash: Option<H256>,
pub code: Option<Code>,
pub added_storage: FxHashMap<H256, U256>,
}
#[derive(Debug, Clone, Default)]
pub struct BalSynthesisItem {
pub balance: Option<U256>,
pub nonce: Option<u64>,
pub code_hash: Option<H256>,
pub code: Option<Code>,
pub added_storage: FxHashMap<H256, U256>,
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/common/types/block_access_list.rs
Line: 1600-1608

Comment:
The `address` field duplicates the map key in `FxHashMap<Address, BalSynthesisItem>`. In `handle_merkleization_bal_from_updates` the key `addr` is always used instead of `item.address`, and in `optimistic_witness` `*addr` is used too — so this field is written but never read. Removing it saves 20 bytes per account in the synthesized map.

```suggestion
#[derive(Debug, Clone, Default)]
pub struct BalSynthesisItem {
    pub balance: Option<U256>,
    pub nonce: Option<u64>,
    pub code_hash: Option<H256>,
    pub code: Option<Code>,
    pub added_storage: FxHashMap<H256, U256>,
}
```

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

Comment on lines 319 to 326
struct BalStateWorkItem {
hashed_address: H256,
info: Option<AccountInfo>,
nonce: Option<u64>,
balance: Option<U256>,
code_hash: Option<H256>,
removed: bool,
/// Pre-computed storage root from Stage B, or None to keep existing.
storage_root: Option<H256>,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 On the BAL synthesis path removed is hardcoded to false and handle_merkleization_bal_from_updates never sets it. The if item.removed { account_state = AccountState::default(); } branch in the Stage C worker is therefore unreachable on this path. Dropping the field from the struct would make this invariant visible and avoid confusion for future readers.

Suggested change
struct BalStateWorkItem {
hashed_address: H256,
info: Option<AccountInfo>,
nonce: Option<u64>,
balance: Option<U256>,
code_hash: Option<H256>,
removed: bool,
/// Pre-computed storage root from Stage B, or None to keep existing.
storage_root: Option<H256>,
struct BalStateWorkItem {
hashed_address: H256,
nonce: Option<u64>,
balance: Option<U256>,
code_hash: Option<H256>,
/// Pre-computed storage root from Stage B, or None to keep existing.
storage_root: Option<H256>,
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/blockchain/blockchain.rs
Line: 319-326

Comment:
On the BAL synthesis path `removed` is hardcoded to `false` and `handle_merkleization_bal_from_updates` never sets it. The `if item.removed { account_state = AccountState::default(); }` branch in the Stage C worker is therefore unreachable on this path. Dropping the field from the struct would make this invariant visible and avoid confusion for future readers.

```suggestion
struct BalStateWorkItem {
    hashed_address: H256,
    nonce: Option<u64>,
    balance: Option<U256>,
    code_hash: Option<H256>,
    /// Pre-computed storage root from Stage B, or None to keep existing.
    storage_root: Option<H256>,
```

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

Comment on lines +921 to 932
// Warm parent state-trie pages for all touched accounts in parallel before
// Stage B / Stage C race for them. This replaces the prefetch that the old
// streaming path got for free via `bal_to_account_updates`.
accounts
.par_iter()
.try_for_each(|(hashed_address, _)| -> Result<(), StoreError> {
let state_trie = self.storage.open_state_trie(parent_state_root)?;
let _ = state_trie.get(hashed_address.as_bytes())?;
Ok(())
})?;

// === Stage B: Parallel per-account storage root computation ===
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Pre-warm opens one state_trie handle per account

The rayon closure calls open_state_trie(parent_state_root) for each of the N accounts, creating N independent trie handles before Stage B opens another 16 (one per worker). If open_state_trie acquires any internal lock, allocates a per-handle arena, or does RocksDB snapshot work, N parallel opens could serialise or spike allocation far beyond what Stage B already requires. Worth verifying that open_state_trie is a lightweight read-view wrapper, or considering a single trie opened per rayon thread rather than per item.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/blockchain/blockchain.rs
Line: 921-932

Comment:
**Pre-warm opens one `state_trie` handle per account**

The rayon closure calls `open_state_trie(parent_state_root)` for each of the N accounts, creating N independent trie handles before Stage B opens another 16 (one per worker). If `open_state_trie` acquires any internal lock, allocates a per-handle arena, or does RocksDB snapshot work, N parallel opens could serialise or spike allocation far beyond what Stage B already requires. Worth verifying that `open_state_trie` is a lightweight read-view wrapper, or considering a single trie opened per rayon thread rather than per item.

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

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

Labels

L1 Ethereum client performance Block execution throughput and performance in general

Projects

Status: In Review
Status: Todo

Development

Successfully merging this pull request may close these issues.

1 participant