Skip to content

perf(l1): speed up snap sync validation with parallelism and deduplication#6191

Merged
ilitteri merged 5 commits intomainfrom
snap-sync-validation-optimizations
Feb 25, 2026
Merged

perf(l1): speed up snap sync validation with parallelism and deduplication#6191
ilitteri merged 5 commits intomainfrom
snap-sync-validation-optimizations

Conversation

@ilitteri
Copy link
Collaborator

Motivation

The three post-sync validation functions (validate_state_root, validate_storage_root, validate_bytecodes) run via debug_assert! in release-with-debug-assertions builds to verify trie integrity after snap sync. We use these to validate that optimization changes don't break sync correctness, but they take too long — slowing down the testing feedback loop.

Neither geth nor nethermind perform full trie traversal after snap sync (they trust healing completion), so these are ethrex-specific correctness checks we want to keep but make faster.

Description

validate_state_root — parallel subtree validation

  • Add Trie::validate_parallel() that fetches the root branch node and validates each of its 16 subtrees independently via rayon
  • Extracted validate_subtree() helper for iterative DFS with hash checking
  • Expected ~4-8x speedup from 16-way parallelism

validate_storage_root — skip empty + eliminate par_bridge overhead

  • Filter accounts with storage_root == EMPTY_TRIE_HASH before processing (~90% of accounts are EOAs with empty storage)
  • Replace par_bridge() with chunked collect() + par_iter() (4096 accounts per chunk) to eliminate the per-item mutex contention inherent to par_bridge's sequential-to-parallel adapter

validate_bytecodes — dedup + existence check + parallelize

  • Collect unique code hashes into a HashSet first — on mainnet ~70M contracts share only ~2.5M unique bytecodes (28:1 ratio)
  • Add Store::code_exists() that checks key existence without reading the full bytecode blob or RLP decoding (avoids BlobDB value reads)
  • Parallelize with par_iter() over the deduplicated set

How to Test

  1. Build with debug assertions: cargo build --release --bin ethrex --profile release-with-debug-assertions
  2. Run a snap sync on any network (hoodi, sepolia, mainnet)
  3. After sync completes, observe validation timings in logs:
    • Starting validate_state_root / Succesfully validated tree
    • Starting validate_storage_root / Finished validate_storage_root
    • Starting validate_bytecodes / Collected N unique code hashes for validation
  4. Compare timings against main branch

Copilot AI review requested due to automatic review settings February 12, 2026 02:06
@ilitteri ilitteri requested a review from a team as a code owner February 12, 2026 02:06
@github-actions github-actions bot added L1 Ethereum client performance Block execution throughput and performance in general labels Feb 12, 2026
@github-actions
Copy link

🤖 Kimi Code Review

Review Summary

This PR introduces parallel validation for Ethereum state tries and storage roots, along with optimizations for bytecode validation. Here are the key issues found:

1. Critical Bug in validate_parallel (trie.rs:575-576)

The function returns Ok(()) when the root is not valid, which is incorrect. This silently skips validation for invalid tries.

if !self.root.is_valid() {
    return Ok(()); // BUG: Should return error, not success
}

Fix: Return an appropriate error:

if !self.root.is_valid() {
    return Err(TrieError::Verify("Invalid root node".to_string()));
}

2. Incorrect Node Count Logic in validate_subtree (trie.rs:635-641)

The expected_count check is flawed. The current implementation:

  • Starts with expected_count = 1
  • Decrements for each node processed
  • Increments for each child found
  • Expects final count to be 0

This logic doesn't account for the fact that we start with 1 node but may have multiple children. The check will often fail incorrectly.

Fix: Remove the count check entirely or implement a proper node count validation.

3. Inefficient Storage Root Validation (snap_sync.rs:713-748)

The chunked parallel validation has several issues:

  • Line 713: Manual chunking with const CHUNK_SIZE is unnecessary - Rayon can handle this automatically
  • Lines 720-748: The loop structure is overly complex and may miss some accounts if iter_accounts returns an error

Fix: Simplify to:

store.iter_accounts(state_root)
    .expect("couldn't iterate accounts")
    .filter(|(_, account_state)| account_state.storage_root != *EMPTY_TRIE_HASH)
    .collect::<Vec<_>>()
    .into_par_iter()
    .try_for_each(|(hashed_address, account_state)| {
        // ... validation logic
    })

4. Performance Concern: Unnecessary Collect (snap_sync.rs:761-770)

The bytecode validation collects all unique hashes into a HashSet before parallel processing. This requires:

  • Storing all hashes in memory
  • Two passes over the data (collect + validate)

Fix: Use Rayon's parallel iterator directly:

store.iter_accounts(state_root)
    .expect("we couldn't iterate over accounts")
    .filter(|(_, account_state)| account_state.code_hash != *EMPTY_KECCACK_HASH)
    .map(|(_, account_state)| account_state.code_hash)
    .collect::<HashSet<_>>()
    .into_par_iter()
    .filter(|code_hash| !store.code_exists(*code_hash).unwrap_or(false))
    .collect::<Vec<_>>()

5. Minor Issues

  • trie.rs:590: The comment "Non-branch root (rare)" is misleading - extension nodes at root are not that rare
  • snap_sync.rs:742: The expect("couldn't open storage trie") should probably return false instead of panicking
  • store.rs:720-727: The cache check uses get(&code_hash)? which might return Some(&Vec<u8>) - this could be inefficient for large bytecodes

Security Considerations

  • The parallel validation doesn't introduce new security issues, but the bug in validate_parallel could allow invalid state to be accepted
  • The code_exists function is a good optimization that avoids reading large bytecodes into memory

Overall Assessment

The PR introduces valuable performance improvements through parallelization, but the critical bug in validate_parallel must be fixed before merging. The storage root validation logic should also be simplified to use Rayon's built-in capabilities more effectively.


Automated review by Kimi (Moonshot AI) · custom prompt

@github-actions
Copy link

🤖 Codex Code Review

Findings:

  1. Potential integrity gap: validate_bytecodes now only checks key existence via code_exists, which does not validate RLP decoding or data integrity. This can let corrupted code blobs pass validation (previously get_account_code would fail on decode). Consider keeping a decode-based validation path (maybe optional/slow) or extending code_exists to verify the value’s integrity.
    crates/networking/p2p/sync/snap_sync.rs:757-789
    crates/storage/store.rs:716-736

  2. Error handling loss: unwrap_or(false) treats any StoreError as “missing code” and exits without surfacing the actual error (e.g., DB read failure, lock poisoning). Consider logging the error and failing with context.
    crates/networking/p2p/sync/snap_sync.rs:774-780

  3. Concurrency caution: validate_storage_root now shares a single Store across rayon threads. This relies on Store: Sync and its internals being safe for concurrent reads. If Store has internal locks or non-thread-safe caches, this can create contention or subtle issues. If the intent is read-parallelism, consider using per-thread clones or explicit read handles.
    crates/networking/p2p/sync/snap_sync.rs:712-744

Positives: the unique code-hash dedup and chunked parallelism are good for performance; the new parallel trie validation is a solid speedup with minimal structural change.

Tests not run (not requested). If you want, I can run targeted tests or add a validation-mode flag to preserve strong integrity checks while keeping the fast path.


Automated review by OpenAI Codex · custom prompt

@github-actions
Copy link

github-actions bot commented Feb 12, 2026

Lines of code report

Total lines added: 105
Total lines removed: 0
Total lines changed: 105

Detailed view
+------------------------------------------------+-------+------+
| File                                           | Lines | Diff |
+------------------------------------------------+-------+------+
| ethrex/crates/common/trie/trie.rs              | 530   | +66  |
+------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/sync/snap_sync.rs | 974   | +23  |
+------------------------------------------------+-------+------+
| ethrex/crates/storage/store.rs                 | 2502  | +16  |
+------------------------------------------------+-------+------+

@greptile-apps
Copy link

greptile-apps bot commented Feb 12, 2026

Greptile Overview

Greptile Summary

This PR introduces significant performance optimizations to snap sync validation functions by leveraging parallelism and smart filtering. The changes are well-architected and follow sound optimization principles.

Key improvements:

  • validate_state_root: Implements parallel subtree validation by splitting at the root branch node (16-way parallelism). The validate_subtree() helper uses iterative DFS with proper hash validation via get_node_checked().
  • validate_storage_root: Filters out ~90% of accounts (EOAs with empty storage) before processing and replaces par_bridge() with chunked iteration (4096 per chunk) to eliminate mutex contention overhead.
  • validate_bytecodes: Deduplicates code hashes (28:1 ratio on mainnet: ~70M contracts → ~2.5M unique bytecodes) and adds efficient Store::code_exists() that skips blob reads and RLP decoding.

