Skip to content

Commit 4daf481

Browse files
feat: add block root slot index (#495)
## 🗒️ Description / Motivation - Adds a canonical slot-to-block-root index to storage. - BlocksByRange currently has to walk backward from the head and collect roots by slot. - This PR makes range lookups efficient by reading canonical block roots directly by slot. ## What Changed - Added a new `BlockRoots` storage table: - key: slot / block number - value: canonical block root - Registers the new table for in-memory and RocksDB backends. - Initializes the index for anchor/checkpoint stores. - Maintains the index when fork-choice head changes. - Handles reorgs by deleting old canonical slot entries and inserting the new canonical branch. - Rebuilds the index on DB restore if the persisted index is missing. - Cleans index entries when old block data is pruned. - Updates BlocksByRange handling to use `Store::get_block_root_by_slot` instead of walking parent headers. ## Correctness / Behavior Guarantees - The index points only to canonical block roots. - Side-fork blocks do not appear in BlocksByRange responses. - Skipped slots remain absent from the index and are skipped in responses. - Reorgs update the index to match the new canonical chain. - Existing persisted DBs can rebuild the index from the current head chain. - Pruned blocks have their canonical index entries removed when applicable. ## Tests Added / Run - Added storage tests for: - canonical slot index updates - skipped slots - side forks - reorgs - DB restore index rebuild - pruning cleanup - Existing BlocksByRange test now exercises indexed lookup. ## Related Issues / PRs - Closes #355 - Related to #348 - Related to #351 ## ✅ Verification Checklist - [x] Ran `make fmt` — clean - [x] Ran `make lint` (clippy with `-D warnings`) — clean - [x] Ran `cargo test --workspace --release` — all passing --------- Co-authored-by: Tomás Grüner <47506558+MegaRedHand@users.noreply.github.com>
1 parent a886568 commit 4daf481

5 files changed

Lines changed: 249 additions & 80 deletions

File tree

crates/blockchain/src/events.rs

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -474,42 +474,6 @@ mod tests {
474474
assert!(matches!(rx.try_recv(), Err(TryRecvError::Empty)));
475475
}
476476

477-
/// A head whose header cannot be read is skipped (warn), while checkpoint
478-
/// moves still emit.
479-
#[test]
480-
fn chain_event_diff_skips_head_with_missing_header() {
481-
let mut store = test_store();
482-
let bus = EventBus::new(8);
483-
let mut rx = bus.subscribe();
484-
485-
let snapshot = ChainEventSnapshot::capture(&store);
486-
487-
// Point the head at a root with no stored header; advance finalized to
488-
// a real block so its event still fires.
489-
let orphan_head = H256([7u8; 32]);
490-
let genesis = store.head().expect("store head exists");
491-
let finalized_root = H256([8u8; 32]);
492-
let finalized_state = H256([88u8; 32]);
493-
insert_test_block(&mut store, finalized_root, 1, genesis, finalized_state);
494-
let finalized = Checkpoint {
495-
root: finalized_root,
496-
slot: 1,
497-
};
498-
store
499-
.update_checkpoints(ForkCheckpoints::new(orphan_head, None, Some(finalized)))
500-
.expect("update_checkpoints should succeed");
501-
502-
snapshot.diff_and_emit(&store, &bus, 1);
503-
504-
match rx.try_recv().unwrap() {
505-
ChainEvent::FinalizedCheckpoint { slot, block, state } => {
506-
assert_eq!((slot, block, state), (1, finalized_root, finalized_state));
507-
}
508-
other => panic!("expected finalized_checkpoint only, got: {other:?}"),
509-
}
510-
assert!(matches!(rx.try_recv(), Err(TryRecvError::Empty)));
511-
}
512-
513477
/// A head far below the wall-clock slot (catch-up/backfill) emits no
514478
/// `head` event, but `justified_checkpoint`/`finalized_checkpoint` still
515479
/// fire since only `head` is gated.

crates/net/p2p/src/req_resp/handlers.rs

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::collections::{HashMap, HashSet};
1+
use std::collections::HashSet;
22

33
use ethlambda_storage::Store;
44
use libp2p::{PeerId, request_response};
@@ -269,31 +269,17 @@ fn canonical_blocks_by_range(store: &Store, start_slot: u64, count: u64) -> Vec<
269269
return Vec::new();
270270
};
271271

272-
let mut roots_by_slot = HashMap::new();
273-
let mut current_root = store.head().expect("head block exists");
274-
275-
while !current_root.is_zero() {
276-
let Ok(Some(header)) = store.get_block_header(&current_root) else {
277-
break;
278-
};
279-
280-
if header.slot < start_slot {
281-
break;
282-
}
283-
284-
if header.slot <= end_slot {
285-
roots_by_slot.insert(header.slot, current_root);
286-
}
287-
288-
current_root = header.parent_root;
289-
}
290-
291-
(start_slot..=end_slot)
292-
.filter_map(|slot| {
293-
let root = roots_by_slot.get(&slot)?;
294-
store.get_signed_block(root).ok().flatten()
272+
store
273+
.get_signed_blocks_by_slot_range(start_slot, end_slot)
274+
.inspect_err(|err| {
275+
warn!(
276+
start_slot,
277+
end_slot,
278+
?err,
279+
"Failed to get signed blocks by slot range"
280+
)
295281
})
296-
.collect()
282+
.unwrap_or_default()
297283
}
298284

299285
async fn handle_blocks_by_root_response(

crates/storage/src/api/tables.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ pub enum Table {
1313
/// finalized boundary are pruned (`prune_old_block_signatures`), while
1414
/// headers and bodies are kept forever.
1515
BlockSignatures,
16+
/// Canonical block index: slot -> block root
17+
BlockRoots,
1618
/// State storage: H256 -> State
1719
///
1820
/// Holds full-state snapshots only: the bootstrap anchor plus one anchor
@@ -35,10 +37,11 @@ pub enum Table {
3537
}
3638

3739
/// All table variants.
38-
pub const ALL_TABLES: [Table; 7] = [
40+
pub const ALL_TABLES: [Table; 8] = [
3941
Table::BlockHeaders,
4042
Table::BlockBodies,
4143
Table::BlockSignatures,
44+
Table::BlockRoots,
4245
Table::States,
4346
Table::StateDiffs,
4447
Table::Metadata,
@@ -52,6 +55,7 @@ impl Table {
5255
Table::BlockHeaders => "block_headers",
5356
Table::BlockBodies => "block_bodies",
5457
Table::BlockSignatures => "block_signatures",
58+
Table::BlockRoots => "block_roots",
5559
Table::States => "states",
5660
Table::StateDiffs => "state_diffs",
5761
Table::Metadata => "metadata",

crates/storage/src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
use ethlambda_types::primitives::H256;
2+
13
#[derive(Debug, thiserror::Error)]
24
pub enum Error {
35
#[error("storage error: {0}")]
46
Storage(#[from] crate::api::Error),
7+
#[error("unexpected missing block header for root {0}")]
8+
UnexpectedMissingBlockHeader(H256),
59
}

0 commit comments

Comments
 (0)