feat: add block root slot index#495
Conversation
Greptile SummaryThis PR introduces a canonical slot-to-block-root index (
Confidence Score: 3/5The index maintenance logic and handler change are correct, but from_db_state now silently assumes KEY_HEAD is present and will panic rather than return None on a DB missing that key. The LCA walk in block_root_index_changes is algorithmically sound and the tests cover reorgs, skipped slots, and restore paths well. However, from_db_state now calls store.head_slot() — which panics via get_metadata if KEY_HEAD is absent — without first probing for the key the way it already probes KEY_LATEST_FINALIZED. Any DB that passes the initial key checks but is missing KEY_HEAD will crash the process instead of failing gracefully. Additionally, rebuild_block_root_index takes &self while writing to the backend, deviating from the &mut self convention used everywhere else in the Store API. crates/storage/src/store.rs — specifically the from_db_state restore guard and block_root_index_changes error handling
|
| Filename | Overview |
|---|---|
| crates/storage/src/store.rs | Core of the PR — adds BlockRoots index with init, LCA-walk update-on-head-change, rebuild-on-restore, and prune cleanup. The from_db_state guard can panic if KEY_HEAD is missing; block_root_index_changes uses expect() on header lookups; rebuild_block_root_index is a mutating method with a &self signature. |
| crates/net/p2p/src/req_resp/handlers.rs | Replaces the backward head-walk in canonical_blocks_by_range with a direct indexed slot lookup using get_block_root_by_slot; removes the HashMap and related imports. Logic is correct and the early-exit guard is preserved. |
| crates/storage/src/api/tables.rs | Adds BlockRoots variant to the Table enum and ALL_TABLES constant; name string is consistent with other tables. |
| crates/storage/src/backend/rocksdb.rs | Registers the new block_roots column family in the RocksDB backend; change is minimal and correct. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant FC as Fork Choice
participant Store
participant BlockRoots as BlockRoots Table
participant BlockHeaders as BlockHeaders Table
Note over FC,BlockHeaders: Head update (normal extension)
FC->>Store: update_checkpoints(new_head)
Store->>Store: block_root_index_changes(old_head, new_head)
Store->>BlockHeaders: get_block_header(new_head) [walk back to LCA]
BlockHeaders-->>Store: headers
Store->>BlockRoots: delete_batch(old canonical slots)
Store->>BlockRoots: put_batch(new canonical slots)
Store-->>FC: committed
Note over FC,BlockHeaders: DB restore
FC->>Store: from_db_state(backend)
Store->>BlockRoots: get(head_slot) check index
alt index stale or missing
Store->>Store: rebuild_block_root_index()
Store->>BlockHeaders: walk from head to anchor
Store->>BlockRoots: delete_batch(old) + put_batch(rebuilt)
end
Store-->>FC: Some(store)
Note over FC,BlockHeaders: BlocksByRange P2P request
FC->>Store: get_block_root_by_slot(slot) x count
Store->>BlockRoots: get(slot_key)
BlockRoots-->>Store: block_root
Store-->>FC: signed blocks
%%{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"}}}%%
sequenceDiagram
participant FC as Fork Choice
participant Store
participant BlockRoots as BlockRoots Table
participant BlockHeaders as BlockHeaders Table
Note over FC,BlockHeaders: Head update (normal extension)
FC->>Store: update_checkpoints(new_head)
Store->>Store: block_root_index_changes(old_head, new_head)
Store->>BlockHeaders: get_block_header(new_head) [walk back to LCA]
BlockHeaders-->>Store: headers
Store->>BlockRoots: delete_batch(old canonical slots)
Store->>BlockRoots: put_batch(new canonical slots)
Store-->>FC: committed
Note over FC,BlockHeaders: DB restore
FC->>Store: from_db_state(backend)
Store->>BlockRoots: get(head_slot) check index
alt index stale or missing
Store->>Store: rebuild_block_root_index()
Store->>BlockHeaders: walk from head to anchor
Store->>BlockRoots: delete_batch(old) + put_batch(rebuilt)
end
Store-->>FC: Some(store)
Note over FC,BlockHeaders: BlocksByRange P2P request
FC->>Store: get_block_root_by_slot(slot) x count
Store->>BlockRoots: get(slot_key)
BlockRoots-->>Store: block_root
Store-->>FC: signed blocks
Comments Outside Diff (3)
-
crates/storage/src/store.rs, line 577-579 (link)from_db_statecan panic ifKEY_HEADis absentfrom_db_stateis designed to returnNonegracefully when the DB is empty or incompatible — it probesKEY_CONFIGandKEY_LATEST_FINALIZEDwith?to do so. But it now callsstore.head_slot()→store.head()→get_metadata(KEY_HEAD), andget_metadatauses an innerexpect("metadata key exists")that panics on a missing key. IfKEY_HEADis absent (corrupted write, partial migration, or a hand-crafted fixture like the one fixed in the test at line 1905), the process panics instead of returningNone. The probe forKEY_HEADshould be added alongside the existing probe forKEY_LATEST_FINALIZED.Prompt To Fix With AI
This is a comment left during a code review. Path: crates/storage/src/store.rs Line: 577-579 Comment: **`from_db_state` can panic if `KEY_HEAD` is absent** `from_db_state` is designed to return `None` gracefully when the DB is empty or incompatible — it probes `KEY_CONFIG` and `KEY_LATEST_FINALIZED` with `?` to do so. But it now calls `store.head_slot()` → `store.head()` → `get_metadata(KEY_HEAD)`, and `get_metadata` uses an inner `expect("metadata key exists")` that panics on a missing key. If `KEY_HEAD` is absent (corrupted write, partial migration, or a hand-crafted fixture like the one fixed in the test at line 1905), the process panics instead of returning `None`. The probe for `KEY_HEAD` should be added alongside the existing probe for `KEY_LATEST_FINALIZED`. How can I resolve this? If you propose a fix, please make it concise.
-
crates/storage/src/store.rs, line 849-892 (link)block_root_index_changespanics on missing headers during a reorgBoth
expect("old canonical block header exists")andexpect("new canonical block header exists")will panic if a header cannot be fetched. WhileBLOCKS_TO_KEEP = 21_600makes a reorg crossing the prune boundary essentially impossible on mainnet, the same function is exercised during DB restore rebuilds. A more defensive approach would be to break out of the loop on a missing header (similar to thelet Some(header) = ... else { break }pattern used inrebuild_block_root_index), leaving any deeper chain entries unchanged rather than panicking.Prompt To Fix With AI
This is a comment left during a code review. Path: crates/storage/src/store.rs Line: 849-892 Comment: **`block_root_index_changes` panics on missing headers during a reorg** Both `expect("old canonical block header exists")` and `expect("new canonical block header exists")` will panic if a header cannot be fetched. While `BLOCKS_TO_KEEP = 21_600` makes a reorg crossing the prune boundary essentially impossible on mainnet, the same function is exercised during DB restore rebuilds. A more defensive approach would be to break out of the loop on a missing header (similar to the `let Some(header) = ... else { break }` pattern used in `rebuild_block_root_index`), leaving any deeper chain entries unchanged rather than panicking. How can I resolve this? If you propose a fix, please make it concise.
-
crates/storage/src/store.rs, line 895-923 (link)rebuild_block_root_indextakes&selfbut performs a writeEvery other mutating method on
Store(update_checkpoints,prune_old_blocks,insert_signed_block, etc.) takes&mut self.rebuild_block_root_indexwrites to the backend through the innerArcwhile only holding&self, which is inconsistent and sidesteps Rust's normal exclusivity guarantee. This is safe in the narrow call site infrom_db_state(no other holders exist yet), but makes the method's signature silently lie about its mutation semantics. Changing it to&mut selfwould be consistent with the rest of the API.Prompt To Fix With AI
This is a comment left during a code review. Path: crates/storage/src/store.rs Line: 895-923 Comment: **`rebuild_block_root_index` takes `&self` but performs a write** Every other mutating method on `Store` (`update_checkpoints`, `prune_old_blocks`, `insert_signed_block`, etc.) takes `&mut self`. `rebuild_block_root_index` writes to the backend through the inner `Arc` while only holding `&self`, which is inconsistent and sidesteps Rust's normal exclusivity guarantee. This is safe in the narrow call site in `from_db_state` (no other holders exist yet), but makes the method's signature silently lie about its mutation semantics. Changing it to `&mut self` would be consistent with the rest of the API. How can I resolve this? If you propose a fix, please make it concise.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
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/storage/src/store.rs:577-579
**`from_db_state` can panic if `KEY_HEAD` is absent**
`from_db_state` is designed to return `None` gracefully when the DB is empty or incompatible — it probes `KEY_CONFIG` and `KEY_LATEST_FINALIZED` with `?` to do so. But it now calls `store.head_slot()` → `store.head()` → `get_metadata(KEY_HEAD)`, and `get_metadata` uses an inner `expect("metadata key exists")` that panics on a missing key. If `KEY_HEAD` is absent (corrupted write, partial migration, or a hand-crafted fixture like the one fixed in the test at line 1905), the process panics instead of returning `None`. The probe for `KEY_HEAD` should be added alongside the existing probe for `KEY_LATEST_FINALIZED`.
### Issue 2 of 3
crates/storage/src/store.rs:849-892
**`block_root_index_changes` panics on missing headers during a reorg**
Both `expect("old canonical block header exists")` and `expect("new canonical block header exists")` will panic if a header cannot be fetched. While `BLOCKS_TO_KEEP = 21_600` makes a reorg crossing the prune boundary essentially impossible on mainnet, the same function is exercised during DB restore rebuilds. A more defensive approach would be to break out of the loop on a missing header (similar to the `let Some(header) = ... else { break }` pattern used in `rebuild_block_root_index`), leaving any deeper chain entries unchanged rather than panicking.
### Issue 3 of 3
crates/storage/src/store.rs:895-923
**`rebuild_block_root_index` takes `&self` but performs a write**
Every other mutating method on `Store` (`update_checkpoints`, `prune_old_blocks`, `insert_signed_block`, etc.) takes `&mut self`. `rebuild_block_root_index` writes to the backend through the inner `Arc` while only holding `&self`, which is inconsistent and sidesteps Rust's normal exclusivity guarantee. This is safe in the narrow call site in `from_db_state` (no other holders exist yet), but makes the method's signature silently lie about its mutation semantics. Changing it to `&mut self` would be consistent with the rest of the API.
Reviews (1): Last reviewed commit: "Merge branch 'main' into feat/block-root..." | Re-trigger Greptile
🗒️ Description / Motivation
What Changed
BlockRootsstorage table:Store::get_block_root_by_slotinstead of walking parent headers.Correctness / Behavior Guarantees
Tests Added / Run
Related Issues / PRs
✅ Verification Checklist
make fmt— cleanmake lint(clippy with-D warnings) — cleancargo test --workspace --release— all passing