Technical soundness:

  • Thread safety is ensured as TrieDB trait already requires Send + Sync
  • Hash validation is preserved throughout via get_node_checked()
  • Chunked processing prevents unbounded memory growth
  • Cache-first lookups in code_exists() optimize hot paths

Minor issue:

  • validate_bytecodes uses unwrap_or(false) which could silently mask database errors during validation - should propagate errors instead

Confidence Score: 4/5

  • This PR is safe to merge with one minor fix recommended for error handling
  • The optimizations are well-implemented with proper thread safety and hash validation preserved. Score is 4/5 (not 5) due to the error-swallowing pattern in validate_bytecodes line 780 which could mask database errors during validation - this should use .expect() instead of .unwrap_or(false) to ensure errors are surfaced
  • Pay close attention to crates/networking/p2p/sync/snap_sync.rs line 780 - the error handling should be fixed before merge

Important Files Changed

Filename Overview
crates/common/trie/trie.rs Added validate_parallel() method and validate_subtree() helper for parallel trie validation by splitting at root branch node - implementation is sound with proper hash checking via get_node_checked()
crates/networking/p2p/sync/snap_sync.rs Optimized validation functions with parallel processing, deduplication, and empty storage filtering - solid optimizations with one minor potential issue in error handling
crates/storage/store.rs Added code_exists() method for efficient existence checks - skips blob reads and RLP decoding, properly checks cache first

Sequence Diagram

sequenceDiagram
    participant Validator as Validation Function
    participant Rayon as Rayon Thread Pool
    participant Trie as Trie (validate_parallel)
    participant DB as TrieDB
    participant Store as Store (code_exists)

    Note over Validator: validate_state_root
    Validator->>Trie: validate_parallel()
    Trie->>DB: get_node_checked(root)
    DB-->>Trie: root branch node
    Trie->>Rayon: par_iter().try_for_each(16 subtrees)
    loop For each subtree (parallel)
        Rayon->>Trie: validate_subtree(path, ref)
        Trie->>DB: get_node_checked(nodes)
        DB-->>Trie: nodes with hash validation
    end
    Rayon-->>Trie: all subtrees valid
    Trie-->>Validator: Ok(())

    Note over Validator: validate_storage_root
    Validator->>Store: iter_accounts(state_root)
    Store-->>Validator: accounts iterator
    Validator->>Validator: filter(storage_root != EMPTY)
    loop Chunked (4096 per chunk)
        Validator->>Rayon: par_iter().try_for_each(chunk)
        Rayon->>Store: open_locked_storage_trie()
        Store->>Trie: validate()
        Trie->>DB: sequential validation
        DB-->>Trie: validated
        Trie-->>Store: Ok(())
        Store-->>Rayon: Ok(())
    end
    Rayon-->>Validator: all storage tries valid

    Note over Validator: validate_bytecodes
    Validator->>Store: iter_accounts(state_root)
    Store-->>Validator: accounts iterator
    Validator->>Validator: collect unique code hashes (HashSet)
    Validator->>Rayon: par_iter().filter(missing)
    loop For each unique hash (parallel)
        Rayon->>Store: code_exists(hash)
        Store->>Store: check cache
        Store->>DB: get(ACCOUNT_CODES, hash)
        DB-->>Store: exists/not exists
        Store-->>Rayon: bool
    end
    Rayon-->>Validator: missing hashes list
Loading

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

5 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

use rayon::prelude::*;
let missing: Vec<_> = unique_hashes
.par_iter()
.filter(|code_hash| !store.code_exists(**code_hash).unwrap_or(false))
Copy link

Choose a reason for hiding this comment

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

using unwrap_or(false) silently treats database errors as "code doesn't exist", which could mask real issues during validation

Suggested change
.filter(|code_hash| !store.code_exists(**code_hash).unwrap_or(false))
.filter(|code_hash| !store.code_exists(**code_hash).expect("failed to check code existence"))
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/networking/p2p/sync/snap_sync.rs
Line: 780:780

Comment:
using `unwrap_or(false)` silently treats database errors as "code doesn't exist", which could mask real issues during validation

```suggestion
        .filter(|code_hash| !store.code_exists(**code_hash).expect("failed to check code existence"))
```

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

@github-actions
Copy link

🤖 Claude Code Review

Now let me compile my review. I have enough context to provide a comprehensive review.

PR #6191 Review: Speed up snap sync validation with parallelism and deduplication

Overall Assessment

This is a well-motivated optimization PR that improves debug validation performance without affecting production builds. The approach is sound, but there are several correctness and safety issues that need addressing.


Critical Issues

1. Thread Safety Violation in validate_parallel (trie.rs:575)

Issue: The code uses &*self.db to share a raw reference to the trait object across threads via rayon's par_iter:

let db = &*self.db;  // Line 575
// ...
children.par_iter().try_for_each(|(start_path, start_ref)| {
    validate_subtree(db, start_path.clone(), start_ref.clone())  // db reference sent to other threads
})

Problem: While TrieDB has Send + Sync bounds, using a raw reference &dyn TrieDB in parallel iteration is unsafe. The reference itself isn't Send, and the compiler may not catch this since the closure captures the reference indirectly.

Fix: Use Arc<dyn TrieDB> or ensure proper lifetime bounds. Consider:

let db = Arc::clone(&self.db);
children.par_iter().try_for_each(|(start_path, start_ref)| {
    validate_subtree(&*db, start_path.clone(), start_ref.clone())
})

Verify that self.db is already an Arc internally, or adjust accordingly.


2. Iterator Invalidation Risk (snap_sync.rs:714-717)

Issue: The code filters an iterator and then manually chunks it:

let mut iter = store
    .iter_accounts(state_root)
    .expect("couldn't iterate accounts")
    .filter(|(_, account_state)| account_state.storage_root != *EMPTY_TRIE_HASH);

loop {
    let chunk: Vec<_> = iter.by_ref().take(CHUNK_SIZE).collect();
    // ...
}

Problem: If iter_accounts returns an iterator that holds internal state (e.g., a trie iterator with database connections), the iterator might be invalidated or have thread-safety issues when store is cloned inside the parallel loop.

Risk: Medium - depends on iterator implementation.

Recommendation: Review iter_accounts implementation to ensure it's safe to use across the blocking task and that database handles are properly managed.


3. Potential Panic in Validation (snap_sync.rs:737)

Issue: Uses .expect() when opening storage tries:

store
    .open_locked_storage_trie(
        *hashed_address,
        state_root,
        account_state.storage_root,
    )
    .expect("couldn't open storage trie")

Problem: In validation code, panicking defeats the purpose. If a trie is corrupted or missing, this should return an error, not crash the validator.

Fix: Propagate errors properly:

store
    .open_locked_storage_trie(
        *hashed_address,
        state_root,
        account_state.storage_root,
    )
    .map_err(|e| TrieError::Verify(format!("Failed to open storage trie: {}", e)))?
    .validate()

Same issue in: Lines 695, 715, 764


Performance & Correctness

4. Unnecessary Clones in Hot Path (trie.rs:597)

Issue: The parallel validation clones paths and node refs for each iteration:

validate_subtree(db, start_path.clone(), start_ref.clone())

Impact: Nibbles and NodeRef clones might be cheap (Arc-based), but verify this. If Nibbles::clone() allocates, this could hurt performance.

Recommendation: Profile and consider using references if safe, or document why clones are necessary.


5. Chunk Size Constant Lacks Justification (snap_sync.rs:719)

Issue: CHUNK_SIZE = 4096 is hardcoded without explanation.

const CHUNK_SIZE: usize = 4096;

Problem: This value significantly impacts memory usage and parallelism. Too large = memory pressure; too small = overhead from spawning tasks.

Recommendation: Either:

  • Document why 4096 was chosen (based on profiling?)
  • Make it configurable
  • Use a heuristic based on available cores

6. Error Handling Inconsistency (snap_sync.rs:728-743)

Issue: Early break on error but no logging before the loop ends:

result = chunk.par_iter().try_for_each(...);

if result.is_err() {
    break;  // Silent early exit
}

Later at line 751-752:

if is_valid.is_err() {
    std::process::exit(1);
}

Problem: If an error occurs mid-validation, there's no indication of which storage trie failed until the very end when we call process::exit(1).

Fix: Log the error immediately:

if let Err(e) = result {
    error!("Storage validation failed: {}", e);
    result = Err(e);
    break;
}

Minor Issues

7. Missing HashSet Import (snap_sync.rs:761)

Issue: HashSet is used but verify the import is correct:

let mut unique_hashes = HashSet::new();

Check: Ensure use std::collections::HashSet; is at the top (it is at line 6, confirmed).

Status: ✅ Correct


8. Code Duplication in Error Messages (trie.rs:621, 642)

Issue: Similar error formatting appears twice:

TrieError::Verify(format!("Missing node at path {path:?}"))
TrieError::Verify(format!("Node count mismatch in subtree, expected {expected_count} more"))

Recommendation: Consider a helper function or consistent error formatting pattern.


9. Typo in Log Message (snap_sync.rs:702)

Issue: "Succesfully" → "Successfully"

info!("Succesfully validated tree, {state_root} found");

Ethereum-Specific Considerations

10. Storage Root Validation Correctness

Verification Needed: Confirm that EMPTY_TRIE_HASH correctly identifies EOAs vs. contracts with empty storage.

  • EOAs have storage_root == KECCAK256(RLP([]))
  • Contracts with no storage slots should also have the same root

Status: Assuming EMPTY_TRIE_HASH is correctly defined as 0x56e81f171bcc55a6ff8345e692c0f86e5b47e5b8b8b8d3355c6c7fbf7f1f3f6.

Recommendation: Add a comment explaining this optimization targets EOAs specifically.


11. Code Hash Deduplication (snap_sync.rs:766)

Good: The deduplication is a smart optimization given proxy patterns and ERC20 clones.

Concern: The code checks code_hash != EMPTY_KECCACK_HASH but doesn't verify that accounts with bytecode actually have code_hash set correctly.

Low Risk: This is validation code, not consensus code, so the impact is limited to catching bugs.


Security Considerations

12. DOS via Memory Exhaustion (snap_sync.rs:723)

Scenario: If an attacker controls the database state during snap sync, they could create a trie with many non-empty storage roots, causing large chunk allocations.

Impact: Low - this is debug-only validation code, and the chunk size limits blast radius.

Recommendation: Consider adding a max iteration limit as a safeguard:

let mut total_processed = 0;
const MAX_ACCOUNTS: usize = 100_000_000;

loop {
    if total_processed > MAX_ACCOUNTS {
        error!("Exceeded max accounts during validation");
        return Err(...);
    }
    // ...
}

Code Quality

13. Function code_exists is Well-Designed (store.rs:716-733)

Positive: This function correctly:

  • Checks cache first (fast path)
  • Falls back to existence check without reading value (efficient)
  • Properly handles lock errors

Suggestion: Consider adding a unit test to verify cache hit/miss behavior.


14. Parallel Validation Architecture

Positive: Splitting at the root branch is clever and leverages the trie structure well. The 16-way split aligns with hexary trie branching.

Concern: Non-branch root fallback (line 600-602) means small tries get no benefit. Consider logging this case for visibility during testing.


Testing Recommendations

  1. Test parallel validation on corrupted tries - ensure errors are caught correctly
  2. Benchmark with various trie shapes - measure speedup for deep vs. wide tries
  3. Test with very small tries (< 16 nodes) - verify fallback path works
  4. Memory profiling - ensure chunking doesn't cause excessive allocations
  5. Concurrent access testing - verify thread safety of TrieDB implementations

Summary

Approve with changes required

The optimization approach is sound and well-motivated. Key issues to address before merge:

  1. CRITICAL: Fix thread safety in validate_parallel (issue docs: add milestones #1)
  2. REQUIRED: Remove .expect() calls in validation paths (issue build: add Github actions boilerplate #3)
  3. RECOMMENDED: Improve error logging (issue Add lint and test jobs to CI #6)
  4. MINOR: Fix typo in success message (issue feat: add support for separate engine and rpc apis #9)

After addressing the critical and required issues, this PR will be a solid performance improvement for development workflow.


Automated review by Claude (Anthropic) · custom prompt

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR speeds up ethrex’s post-snap-sync correctness validations (state trie, storage tries, and bytecode presence) that run under debug_assert! in release-with-debug-assertions, reducing end-to-end sync validation time while preserving the extra integrity checks.

Changes:

  • Added parallel trie validation by splitting root branch subtrees and validating them concurrently with rayon.
  • Optimized storage trie validation by skipping empty storage roots and replacing par_bridge() with chunked collection + par_iter().
  • Optimized bytecode validation by deduplicating code hashes and adding a Store::code_exists() helper for existence checks.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
crates/storage/store.rs Adds code_exists() helper used by bytecode validation.
crates/networking/p2p/sync/snap_sync.rs Switches state root validation to parallel mode; optimizes storage and bytecode validation workflows.
crates/common/trie/trie.rs Introduces Trie::validate_parallel() and subtree validator implementation.
crates/common/trie/Cargo.toml Adds rayon dependency for parallel trie validation.
Cargo.lock Locks new rayon dependency.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +727 to +732
// Check DB without reading the full value
Ok(self
.backend
.begin_read()?
.get(ACCOUNT_CODES, code_hash.as_bytes())?
.is_some())
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

code_exists claims to avoid "blob value reads", but it calls begin_read().get(ACCOUNT_CODES, ...), which on the RocksDB backend is configured with set_enable_blob_files(true) for ACCOUNT_CODES and will still fetch the value (often from blob files). If the goal is an existence-only check without reading bytecode blobs, consider checking ACCOUNT_CODE_METADATA (fixed-size, non-blob CF) first and only falling back to ACCOUNT_CODES if metadata is missing (older DBs).

Copilot uses AI. Check for mistakes.
Comment on lines +717 to +725
// Check cache first
if self
.account_code_cache
.lock()
.map_err(|_| StoreError::LockError)?
.get(&code_hash)?
.is_some()
{
return Ok(true);
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

code_exists always takes the account_code_cache mutex even though cache hits are likely rare during bulk validation. When called from a rayon par_iter (as in validate_bytecodes), this mutex can serialize the hot path and reduce parallel speedups. Consider either skipping the code cache for code_exists, or restructuring the cache to allow concurrent reads (e.g., RwLock + a read-only lookup method).

Suggested change
// Check cache first
if self
.account_code_cache
.lock()
.map_err(|_| StoreError::LockError)?
.get(&code_hash)?
.is_some()
{
return Ok(true);
// Opportunistically check cache first without blocking other threads.
match self.account_code_cache.try_lock() {
Ok(cache) => {
if cache.get(&code_hash)?.is_some() {
return Ok(true);
}
}
Err(std::sync::TryLockError::Poisoned(_)) => {
// Preserve previous behavior on poisoned lock.
return Err(StoreError::LockError);
}
Err(std::sync::TryLockError::WouldBlock) => {
// Contended: skip cache and fall back to DB check.
}

Copilot uses AI. Check for mistakes.
use rayon::prelude::*;
let missing: Vec<_> = unique_hashes
.par_iter()
.filter(|code_hash| !store.code_exists(**code_hash).unwrap_or(false))
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

unwrap_or(false) converts any StoreError from code_exists into "missing" without logging the underlying error, which can make diagnosing DB/corruption issues harder (and can misattribute failures). Consider handling the Err case explicitly (e.g., log the error and treat validation as failed) instead of silently defaulting to false.

Suggested change
.filter(|code_hash| !store.code_exists(**code_hash).unwrap_or(false))
.filter(|code_hash| {
match store.code_exists(**code_hash) {
Ok(exists) => !exists,
Err(err) => {
error!(
"Error while checking existence of code hash {:x}: {:?}",
code_hash, err
);
// Treat store errors as validation failures (equivalent to "missing")
true
}
}
})

Copilot uses AI. Check for mistakes.
account_state.storage_root,
)
.expect("couldn't open storage trie")
.validate()
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not validate_parallel?

let db = &*self.db;
let root_node = self
.root
.get_node_checked(db, Nibbles::default())?
Copy link
Contributor

Choose a reason for hiding this comment

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

Note: get_node_checked won't detect a hash mismatch if the root node was already loaded.

This shouldn't matter right now given we usually call this on a freshly opened trie, but it might be a good idea to verify.

start_path: Nibbles,
start_ref: NodeRef,
) -> Result<(), TrieError> {
let mut expected_count: isize = 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

It shouldn't be necessary to keep this. validate() only had this because the iterator has no way of distinguishing "ended because of error" and "ended because all nodes were visited".

@github-project-automation github-project-automation bot moved this to In Review in ethrex_l1 Feb 13, 2026
@github-project-automation github-project-automation bot moved this from In Review to In Progress in ethrex_l1 Feb 13, 2026
@ilitteri ilitteri force-pushed the snap-sync-validation-optimizations branch from 22da23c to f4a8095 Compare February 25, 2026 13:08
…ation

Optimize the three debug_assert! validation functions that run after snap
sync to verify trie integrity. These are used during testing with
release-with-debug-assertions builds to validate optimization correctness.

validate_state_root:
- Add validate_parallel() to Trie that splits at the root branch node
  and validates each of the 16 subtrees independently via rayon (~4-8x)

validate_storage_root:
- Skip accounts with empty storage tries (EMPTY_TRIE_HASH) — ~90% of
  accounts are EOAs with nothing to validate
- Replace par_bridge() with chunked collect + par_iter to eliminate
  per-item mutex contention from the sequential iterator adapter

validate_bytecodes:
- Deduplicate code hashes via HashSet before checking — ~28:1 ratio of
  contracts to unique bytecodes on mainnet
- Add Store::code_exists() for key-only existence checks, avoiding full
  blob reads and RLP decoding
- Parallelize with rayon par_iter on the deduplicated set
- Remove unused top-level `rayon::iter::ParallelIterator` import (clippy error)
- Update sp1/risc0/zisk/openvm Cargo.lock files for rayon dep in ethrex-trie
- Propagate DB errors in `validate_bytecodes` instead of silently converting
  them to "code missing" via `unwrap_or(false)` (ElFantasma review)
- Fix `code_exists` docstring: the underlying `get()` still reads the full
  value from RocksDB; the savings are from skipping RLP decoding and Code
  struct construction, not blob reads (ElFantasma review)
…import

- Fix cargo fmt in trie.rs
- Update remaining Cargo.lock files (tee/quote-gen, levm/bench, tooling)
- Add ParallelIterator import to not(rocksdb) insert_storages to fix
  compilation in prover/exec backend CI targets
@ilitteri ilitteri force-pushed the snap-sync-validation-optimizations branch from 3c691a8 to 1a190ab Compare February 25, 2026 13:34
@github-actions
Copy link

github-actions bot commented Feb 25, 2026

Benchmark Results Comparison

No significant difference was registered for any benchmark run.

Detailed Results

Benchmark Results: BubbleSort

Command Mean [s] Min [s] Max [s] Relative
main_revm_BubbleSort 3.012 ± 0.064 2.949 3.156 1.11 ± 0.02
main_levm_BubbleSort 2.733 ± 0.028 2.700 2.788 1.01 ± 0.01
pr_revm_BubbleSort 2.972 ± 0.013 2.949 2.989 1.09 ± 0.01
pr_levm_BubbleSort 2.715 ± 0.011 2.700 2.736 1.00

Benchmark Results: ERC20Approval

Command Mean [s] Min [s] Max [s] Relative
main_revm_ERC20Approval 1.008 ± 0.021 0.994 1.063 1.01 ± 0.02
main_levm_ERC20Approval 1.031 ± 0.005 1.021 1.036 1.03 ± 0.01
pr_revm_ERC20Approval 0.998 ± 0.009 0.986 1.013 1.00
pr_levm_ERC20Approval 1.033 ± 0.009 1.022 1.056 1.04 ± 0.01

Benchmark Results: ERC20Mint

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Mint 133.2 ± 1.7 131.4 137.2 1.00
main_levm_ERC20Mint 159.2 ± 1.6 157.1 162.3 1.20 ± 0.02
pr_revm_ERC20Mint 133.4 ± 1.0 132.1 135.2 1.00 ± 0.01
pr_levm_ERC20Mint 158.8 ± 6.2 155.8 176.4 1.19 ± 0.05

Benchmark Results: ERC20Transfer

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Transfer 233.0 ± 2.2 230.3 236.9 1.00
main_levm_ERC20Transfer 268.9 ± 4.4 263.8 279.3 1.15 ± 0.02
pr_revm_ERC20Transfer 233.9 ± 2.0 231.7 237.5 1.00 ± 0.01
pr_levm_ERC20Transfer 270.3 ± 6.0 263.0 282.8 1.16 ± 0.03

Benchmark Results: Factorial

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Factorial 229.2 ± 0.7 228.2 230.0 1.00
main_levm_Factorial 249.4 ± 2.1 247.3 253.3 1.09 ± 0.01
pr_revm_Factorial 229.2 ± 0.5 228.3 230.1 1.00 ± 0.00
pr_levm_Factorial 250.1 ± 4.0 247.5 261.0 1.09 ± 0.02

Benchmark Results: FactorialRecursive

Command Mean [s] Min [s] Max [s] Relative
main_revm_FactorialRecursive 1.670 ± 0.043 1.608 1.734 1.00 ± 0.04
main_levm_FactorialRecursive 9.657 ± 0.031 9.615 9.713 5.78 ± 0.20
pr_revm_FactorialRecursive 1.670 ± 0.056 1.553 1.756 1.00
pr_levm_FactorialRecursive 9.626 ± 0.031 9.592 9.688 5.77 ± 0.20

Benchmark Results: Fibonacci

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Fibonacci 208.4 ± 1.6 205.5 212.1 1.00
main_levm_Fibonacci 227.6 ± 2.9 225.1 232.9 1.09 ± 0.02
pr_revm_Fibonacci 208.9 ± 0.6 208.0 209.6 1.00 ± 0.01
pr_levm_Fibonacci 227.2 ± 3.0 224.8 234.4 1.09 ± 0.02

Benchmark Results: FibonacciRecursive

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_FibonacciRecursive 861.2 ± 7.5 848.7 871.3 1.26 ± 0.01
main_levm_FibonacciRecursive 683.9 ± 3.3 675.5 687.6 1.00
pr_revm_FibonacciRecursive 847.2 ± 14.5 820.6 873.1 1.24 ± 0.02
pr_levm_FibonacciRecursive 686.7 ± 14.1 673.3 720.3 1.00 ± 0.02

Benchmark Results: ManyHashes

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ManyHashes 8.4 ± 0.0 8.3 8.5 1.00
main_levm_ManyHashes 9.8 ± 0.1 9.7 10.0 1.17 ± 0.01
pr_revm_ManyHashes 8.4 ± 0.1 8.3 8.6 1.00 ± 0.01
pr_levm_ManyHashes 9.8 ± 0.1 9.7 10.0 1.17 ± 0.01

Benchmark Results: MstoreBench

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_MstoreBench 266.0 ± 9.8 260.0 291.1 1.21 ± 0.05
main_levm_MstoreBench 221.6 ± 5.2 216.8 233.6 1.01 ± 0.03
pr_revm_MstoreBench 261.9 ± 1.5 260.3 265.7 1.19 ± 0.01
pr_levm_MstoreBench 219.8 ± 1.8 217.3 224.3 1.00

Benchmark Results: Push

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Push 298.2 ± 1.9 296.7 302.9 1.07 ± 0.02
main_levm_Push 277.5 ± 3.8 273.7 286.7 1.00
pr_revm_Push 298.7 ± 2.7 295.1 305.4 1.08 ± 0.02
pr_levm_Push 277.6 ± 7.3 273.8 297.8 1.00 ± 0.03

Benchmark Results: SstoreBench_no_opt

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_SstoreBench_no_opt 169.5 ± 4.3 164.2 178.4 1.56 ± 0.04
main_levm_SstoreBench_no_opt 108.4 ± 0.9 107.4 110.5 1.00
pr_revm_SstoreBench_no_opt 168.7 ± 4.2 163.6 174.1 1.56 ± 0.04
pr_levm_SstoreBench_no_opt 109.5 ± 3.2 107.4 118.4 1.01 ± 0.03

@github-actions
Copy link

github-actions bot commented Feb 25, 2026

Benchmark Block Execution Results Comparison Against Main

Command Mean [s] Min [s] Max [s] Relative
base 65.568 ± 0.302 65.212 66.218 1.00
head 65.860 ± 0.471 65.572 67.139 1.00 ± 0.01

@github-project-automation github-project-automation bot moved this from In Progress to In Review in ethrex_l1 Feb 25, 2026
@ilitteri ilitteri enabled auto-merge February 25, 2026 21:20
@ilitteri ilitteri added this pull request to the merge queue Feb 25, 2026
Merged via the queue into main with commit ffefcdf Feb 25, 2026
66 checks passed
@ilitteri ilitteri deleted the snap-sync-validation-optimizations branch February 25, 2026 23:06
@github-project-automation github-project-automation bot moved this from In Review to Done in ethrex_l1 Feb 25, 2026
@github-project-automation github-project-automation bot moved this from Todo to Done in ethrex_performance Feb 25, 2026
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: Done
Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants