Skip to content

perf(levm): add block hash caching to improve retrieval performance#6180

Open
akshay-ap wants to merge 14 commits into
lambdaclass:mainfrom
akshay-ap:feat/cache-block-hash
Open

perf(levm): add block hash caching to improve retrieval performance#6180
akshay-ap wants to merge 14 commits into
lambdaclass:mainfrom
akshay-ap:feat/cache-block-hash

Conversation

@akshay-ap

@akshay-ap akshay-ap commented Feb 11, 2026

Copy link
Copy Markdown

Fixes #5689

Copilot AI review requested due to automatic review settings February 11, 2026 15:59
@akshay-ap akshay-ap requested a review from a team as a code owner February 11, 2026 15:59
@akshay-ap akshay-ap marked this pull request as draft February 11, 2026 16:00
@akshay-ap akshay-ap changed the title feat(store): add block hash caching to improve retrieval performance pref(levm): add block hash caching to improve retrieval performance Feb 11, 2026
@greptile-apps

greptile-apps Bot commented Feb 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a 256-entry LRU block hash cache to Store to avoid repeated DB reads when the EVM's BLOCKHASH opcode looks up recent block hashes.

  • The cache is populated by forkchoice_update_inner and correctly evicts stale entries during reorgs and remove_block, with two new tests covering those invariants.
  • Cache reads hold the Mutex exclusively (required because LruCache::get updates recency), and the lock-error path is consistent with the rest of the file's error-handling pattern.
  • Cache misses that fall through to the DB do not write the result back into the LRU, so the cache stays cold for any block that was already canonical before the current process started (e.g., after restart or during sync catch-up).

Confidence Score: 5/5

Safe to merge; the correctness-critical invariants (eviction on reorg and remove_block) are properly implemented and tested. The cache never serves a hash after the DB entry is deleted.

The cache eviction logic is sound: reorgs pop the abandoned range from the LRU before inserting the new canonical set, and remove_block evicts unconditionally after the DB commit. Two new tests directly exercise both paths. The only gaps are a missing read-back on cache miss (limiting warm-up after restarts) and a misleading code comment about the poisoned-lock fallback path, neither of which affects correctness.

No files require special attention beyond the warm-up limitation noted in the review comments.

Important Files Changed

Filename Overview
crates/storage/store.rs Adds a 256-entry LRU block hash cache populated by forkchoice_update_inner; reads acquire a Mutex even for lookups (required because LruCache.get updates recency); cache misses fall through to the DB but results are not written back, limiting warm-up after restarts; eviction on reorg and remove_block is correct; two dedicated tests cover the eviction invariants.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["get_canonical_block_hash_sync(block_number)"] --> B{Is latest_block_header?}
    B -- yes --> C[Return head hash]
    B -- no --> D{LRU cache hit?}
    D -- yes --> E[Return cached hash]
    D -- no --> F[DB read: CANONICAL_BLOCK_HASHES]
    F --> G{Found in DB?}
    G -- yes --> H[Return DB hash\n⚠️ not written back to cache]
    G -- no --> I[Return None]

    J["forkchoice_update_inner(new_canonical_blocks, head)"] --> K[Read latest block number]
    K --> L[Collect blocks_to_cache]
    L --> M[spawn_blocking: DB write txn.commit]
    M --> N[Acquire block_hash_cache lock]
    N -- poisoned --> O[warn + skip cache update]
    N -- ok --> P[Evict reorged blocks head+1..=latest]
    P --> Q[Insert new canonical blocks into LRU]

    R["remove_block(block_number)"] --> S[DB delete commit]
    S --> T[Acquire block_hash_cache lock]
    T -- ok --> U[cache.pop block_number]
    T -- poisoned --> V[warn + skip eviction]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["get_canonical_block_hash_sync(block_number)"] --> B{Is latest_block_header?}
    B -- yes --> C[Return head hash]
    B -- no --> D{LRU cache hit?}
    D -- yes --> E[Return cached hash]
    D -- no --> F[DB read: CANONICAL_BLOCK_HASHES]
    F --> G{Found in DB?}
    G -- yes --> H[Return DB hash\n⚠️ not written back to cache]
    G -- no --> I[Return None]

    J["forkchoice_update_inner(new_canonical_blocks, head)"] --> K[Read latest block number]
    K --> L[Collect blocks_to_cache]
    L --> M[spawn_blocking: DB write txn.commit]
    M --> N[Acquire block_hash_cache lock]
    N -- poisoned --> O[warn + skip cache update]
    N -- ok --> P[Evict reorged blocks head+1..=latest]
    P --> Q[Insert new canonical blocks into LRU]

    R["remove_block(block_number)"] --> S[DB delete commit]
    S --> T[Acquire block_hash_cache lock]
    T -- ok --> U[cache.pop block_number]
    T -- poisoned --> V[warn + skip eviction]
Loading

Reviews (2): Last reviewed commit: "Merge branch 'main' into feat/cache-bloc..." | Re-trigger Greptile

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps

greptile-apps Bot commented Feb 11, 2026

Copy link
Copy Markdown
Additional Comments (1)

crates/storage/store.rs
Stale canonical cache

forkchoice_update_inner deletes canonical hashes for (head_number + 1)..=latest in the DB, but block_hash_cache is never purged for those block numbers. Because get_canonical_block_hash_sync checks the LRU before hitting the DB, a reorg that truncates the chain can cause this method to return a hash for a block number that is no longer canonical (or no longer present in CANONICAL_BLOCK_HASHES). You likely need to invalidate cache entries for the deleted range when applying the reorg.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/storage/store.rs
Line: 1007:1009

Comment:
**Stale canonical cache**

`forkchoice_update_inner` deletes canonical hashes for `(head_number + 1)..=latest` in the DB, but `block_hash_cache` is never purged for those block numbers. Because `get_canonical_block_hash_sync` checks the LRU before hitting the DB, a reorg that truncates the chain can cause this method to return a hash for a block number that is no longer canonical (or no longer present in `CANONICAL_BLOCK_HASHES`). You likely need to invalidate cache entries for the deleted range when applying the reorg.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds an LRU cache for block hash lookups to improve retrieval performance in the storage layer. The cache stores mappings from block numbers to block hashes, reducing the need to query the database for frequently accessed canonical block hashes.

Changes:

  • Added a 256-entry LRU cache for block number to block hash mappings
  • Integrated cache lookups in get_canonical_block_hash_sync method with fallback to database
  • Updated forkchoice_update_inner to populate cache when canonical blocks are written to the database
Comments suppressed due to low confidence (1)

crates/storage/store.rs:1165

  • The cache is not populated when reading from the database (read-through caching is missing). Following the pattern established by account_code_cache (lines 678-712), when there's a cache miss, the value should be inserted into the cache after being read from the database. Add cache population logic after the database read at lines 1157-1164 to improve cache effectiveness and future read performance.
        // Cache miss: read from database
        let txn = self.backend.begin_read()?;
        txn.get(
            CANONICAL_BLOCK_HASHES,
            block_number.to_le_bytes().as_slice(),
        )?
        .map(|bytes| H256::decode(bytes.as_slice()))
        .transpose()
        .map_err(StoreError::from)
    }

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

Comment thread crates/storage/store.rs Outdated
Comment thread crates/storage/store.rs Outdated
Comment thread crates/storage/store.rs Outdated
Comment thread crates/storage/store.rs Outdated
Comment thread crates/storage/store.rs Outdated
Comment thread crates/storage/store.rs Outdated
Comment thread crates/storage/store.rs
@akshay-ap akshay-ap force-pushed the feat/cache-block-hash branch from d24cac0 to 5c8cdfc Compare June 17, 2026 13:22
@akshay-ap akshay-ap changed the title pref(levm): add block hash caching to improve retrieval performance feat(levm): add block hash caching to improve retrieval performance Jun 17, 2026
@akshay-ap akshay-ap changed the title feat(levm): add block hash caching to improve retrieval performance perf(levm): add block hash caching to improve retrieval performance Jun 17, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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

Comments suppressed due to low confidence (1)

crates/storage/store.rs:1415

  • On a cache miss, the block hash is read from the DB but never inserted into block_hash_cache. This means repeated lookups for the same block number (e.g., after restart before the cache is warmed, or for frequently queried older blocks) will keep hitting the backend.

After decoding a non-None hash from CANONICAL_BLOCK_HASHES, insert it into the LRU cache (best-effort) before returning.

        // Cache miss: read from database
        let txn = self.backend.begin_read()?;
        txn.get(
            CANONICAL_BLOCK_HASHES,
            block_number.to_le_bytes().as_slice(),

Comment thread crates/storage/store.rs Outdated
Comment thread crates/storage/store.rs Outdated
Comment thread crates/storage/store.rs
@akshay-ap akshay-ap marked this pull request as ready for review June 17, 2026 21:30
Comment thread crates/storage/store.rs Outdated
Comment thread crates/storage/store.rs
@akshay-ap akshay-ap requested a review from ElFantasma June 18, 2026 17:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace BLOCKHASH cache with a precomputed array of block hashes

4 participants