Skip to content

feat(l1): improve rlp import#6666

Merged
edg-l merged 4 commits into
mainfrom
feat/import-bench-bal-tooling
May 21, 2026
Merged

feat(l1): improve rlp import#6666
edg-l merged 4 commits into
mainfrom
feat/import-bench-bal-tooling

Conversation

@edg-l
Copy link
Copy Markdown
Contributor

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

Summary

  • Adds --export-bal <FILE> and --with-bal <FILE> flags to import-bench (cmd/ethrex/cli.rs). --export-bal runs sequential execution and writes one RLP-encoded BlockAccessList per Amsterdam+ block into a single file; --with-bal decodes that file and feeds BALs into the parallel execution path.
  • Replaces the magic-number tokio::time::sleep(500ms) between blocks in import_blocks_bench with an explicit Store::wait_for_persistence_idle() call. Uses a new TrieMessage::Ping over the sync_channel(0) rendezvous channel: a successful send proves the trie-update worker finished its Phase 2 disk write and returned to recv().
  • No production/consensus code paths are affected; changes are scoped to bench tooling and one new Store method.

Import speed improvement

The previous inter-block sleep(500ms) is replaced by a ping-based wait that resolves in ~3-10ms (typical Phase 2/3 drain time).

For a 200-block import, total inter-block waiting:

  • Old: 200 x 500ms ~= 100s
  • New: 200 x ~5ms ~= <1s

So ~99 seconds saved in pure waiting per 200 blocks. The per-block timer is also now accurate -- it no longer absorbs the previous block's Phase 2/3 background persistence cost.

Notes

Cherry-picked from two feature branches:

  • bal-benchmark-setup -- --export-bal / --with-bal plumbing
  • perf/bal-parallel-overhead-rebased -- persistence-idle wait

Isolated here so they can ship as a focused tooling PR without dragging in unrelated work from either parent branch.

Test plan

  • `cargo fmt --all`
  • `cargo check -p ethrex`
  • Manual round-trip: run `import-bench --export-bal /tmp/bals.rlp`, then `import-bench --with-bal /tmp/bals.rlp` and verify parallel execution path is exercised
  • Confirm no sleep regression: bench should not stall between blocks without the old sleep

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 18, 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.

@github-actions github-actions Bot added the L1 Ethereum client label May 18, 2026
@edg-l edg-l changed the title feat(l1): import-bench BAL RLP I/O + persistence-idle wait improve rlp import May 18, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 18, 2026

Lines of code report

Total lines added: 104
Total lines removed: 0
Total lines changed: 104

Detailed view
+--------------------------------+-------+------+
| File                           | Lines | Diff |
+--------------------------------+-------+------+
| ethrex/cmd/ethrex/cli.rs       | 1197  | +89  |
+--------------------------------+-------+------+
| ethrex/crates/storage/store.rs | 2697  | +15  |
+--------------------------------+-------+------+

@github-actions github-actions Bot removed the L1 Ethereum client label May 18, 2026
@edg-l edg-l changed the title improve rlp import feat(l1): improve rlp import May 18, 2026
@github-actions github-actions Bot added the L1 Ethereum client label May 18, 2026
@edg-l edg-l force-pushed the feat/import-bench-bal-tooling branch from d09b663 to 5c087ae Compare May 18, 2026 08:23
@edg-l edg-l marked this pull request as ready for review May 18, 2026 08:32
@edg-l edg-l requested a review from a team as a code owner May 18, 2026 08:32
@edg-l edg-l moved this to In Review in ethrex_l1 May 18, 2026
@github-actions
Copy link
Copy Markdown

🤖 Kimi Code Review

Overall Assessment: The PR introduces BAL (Block Access List) export/import functionality for benchmark tooling and replaces a fixed sleep with proper synchronization for persistence idle detection. The logic is generally sound, but there are blocking I/O issues in async contexts and missing validation that could lead to benchmarking with incorrect data.


Critical Issues

1. Blocking I/O in Async Context (cmd/ethrex/cli.rs)
Lines 936 and 1077 use std::fs::read and std::fs::write inside an async function, which blocks the Tokio runtime threads.

  • Line 936: let data = std::fs::read(bal_path).expect("Failed to read BAL file");
  • Line 1077: std::fs::write(bal_path, &buf).expect("Failed to write BAL file");

Fix: Use tokio::fs::read and tokio::fs::write, or wrap in tokio::task::spawn_blocking.


Correctness & Validation

2. Missing BAL Hash Validation (cmd/ethrex/cli.rs:1004)
When loading BALs via --with-bal, the code retrieves the BAL by index but never validates that the loaded BAL's hash matches block.header.block_access_list_hash. Using an incorrect BAL could lead to invalid state transitions or meaningless benchmark results.

let bal = if block.header.block_access_list_hash.is_some() {
    let b = preloaded_bals.as_ref().and_then(|bals| bals.get(bal_index));
    // TODO: Verify b.hash() matches block.header.block_access_list_hash
    bal_index += 1;
    b
} else {
    None
};

Fix: Verify the hash matches before using the preloaded BAL, and return an error if validation fails.

3. Silent Index Mismatch (cmd/ethrex/cli.rs:1004)
If the BAL file contains fewer entries than blocks with access list hashes, bals.get(bal_index) returns None silently, causing the benchmark to potentially fall back to sequential execution without warning.

Fix: Return an explicit error if bal_index >= bals.len() when a BAL is expected.

4. Panic on Decode Error (cmd/ethrex/cli.rs:943-944)
The use of .expect("Failed to decode BAL") causes an immediate panic on corrupt data. Since the function returns Result<(), ChainError>, this should be propagated properly.

Fix: Use ? operator with proper error mapping instead of expect.


Code Quality

5. Incorrect Error Message (crates/storage/store.rs:1434)
The error message says "failed to read new trie layer notification" but this is a send operation. This appears to be pre-existing but should be fixed as a drive-by.

Current:

StoreError::Custom(format!("failed to read new trie layer notification: {e}"))

Fix: Change "read" to "send".

6. Memory Usage for Large Chains (cmd/ethrex/cli.rs:983)
exported_bals accumulates all BALs in memory until the end of the import. For long chains, this could consume significant RAM. While acceptable for a benchmark tool, consider documenting this behavior or streaming writes for very large datasets.


Positive Aspects

  1. Rendezvous Ping Mechanism: The wait_for_persistence_idle implementation using sync_channel(0) for backpressure detection is clever and correct.
  2. CLI Safety: Proper use of `conflicts_with

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 18, 2026

Greptile Summary

This PR replaces the sleep(500ms) inter-block stall in import-bench with a rendezvous-channel ping that resolves as soon as the trie-update worker drains, and adds --export-bal / --with-bal flags to round-trip BlockAccessList data between a sequential and a parallel execution run. No production or consensus paths are touched.

  • store.rs adds TrieMessage::{Update, Ping} and Store::wait_for_persistence_idle, using the existing sync_channel(0) rendezvous property — a Ping send cannot complete until the worker loops back to recv(), which proves Phase 2+3 have finished. The logic is sound and well-documented.
  • cli.rs wires the two new CLI flags through to import_blocks_bench, pre-loads the BAL file into memory up front, and selects between add_block_pipeline_bal (export path) and add_block_pipeline (normal/parallel path) per block. bal_index is reset to zero for each chain in the multi-chain loop, which misaligns BALs for directory-based imports; a fix is suggested inline.

Confidence Score: 3/5

Safe to merge for single-file bench runs; directory-based imports with --with-bal will silently feed wrong BALs to every chain after the first

The store.rs changes are clean and the ping/rendezvous mechanism is correctly implemented. The only issue lives in cli.rs: bal_index resets to zero at the start of each chain in the multi-chain loop, so any directory-based bench run using --with-bal will correlate BALs from chain 1 to blocks in chain 2+, producing silently incorrect parallel-execution inputs. The typical single-file workflow is unaffected, but the fix is a one-line variable hoist.

cmd/ethrex/cli.rs — specifically the bal_index declaration and the out-of-bounds fallback in the block loop

Important Files Changed

Filename Overview
cmd/ethrex/cli.rs Adds --export-bal/--with-bal flags and ping-based persistence wait to import-bench; bal_index resets per-chain causing BAL misalignment for directory imports
crates/storage/store.rs Introduces TrieMessage enum and wait_for_persistence_idle; rendezvous-channel ping semantics are correct and the change is well-isolated from production paths

Sequence Diagram

sequenceDiagram
    participant Bench as import_blocks_bench
    participant Pipeline as add_block_pipeline[_bal]
    participant Store as Store (sync send)
    participant Worker as trie-update worker thread

    Bench->>Pipeline: execute block N
    Pipeline->>Store: send TrieMessage::Update (rendezvous blocks)
    Store-->>Worker: worker receives Update, begins Phase 1+2+3
    Store-->>Pipeline: send returns (Phase 1 accepted)
    Pipeline-->>Bench: returns Ok

    Bench->>Store: wait_for_persistence_idle() spawn_blocking(tx.send(Ping))
    Note over Store,Worker: Ping send blocks until worker loops back to recv()
    Worker-->>Worker: Phase 2 disk write completes
    Worker-->>Worker: Phase 3 in-memory removal completes
    Worker-->>Store: worker calls recv() picks up Ping
    Store-->>Bench: send returns persistence idle confirmed

    Bench->>Pipeline: execute block N+1
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
cmd/ethrex/cli.rs:979-989
`bal_index` is declared inside the `for blocks in chains` loop, so it resets to `0` at the start of every chain. When the input is a directory (multiple chain files), chains 2, 3, … will each re-read BALs starting from the beginning of `preloaded_bals` instead of continuing from where the previous chain left off. This means BALs will be assigned to the wrong blocks for every chain after the first, silently producing incorrect parallel-execution inputs.

```suggestion
    let mut exported_bals = Vec::new();
    let mut total_blocks_imported = 0;
    let mut bal_index = 0usize;
    for blocks in chains {
        let size = blocks.len();
        let mut numbers_and_hashes = blocks
            .iter()
            .map(|b| (b.header.number, b.hash()))
            .collect::<Vec<_>>();
        // Execute block by block
        let mut last_progress_log = Instant::now();
```

### Issue 2 of 2
cmd/ethrex/cli.rs:1020-1026
When `--with-bal` is active but `bal_index` has grown past the end of `preloaded_bals` (e.g. the BAL file was exported from a shorter run, or there is a miscount between runs), `bals.get(bal_index)` silently returns `None` and the block is executed on the sequential path. No warning is logged, so a user relying on this bench to test parallel execution would get no indication that the BAL-parallel path was never exercised.

```suggestion
            let bal = if block.header.block_access_list_hash.is_some() {
                let b = preloaded_bals.as_ref().and_then(|bals| {
                    let entry = bals.get(bal_index);
                    if entry.is_none() {
                        warn!(block = number, bal_index, "BAL file exhausted; falling back to sequential execution for this block");
                    }
                    entry
                });
                bal_index += 1;
                b
            } else {
                None
            };
```

Reviews (1): Last reviewed commit: "perf(l1): replace import-bench inter-blo..." | Re-trigger Greptile

Comment thread cmd/ethrex/cli.rs Outdated
Comment on lines +979 to +989
@@ -938,6 +986,7 @@ pub async fn import_blocks_bench(
.collect::<Vec<_>>();
// Execute block by block
let mut last_progress_log = Instant::now();
let mut bal_index = 0usize;
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 bal_index is declared inside the for blocks in chains loop, so it resets to 0 at the start of every chain. When the input is a directory (multiple chain files), chains 2, 3, … will each re-read BALs starting from the beginning of preloaded_bals instead of continuing from where the previous chain left off. This means BALs will be assigned to the wrong blocks for every chain after the first, silently producing incorrect parallel-execution inputs.

Suggested change
let mut exported_bals = Vec::new();
let mut total_blocks_imported = 0;
let mut bal_index = 0usize;
for blocks in chains {
let size = blocks.len();
let mut numbers_and_hashes = blocks
.iter()
.map(|b| (b.header.number, b.hash()))
.collect::<Vec<_>>();
// Execute block by block
let mut last_progress_log = Instant::now();
Prompt To Fix With AI
This is a comment left during a code review.
Path: cmd/ethrex/cli.rs
Line: 979-989

Comment:
`bal_index` is declared inside the `for blocks in chains` loop, so it resets to `0` at the start of every chain. When the input is a directory (multiple chain files), chains 2, 3, … will each re-read BALs starting from the beginning of `preloaded_bals` instead of continuing from where the previous chain left off. This means BALs will be assigned to the wrong blocks for every chain after the first, silently producing incorrect parallel-execution inputs.

```suggestion
    let mut exported_bals = Vec::new();
    let mut total_blocks_imported = 0;
    let mut bal_index = 0usize;
    for blocks in chains {
        let size = blocks.len();
        let mut numbers_and_hashes = blocks
            .iter()
            .map(|b| (b.header.number, b.hash()))
            .collect::<Vec<_>>();
        // Execute block by block
        let mut last_progress_log = Instant::now();
```

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

Comment thread cmd/ethrex/cli.rs
Comment on lines +1020 to +1026
let bal = if block.header.block_access_list_hash.is_some() {
let b = preloaded_bals.as_ref().and_then(|bals| bals.get(bal_index));
bal_index += 1;
b
} else {
None
};
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 When --with-bal is active but bal_index has grown past the end of preloaded_bals (e.g. the BAL file was exported from a shorter run, or there is a miscount between runs), bals.get(bal_index) silently returns None and the block is executed on the sequential path. No warning is logged, so a user relying on this bench to test parallel execution would get no indication that the BAL-parallel path was never exercised.

Suggested change
let bal = if block.header.block_access_list_hash.is_some() {
let b = preloaded_bals.as_ref().and_then(|bals| bals.get(bal_index));
bal_index += 1;
b
} else {
None
};
let bal = if block.header.block_access_list_hash.is_some() {
let b = preloaded_bals.as_ref().and_then(|bals| {
let entry = bals.get(bal_index);
if entry.is_none() {
warn!(block = number, bal_index, "BAL file exhausted; falling back to sequential execution for this block");
}
entry
});
bal_index += 1;
b
} else {
None
};
Prompt To Fix With AI
This is a comment left during a code review.
Path: cmd/ethrex/cli.rs
Line: 1020-1026

Comment:
When `--with-bal` is active but `bal_index` has grown past the end of `preloaded_bals` (e.g. the BAL file was exported from a shorter run, or there is a miscount between runs), `bals.get(bal_index)` silently returns `None` and the block is executed on the sequential path. No warning is logged, so a user relying on this bench to test parallel execution would get no indication that the BAL-parallel path was never exercised.

```suggestion
            let bal = if block.header.block_access_list_hash.is_some() {
                let b = preloaded_bals.as_ref().and_then(|bals| {
                    let entry = bals.get(bal_index);
                    if entry.is_none() {
                        warn!(block = number, bal_index, "BAL file exhausted; falling back to sequential execution for this block");
                    }
                    entry
                });
                bal_index += 1;
                b
            } else {
                None
            };
```

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

@github-actions
Copy link
Copy Markdown

🤖 Claude Code Review

I have enough to write a thorough review now.


Review: feat(l1): improve rlp import

Overall: The persistence-idle wait mechanism is the star of this PR — clean design, sound reasoning, properly implemented. The BAL round-trip plumbing is mostly correct for the primary single-chain use case but has a meaningful scope bug for directory imports, plus a few silent failure modes worth tightening.


crates/storage/store.rs — persistence-idle wait

The core mechanism is correct. The reasoning about sync_channel(0) as a rendezvous is valid:

  • send(Update) completes → worker has dequeued the update and is running apply_trie_updates
  • send(Ping) completes → worker has finished the prior apply_trie_updates and is back at recv()

The use of spawn_blocking for the blocking send is exactly right; blocking a sync send inside an async executor would stall the runtime. TrieMessage wrapping TrieUpdate is a clean, minimal enum expansion.

One thing to confirm: send(Update(trie_update)) in Store::update_state_trie (line ~1434) is called without spawn_blocking. Since the channel has capacity 0, that call also blocks until the worker dequeues the message. If update_state_trie is on the async executor this would stall it — but this appears to be a pre-existing pattern, not introduced here.

Minor: error message copy-paste — the error text "failed to read new trie layer notification" in the TrieMessage::Update send (line ~1434) is unchanged from the original TrieUpdate send. It's a benign staleness but could be updated to "failed to send trie update message" to match the broader TrieMessage framing.


cmd/ethrex/cli.rs — BAL round-trip

Bug: bal_index resets per chain

// line 95 (outer loop)
for blocks in chains {
    ...
    let mut bal_index = 0usize;  // line 101 — INSIDE the outer loop
    for (index, block) in blocks.into_iter().enumerate() {
        let bal = if block.header.block_access_list_hash.is_some() {
            let b = preloaded_bals.as_ref().and_then(|bals| bals.get(bal_index));
            bal_index += 1;
            b
        } ...
    }
}

preloaded_bals is declared outside the outer for blocks in chains loop (line 67), but bal_index is declared inside it (line 101). For single .rlp files there is only one chain, so this is harmless. But for a directory import with multiple chains, bal_index resets to 0 at the start of each chain — the second chain reads the first chain's BALs again from the beginning. The fix is to declare bal_index outside the outer for blocks in chains loop alongside exported_bals and total_blocks_imported.

Silent mismatch: fewer BALs than Amsterdam+ blocks

let b = preloaded_bals.as_ref().and_then(|bals| bals.get(bal_index));
bal_index += 1;

If the BAL file contains fewer entries than the number of Amsterdam+ blocks in the chain, bals.get(bal_index) silently returns None and the block is executed without a BAL. There is no warning, no error. Consider adding at least a warn! here, or a post-loop assertion that bal_index == preloaded_bals.len().

Unused bal computation in the export path

// Always computed:
let bal = if block.header.block_access_list_hash.is_some() {
    let b = preloaded_bals.as_ref().and_then(|bals| bals.get(bal_index));
    bal_index += 1;
    b
} else { None };

// But then:
if export_bal_path.is_some() {
    // `bal` is never used in this branch
    let produced_bal = blockchain.add_block_pipeline_bal(block, None)...;
    ...
} else {
    blockchain.add_block_pipeline(block, bal)...;  // used here
}

When export_bal_path.is_some(), with_bal_path is always None (enforced by conflicts_with), so preloaded_bals is None, making the bal computation a harmless no-op. Rust won't warn because bal is syntactically referenced in the else branch. However, the code reads confusingly — it looks like bal_index might be advancing in the export path. Moving the bal lookup inside the else branch would make the control flow clearer.

expect() panics on I/O / decode failure

let data = std::fs::read(bal_path).expect("Failed to read BAL file");         // line ~78
BlockAccessList::decode_unfinished(remaining).expect("Failed to decode BAL"); // line ~85
std::fs::write(bal_path, &buf).expect("Failed to write BAL file");            // line ~172

import_blocks_bench returns Result<(), ChainError>, so these could return errors instead of panicking. Using expect in a bench tool is tolerable, but given the function already has full error-propagation plumbing, a quick map_err(...)? keeps the exit path consistent and gives the caller (the CLI handler) a chance to print a structured error.

Minor: informational log before the work is done

if let Some(bal_path) = export_bal_path {
    info!(path = %bal_path, "Will export BALs to file");
}

This fires at startup regardless of whether any Amsterdam+ blocks are actually encountered. Could be moved to the point where the first BAL is actually captured, or just left as-is since it's a low-stakes bench log.

std::fs I/O on the tokio executor

std::fs::read (line ~78) and std::fs::write (line ~172) are synchronous and called directly in an async function. For the benchmark's startup/teardown this isn't a correctness problem, but it does stall the executor thread momentarily. Using tokio::fs::read / tokio::fs::write would be more consistent with the async context.


Summary

Severity Item
Bug bal_index resets to 0 per chain — multi-chain directory imports pair BALs incorrectly
Moderate Silent BAL under-supply — no warning when BAL file has fewer entries than Amsterdam+ blocks
Low Unused bal/bal_index computation in export branch — confusing, should be moved into else
Low expect() panics rather than returning errors through the existing Result chain
Low Blocking std::fs I/O directly on the async executor at startup/shutdown
Trivial Stale error message text on TrieMessage::Update send

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

@github-actions
Copy link
Copy Markdown

🤖 Codex Code Review

Findings

  1. cmd/ethrex/cli.rs: bal_index is reset inside for blocks in chains, but the exporter writes a single flat BAL stream across all chains (exported_bals is outside that loop, then written once at lines 1073-1079). For directory inputs, the first Amsterdam block in the second file will reuse the first BAL from the first file. Because cmd/ethrex/cli.rs feeds that into add_block_pipeline(block, bal), this is not just a perf mismatch; it can drive the BAL-validated execution path with the wrong block’s data and produce false failures.

  2. cmd/ethrex/cli.rs: skipped already-imported blocks do not advance the BAL cursor, and the importer never checks that consumed BALs == loaded BALs. That means resuming on a non-empty datadir, or exporting from one, desynchronizes every later BAL. The help text at cmd/ethrex/cli.rs also says “one per block”, but the exporter only writes Some(bal) values (lines 1037-1038), so the actual format is “one BAL per Amsterdam block actually executed in this run”. This needs explicit indexing or an Option<BAL> placeholder format.

  3. cmd/ethrex/cli.rs: the new BAL file path uses expect() for read/decode/write failures. A malformed or truncated --with-bal file, or an unwritable --export-bal destination, will panic the process instead of returning a ChainError. Since import_blocks_bench already returns Result, these should be surfaced as normal errors.

Open Questions

  • I assumed import bench --with-bal is intended to work for directory inputs and without --removedb, because the CLI currently allows both. If that is intentionally unsupported, the command should reject or clearly document those cases.

Summary
Aside from the BAL file indexing/error-handling issues above, the Store::wait_for_persistence_idle() rendezvous in crates/storage/store.rs looks consistent with the current zero-capacity worker-channel design.

I did not run cargo check; the sandbox here blocks rustup from creating temp files in its default location.


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

@edg-l edg-l force-pushed the feat/import-bench-bal-tooling branch from 5c087ae to 87f5a24 Compare May 18, 2026 11:17
@edg-l edg-l force-pushed the feat/import-bench-bal-tooling branch from 87f5a24 to 8192dd1 Compare May 19, 2026 07:42
Add tooling to benchmark the BAL parallel execution path
(execute_block_pipeline) against sequential execution in a
reproducible way.

- cmd/ethrex/cli.rs: two new import-bench flags:
  - --export-bal <FILE>: sequential pass via add_block_pipeline_bal,
    writes a single concatenated RLP file with one BAL per block.
  - --with-bal <FILE>: loads BALs from disk (in-memory) and routes
    each block through add_block_pipeline with the pre-loaded BAL,
    activating the parallel path. BALs are matched to Amsterdam+
    blocks by header block_access_list_hash presence.
  The two flags conflict, making the intent explicit. The
  bootstrap/benchmark split runs the parallel path against
  deterministic, pre-recorded BALs rather than re-deriving them, so
  timings are comparable.

- crates/storage/store.rs: replace import-bench inter-block sleep
  with explicit persistence-idle wait. The trie-update worker
  channel is sync_channel(0) (rendezvous), so a successful send
  proves the worker drained its previous iteration and returned to
  recv(). Add TrieMessage::Ping (no-op) plus
  Store::wait_for_persistence_idle() that sends one from
  spawn_blocking; the import-bench loop calls it instead of sleeping
  100ms. Removes the magic-number sleep and tightens the per-block
  timer against the actual idle signal rather than a worst-case
  Phase 2/3 estimate. Bench-tooling change only; no effect on
  production paths.
@edg-l edg-l force-pushed the feat/import-bench-bal-tooling branch from 8192dd1 to 02a5663 Compare May 19, 2026 07:55
bal_index was declared inside the `for blocks in chains` loop, so it
reset to 0 at the start of every chain. When the input path is a
directory the importer processes multiple chain files sequentially but
`preloaded_bals` is a single flat list across all Amsterdam+ blocks, so
chain 2+ silently consumed chain 1's BALs instead of continuing the
cursor. Move the declaration above the outer loop so the cursor spans
all chains. Single-chain imports are unaffected.
Copy link
Copy Markdown
Contributor

@ElFantasma ElFantasma left a comment

Choose a reason for hiding this comment

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

Both modes batch-load everything into RAM. --export-bal accumulates exported_bals: Vec<BlockAccessList> across all blocks before writing; --with-bal does preloaded_bals: Vec<BlockAccessList> upfront. For a 200-block bench this is fine. For 100k+ blocks on a long chain, total memory could reach multiple GB. Worth a comment in the help text noting the in-memory bound, or a follow-up to stream both sides via length-prefixed framing.

Comment thread cmd/ethrex/cli.rs
Comment thread crates/storage/store.rs
Comment thread cmd/ethrex/cli.rs Outdated
edg-l added 2 commits May 21, 2026 14:42
- assert preloaded BAL count matches Amsterdam+ block count up front; mismatched files would otherwise fall through to sequential silently
- include path in panic messages for BAL read/write/decode and chain path stat
- document single-producer expectation on Store::wait_for_persistence_idle
- note in-memory bound in --export-bal/--with-bal help text
@edg-l edg-l enabled auto-merge May 21, 2026 12:50
@edg-l edg-l added this pull request to the merge queue May 21, 2026
Merged via the queue into main with commit 4acf665 May 21, 2026
54 checks passed
@edg-l edg-l deleted the feat/import-bench-bal-tooling branch May 21, 2026 14:23
@github-project-automation github-project-automation Bot moved this from In Review to Done in ethrex_l1 May 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L1 Ethereum client

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants