diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index b933b928..ac41397c 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -642,14 +642,17 @@ async fn fetch_initial_state( // Checkpoint sync path // Prefer resuming from a fresh on-disk state to avoid re-downloading what we already have. - if let Some(store) = Store::from_db_state(backend.clone(), genesis.genesis_time) { + if let Ok(Some(store)) = Store::from_db_state(backend.clone(), genesis.genesis_time) { let now_ms = SystemTime::UNIX_EPOCH .elapsed() .expect("already past the unix epoch") .as_millis() as u64; let current_slot = now_ms.saturating_sub(genesis.genesis_time * 1000) / MILLISECONDS_PER_SLOT; - let finalized_slot = store.latest_finalized().slot; + let finalized_slot = store + .latest_finalized() + .expect("latest finalized checkpoint exists") + .slot; let gap = current_slot.saturating_sub(finalized_slot); if gap <= MAX_RESUMABLE_DB_STATE_AGE { info!( diff --git a/crates/blockchain/src/coverage.rs b/crates/blockchain/src/coverage.rs index 2cd019f3..820eeb73 100644 --- a/crates/blockchain/src/coverage.rs +++ b/crates/blockchain/src/coverage.rs @@ -130,7 +130,7 @@ pub(crate) fn emit_post_block_coverage( // the head is normally the block proposed at `reporting_slot + 1`, which // carries this round's votes; filter by `data.slot` so we count the same // cohort even if the head is at a different slot. - if let Some(block) = store.get_block(&store.head()) { + if let Ok(Some(block)) = store.get_block(&store.head().expect("head exists")) { for att in block.body.attestations.iter() { if att.data.slot == reporting_slot { cov_add(&mut block_v, &mut block_s, &att.aggregation_bits); diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index 32c2ac15..4eded799 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -113,7 +113,10 @@ impl BlockChain { ) -> BlockChain { metrics::set_is_aggregator(aggregator.is_enabled()); metrics::set_node_sync_status(metrics::SyncStatus::Idle); - let genesis_time = store.config().genesis_time; + let genesis_time = store + .config() + .expect("failed to load config: config missing or database error") + .genesis_time; let mut key_manager = key_manager::KeyManager::new(validator_keys); // Catch XMSS keys up to the current slot before the first tick @@ -213,7 +216,7 @@ pub struct BlockChainServer { impl BlockChainServer { async fn on_tick(&mut self, timestamp_ms: u64, ctx: &Context) { - let genesis_time_ms = self.store.config().genesis_time * 1000; + let genesis_time_ms = self.store.config().expect("config exists").genesis_time * 1000; // Calculate current slot and interval from milliseconds let time_since_genesis_ms = timestamp_ms.saturating_sub(genesis_time_ms); @@ -227,7 +230,7 @@ impl BlockChainServer { // inside VMs, so a tick scheduled for the next interval boundary can fire // while the wall clock still reads the previous interval. let tick_interval = time_since_genesis_ms / MILLISECONDS_PER_INTERVAL; - let store_time = self.store.time(); + let store_time = self.store.time().expect("store time exists"); if store_time > 0 && tick_interval <= store_time { debug!( @@ -514,7 +517,7 @@ impl BlockChainServer { async fn propose_block(&mut self, slot: u64, validator_id: u64) { info!(%slot, %validator_id, "We are the proposer for this slot"); - let genesis_time_ms = self.store.config().genesis_time * 1000; + let genesis_time_ms = self.store.config().expect("config exists").genesis_time * 1000; let slot_start_ms = genesis_time_ms + slot * MILLISECONDS_PER_SLOT; // Build the block. `produce_block_with_signatures` advances the store to @@ -702,8 +705,18 @@ impl BlockChainServer { fn process_block(&mut self, signed_block: SignedBlock) -> Result<(), StoreError> { store::on_block(&mut self.store, signed_block)?; metrics::update_head_slot(self.store.head_slot()); - metrics::update_latest_justified_slot(self.store.latest_justified().slot); - metrics::update_latest_finalized_slot(self.store.latest_finalized().slot); + let latest_justified_slot = self + .store + .latest_justified() + .expect("Error: Latest justified checkpoint does not exist") + .slot; + metrics::update_latest_justified_slot(latest_justified_slot); + let latest_finalized_slot = self + .store + .latest_finalized() + .expect("Error: Latest finalized checkpoint does not exist") + .slot; + metrics::update_latest_finalized_slot(latest_finalized_slot); metrics::update_validators_count(self.key_manager.validator_ids().len() as u64); for table in ALL_TABLES { @@ -727,7 +740,9 @@ impl BlockChainServer { // Prune old states and blocks AFTER the entire cascade completes. // Running this mid-cascade would delete states that pending children // still need, causing re-processing loops when fallback pruning is active. - self.store.prune_old_data(); + self.store + .prune_old_data() + .expect("DB pruning should succeed"); } /// Try to process a single block. If its parent state is missing, store it @@ -747,7 +762,12 @@ impl BlockChainServer { // already part of the canonical chain and cannot affect fork choice. // Discard any pending children: since we won't process this block, // children referencing it as parent would remain stuck indefinitely. - if slot <= self.store.latest_finalized().slot { + let latest_finalized_slot = self + .store + .latest_finalized() + .expect("Error: Latest finalized checkpoint does not exist") + .slot; + if slot <= latest_finalized_slot { self.discard_pending_subtree(block_root); return; } @@ -759,7 +779,7 @@ impl BlockChainServer { // Catching this early also avoids persisting bogus future blocks to // RocksDB and triggering BlocksByRoot fan-out for fabricated parents. let block_start_interval = slot.saturating_mul(INTERVALS_PER_SLOT); - let store_time = self.store.time(); + let store_time = self.store.time().expect("store time exists"); if block_start_interval > store_time + GOSSIP_DISPARITY_INTERVALS { warn!( %slot, @@ -774,7 +794,11 @@ impl BlockChainServer { } // Check if parent state exists before attempting to process - if !self.store.has_state(&parent_root) { + if !self + .store + .has_state(&parent_root) + .expect("DB read should succeed") + { info!(%slot, %parent_root, %block_root, "Block parent missing, storing as pending"); // Resolve the actual missing ancestor by walking the chain. A stale entry @@ -802,14 +826,23 @@ impl BlockChainServer { // session, the actual missing block is further up the chain. // Note: this loop always terminates — blocks reference parents by hash, // so a cycle would require a hash collision. - while let Some(header) = self.store.get_block_header(&missing_root) { - if self.store.has_state(&header.parent_root) { + while let Some(header) = self + .store + .get_block_header(&missing_root) + .expect("DB read should succeed") + { + if self + .store + .has_state(&header.parent_root) + .expect("DB read should succeed") + { // Parent state available — enqueue for processing, cascade // handles the rest via the outer loop. let block = self .store .get_signed_block(&missing_root) - .expect("header and parent state exist, so the full signed block must too"); + .expect("header and parent state exist, so the full signed block must too") + .unwrap(); queue.push_back(block); return; } @@ -921,7 +954,7 @@ impl BlockChainServer { self.pending_block_parents.remove(&block_root); // Load block data from DB - let Some(child_block) = self.store.get_signed_block(&block_root) else { + let Ok(Some(child_block)) = self.store.get_signed_block(&block_root) else { warn!( block_root = %ShortRoot(&block_root.0), "Pending block missing from DB, skipping" @@ -966,7 +999,11 @@ impl BlockChainServer { fn update_sync_status(&mut self, current_slot: u64) { let head_slot = self.store.head_slot(); - let max_seen_slot = self.store.max_live_chain_slot().unwrap_or(head_slot); + let max_seen_slot = self + .store + .max_live_chain_slot() + .expect("max live chain slot exists") + .unwrap_or(head_slot); let status = self .sync_status .update(current_slot, head_slot, max_seen_slot); @@ -990,7 +1027,7 @@ impl BlockChainServer { let now_ms = unix_now_ms(); self.on_tick(now_ms, ctx).await; - let genesis_time_ms = self.store.config().genesis_time * 1000; + let genesis_time_ms = self.store.config().expect("Config exists").genesis_time * 1000; let remaining_at_entry = ms_until_next_interval(now_ms, genesis_time_ms); let now_after_tick = unix_now_ms(); let elapsed = now_after_tick.saturating_sub(now_ms); diff --git a/crates/blockchain/src/reaggregate.rs b/crates/blockchain/src/reaggregate.rs index 58bfd34d..f47d6c51 100644 --- a/crates/blockchain/src/reaggregate.rs +++ b/crates/blockchain/src/reaggregate.rs @@ -61,7 +61,7 @@ pub fn reaggregate_from_block( // The multi-message aggregate proof was built against the parent state's // validator set. Without it we cannot resolve the pubkey layout the SNARK // was bound to. - let Some(parent_state) = store.get_state(&block.parent_root) else { + let Ok(Some(parent_state)) = store.get_state(&block.parent_root) else { debug!( block_root = %ethlambda_types::ShortRoot(&block.hash_tree_root().0), "Skipping reaggregation: parent state missing" @@ -239,7 +239,10 @@ struct Candidate { /// capped at [`MAX_REAGGREGATIONS_PER_BLOCK`] so an attacker-shaped block /// cannot blow past the slot budget. fn select_candidates(store: &Store, attestations: &[AggregatedAttestation]) -> Vec { - let justified_slot = store.latest_justified().slot; + let justified_slot = store + .latest_justified() + .expect("latest justified checkpoint exists") + .slot; let mut candidates: Vec = Vec::new(); for (idx, att) in attestations.iter().enumerate() { if att.data.target.slot <= justified_slot { @@ -315,7 +318,7 @@ mod tests { // Justified at slot 5; an attestation with target.slot = 5 must be skipped. store .update_checkpoints(ethlambda_storage::ForkCheckpoints::new( - store.head(), + store.head().expect("head exists"), Some(Checkpoint { root: H256::ZERO, slot: 5, diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index 4b12bb3f..487617ef 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -38,11 +38,17 @@ fn accept_new_attestations(store: &mut Store, log_tree: bool) { /// When `log_tree` is true, also computes block weights and logs an ASCII /// fork choice tree to the terminal. pub fn update_head(store: &mut Store, log_tree: bool) { - let blocks = store.get_live_chain(); + let blocks = store + .get_live_chain() + .expect("get_live_chain should succeed"); let attestations = store.extract_latest_known_attestations(); - let old_head = store.head(); + let old_head = store.head().expect("head block exists"); + let latest_justified_root = store + .latest_justified() + .expect("latest justified checkpoint exists") + .root; let (new_head, weights) = ethlambda_fork_choice::compute_lmd_ghost_head( - store.latest_justified().root, + latest_justified_root, &blocks, &attestations, 0, @@ -56,8 +62,14 @@ pub fn update_head(store: &mut Store, log_tree: bool) { // Override the store's latest finalized with the head state's let finalized = store .get_state(&new_head) + .expect("head state exists") .map(|state| state.latest_finalized) - .filter(|finalized| store.get_block_header(&finalized.root).is_some()); + .filter(|finalized| { + store + .get_block_header(&finalized.root) + .expect("block header exists") + .is_some() + }); store .update_checkpoints(ForkCheckpoints::new(new_head, None, finalized)) .expect("update_checkpoints should succeed"); @@ -65,14 +77,22 @@ pub fn update_head(store: &mut Store, log_tree: bool) { if old_head != new_head { let old_slot = store .get_block_header(&old_head) + .expect("block header exists") .map(|h| h.slot) .unwrap_or(0); let new_slot = store .get_block_header(&new_head) + .expect("block header exists") .map(|h| h.slot) .unwrap_or(0); - let justified_slot = store.latest_justified().slot; - let finalized_slot = store.latest_finalized().slot; + let justified_slot = store + .latest_justified() + .expect("latest justified checkpoint exists") + .slot; + let finalized_slot = store + .latest_finalized() + .expect("latest finalized checkpoint exists") + .slot; info!( head_slot = new_slot, head_root = %ShortRoot(&new_head.0), @@ -89,8 +109,12 @@ pub fn update_head(store: &mut Store, log_tree: bool) { &blocks, &weights, new_head, - store.latest_justified(), - store.latest_finalized(), + store + .latest_justified() + .expect("latest justified checkpoint exists"), + store + .latest_finalized() + .expect("latest finalized checkpoint exists"), ); info!("\n{tree}"); } @@ -108,15 +132,22 @@ pub fn update_head(store: &mut Store, log_tree: bool) { /// evidence even when live participation has collapsed: exactly the failure /// mode safe target is supposed to prevent. See leanSpec PR #680. fn update_safe_target(store: &mut Store) { - let head_state = store.get_state(&store.head()).expect("head state exists"); - let num_validators = head_state.validators.len() as u64; + let head_state = store + .get_state(&store.head().unwrap()) + .expect("head state exists"); + let num_validators = head_state.unwrap().validators.len() as u64; let min_target_score = (num_validators * 2).div_ceil(3); - let blocks = store.get_live_chain(); + let blocks = store + .get_live_chain() + .expect("get_live_chain should succeed"); let attestations = store.extract_latest_new_attestations(); let (safe_target, _weights) = ethlambda_fork_choice::compute_lmd_ghost_head( - store.latest_justified().root, + store + .latest_justified() + .expect("latest justified checkpoint exists") + .root, &blocks, &attestations, min_target_score, @@ -146,7 +177,10 @@ fn checkpoint_is_ancestor( // The descendant header is already in hand, so begin the walk at its parent. let mut current_root = descendant_header.parent_root; - while let Some(current_header) = store.get_block_header(¤t_root) { + while let Some(current_header) = store + .get_block_header(¤t_root) + .expect("parent block exists") + { if current_header.slot == ancestor.slot { return current_root == ancestor.root; } @@ -175,13 +209,16 @@ fn validate_attestation_data(store: &Store, data: &AttestationData) -> Result<() // Availability Check - We cannot count a vote if we haven't seen the blocks involved. let source_header = store .get_block_header(&data.source.root) + .expect("source block exists") .ok_or(StoreError::UnknownSourceBlock(data.source.root))?; let target_header = store .get_block_header(&data.target.root) + .expect("target block exists") .ok_or(StoreError::UnknownTargetBlock(data.target.root))?; let head_header = store .get_block_header(&data.head.root) + .expect("head block exists") .ok_or(StoreError::UnknownHeadBlock(data.head.root))?; // Topology Check - Source must be older than Target, and Head must be at least as recent. @@ -242,10 +279,10 @@ fn validate_attestation_data(store: &Store, data: &AttestationData) -> Result<() // The bound is in intervals, not slots: a whole-slot margin would let an // adversary pre-publish next-slot aggregates ahead of any honest validator. let attestation_start_interval = data.slot.saturating_mul(INTERVALS_PER_SLOT); - if attestation_start_interval > store.time() + GOSSIP_DISPARITY_INTERVALS { + if attestation_start_interval > store.time().unwrap() + GOSSIP_DISPARITY_INTERVALS { return Err(StoreError::AttestationTooFarInFuture { attestation_slot: data.slot, - store_time: store.time(), + store_time: store.time().unwrap(), }); } @@ -260,30 +297,30 @@ fn validate_attestation_data(store: &Store, data: &AttestationData) -> Result<() /// interval = store.time() % INTERVALS_PER_SLOT pub fn on_tick(store: &mut Store, timestamp_ms: u64, has_proposal: bool) { // Convert UNIX timestamp (ms) to interval count since genesis - let genesis_time_ms = store.config().genesis_time * 1000; + let genesis_time_ms = store.config().unwrap().genesis_time * 1000; let time_delta_ms = timestamp_ms.saturating_sub(genesis_time_ms); let time = time_delta_ms / MILLISECONDS_PER_INTERVAL; // If we're more than a slot behind, fast-forward to a slot before. // Operations are idempotent, so this should be fine. - if time.saturating_sub(store.time()) > INTERVALS_PER_SLOT { + if time.saturating_sub(store.time().unwrap()) > INTERVALS_PER_SLOT { store .set_time(time - INTERVALS_PER_SLOT) .expect("set_time should succeed"); } - while store.time() < time { + while store.time().unwrap() < time { store - .set_time(store.time() + 1) + .set_time(store.time().unwrap() + 1) .expect("set_time should succeed"); - let slot = store.time() / INTERVALS_PER_SLOT; - let interval = SlotInterval::from_intervals_since_genesis(store.time()); + let slot = store.time().unwrap() / INTERVALS_PER_SLOT; + let interval = SlotInterval::from_intervals_since_genesis(store.time().unwrap()); trace!(%slot, ?interval, "processing tick"); // has_proposal is only signaled for the final tick (matching Python spec behavior) - let is_final_tick = store.time() == time; + let is_final_tick = store.time().unwrap() == time; let should_signal_proposal = has_proposal && is_final_tick; // NOTE: here we assume on_tick never skips intervals. @@ -341,6 +378,7 @@ pub fn on_gossip_attestation( let target = attestation.data.target; let target_state = store .get_state(&target.root) + .expect("target state exists") .ok_or(StoreError::MissingTargetState(target.root))?; if validator_id >= target_state.validators.len() as u64 { return Err(StoreError::InvalidValidatorIndex); @@ -424,6 +462,7 @@ fn on_gossip_aggregated_attestation_core( let target_state = store .get_state(&aggregated.data.target.root) + .expect("target state exists") .ok_or(StoreError::MissingTargetState(aggregated.data.target.root))?; let validators = &target_state.validators; let num_validators = validators.len() as u64; @@ -520,20 +559,23 @@ fn on_block_core( let slot = block.slot; // Skip duplicate blocks (idempotent operation) - if store.has_state(&block_root) { + if store + .has_state(&block_root) + .expect("DB read should succeed") + { return Ok(()); } // Verify parent state is available // Note: Parent block existence is checked by the caller before calling this function. // This check ensures the state has been computed for the parent block. - let parent_state = - store - .get_state(&block.parent_root) - .ok_or(StoreError::MissingParentState { - parent_root: block.parent_root, - slot, - })?; + let parent_state = store + .get_state(&block.parent_root) + .expect("DB read should succeed") + .ok_or(StoreError::MissingParentState { + parent_root: block.parent_root, + slot, + })?; // Each unique AttestationData must appear at most once per block. let attestations = &signed_block.message.body.attestations; @@ -576,12 +618,16 @@ fn on_block_core( // Advance the justified checkpoint when the post-state names a higher one // (leanSpec `advance_to`: monotonic by slot). Finalized is intentionally not // latched here; it is recomputed from the head's chain in `update_head`. - let justified = (post_state.latest_justified.slot > store.latest_justified().slot) + let justified = (post_state.latest_justified.slot > store.latest_justified().unwrap().slot) .then_some(post_state.latest_justified); if let Some(justified) = justified { store - .update_checkpoints(ForkCheckpoints::new(store.head(), Some(justified), None)) + .update_checkpoints(ForkCheckpoints::new( + store.head().unwrap(), + Some(justified), + None, + )) .expect("update_checkpoints should succeed"); } @@ -621,8 +667,8 @@ fn on_block_core( pub fn get_attestation_target(store: &Store) -> Checkpoint { get_attestation_target_with_checkpoints( store, - store.latest_justified(), - store.latest_finalized(), + store.latest_justified().unwrap(), + store.latest_finalized().unwrap(), ) } @@ -642,14 +688,16 @@ pub fn get_attestation_target_with_checkpoints( finalized: Checkpoint, ) -> Checkpoint { // Start from current head - let mut target_block_root = store.head(); + let mut target_block_root = store.head().unwrap(); let mut target_header = store .get_block_header(&target_block_root) - .expect("head block exists"); + .expect("head block exists") + .unwrap(); let safe_target_block_slot = store - .get_block_header(&store.safe_target()) + .get_block_header(&store.safe_target().unwrap()) .expect("safe target exists") + .unwrap() .slot; // Walk back toward safe target (up to `JUSTIFICATION_LOOKBACK_SLOTS` steps) @@ -661,7 +709,8 @@ pub fn get_attestation_target_with_checkpoints( target_block_root = target_header.parent_root; target_header = store .get_block_header(&target_block_root) - .expect("parent block exists"); + .expect("parent block exists") + .unwrap(); } else { break; } @@ -679,7 +728,8 @@ pub fn get_attestation_target_with_checkpoints( target_block_root = target_header.parent_root; target_header = store .get_block_header(&target_block_root) - .expect("parent block exists"); + .expect("parent block exists") + .unwrap(); } // Guard: clamp target to justified (not in the spec). // @@ -732,10 +782,11 @@ pub fn get_attestation_target_with_checkpoints( /// always names a real block (otherwise `validate_attestation_data` rejects it /// with `UnknownSourceBlock`). pub fn produce_attestation_data(store: &Store, slot: u64) -> AttestationData { - let head_root = store.head(); + let head_root = store.head().unwrap(); let mut source = store .get_state(&head_root) .expect("head state exists") + .unwrap() .latest_justified; // Replace the placeholder genesis root with the real (head) one. This only @@ -750,6 +801,7 @@ pub fn produce_attestation_data(store: &Store, slot: u64) -> AttestationData { slot: store .get_block_header(&head_root) .expect("head block exists") + .unwrap() .slot, }; @@ -769,7 +821,8 @@ pub fn produce_attestation_data(store: &Store, slot: u64) -> AttestationData { /// before returning the canonical head. fn get_proposal_head(store: &mut Store, slot: u64) -> H256 { // Calculate time corresponding to this slot - let slot_time_ms = store.config().genesis_time * 1000 + slot * MILLISECONDS_PER_SLOT; + let slot_time_ms = + store.config().expect("config exists").genesis_time * 1000 + slot * MILLISECONDS_PER_SLOT; // Advance time to current slot (ticking intervals) on_tick(store, slot_time_ms, true); @@ -777,7 +830,7 @@ fn get_proposal_head(store: &mut Store, slot: u64) -> H256 { // Process any pending attestations before proposal accept_new_attestations(store, false); - store.head() + store.head().expect("store head exists") } /// Produce a block and per-aggregated-attestation signature payloads for the target slot. @@ -794,6 +847,7 @@ pub fn produce_block_with_signatures( let head_root = get_proposal_head(store, slot); let head_state = store .get_state(&head_root) + .expect("head state exists") .ok_or(StoreError::MissingParentState { parent_root: head_root, slot, @@ -811,7 +865,7 @@ pub fn produce_block_with_signatures( // Get known aggregated payloads: data_root -> (AttestationData, Vec) let aggregated_payloads = store.known_aggregated_payloads(); - let known_block_roots = store.get_block_roots(); + let known_block_roots = store.get_block_roots().unwrap(); let (block, signatures, post_checkpoints) = { let _timing = metrics::time_block_building_payload_aggregation(); @@ -833,7 +887,10 @@ pub fn produce_block_with_signatures( // divergence inherited from a minority fork, but it may not always // converge. We still publish the block in that case (halting block // production freezes the chain, which is worse) and only log the divergence. - let store_justified_slot = store.latest_justified().slot; + let store_justified_slot = store + .latest_justified() + .expect("latest finalized checkpoint exists") + .slot; if post_checkpoints.justified.slot < store_justified_slot { warn!( %slot, @@ -1065,8 +1122,12 @@ fn reorg_depth(old_head: H256, new_head: H256, store: &Store) -> Option { return None; } - let old_head_header = store.get_block_header(&old_head)?; - let new_head_header = store.get_block_header(&new_head)?; + let old_head_header = store + .get_block_header(&old_head) + .expect("old head header exists")?; + let new_head_header = store + .get_block_header(&new_head) + .expect("new head header exists")?; let old_slot = old_head_header.slot; let new_slot = new_head_header.slot; @@ -1082,7 +1143,7 @@ fn reorg_depth(old_head: H256, new_head: H256, store: &Store) -> Option { // Bounded to avoid unbounded walks in pathological cases. const MAX_REORG_DEPTH: u64 = 128; let mut depth: u64 = 0; - while let Some(current_header) = store.get_block_header(¤t_root) { + while let Ok(Some(current_header)) = store.get_block_header(¤t_root) { if current_header.slot <= target_slot { // We've reached the target slot - check if we're at the target block return (current_root != target_root).then_some(depth); @@ -1147,7 +1208,7 @@ mod tests { // which a synthetic genesis block with zero state_root cannot satisfy. let mut store = Store::from_anchor_state(backend, genesis_state); - let head_root = store.head(); + let head_root = store.head().expect("store head exists"); let att_data = AttestationData { slot: 0, head: Checkpoint { @@ -1251,7 +1312,7 @@ mod tests { #[test] fn produce_attestation_data_sources_from_head_state_not_store() { let mut store = new_test_store(); - let genesis = store.head(); + let genesis = store.head().expect("store head exists"); // Head chain: genesis(0) <- a(1) <- b(2), with `b` as head. let a = H256([1u8; 32]); @@ -1267,7 +1328,7 @@ mod tests { // checkpoint set; `insert_state` reads the base from // `latest_block_header.parent_root`, and `get_state(b)` then returns it // from the cache. - let genesis_state = store.get_state(&genesis).expect("genesis state"); + let genesis_state = store.get_state(&genesis).expect("genesis state").unwrap(); let mut head_state = genesis_state.clone(); head_state.slot = genesis_state.slot + 1; head_state.latest_justified = head_justified; @@ -1300,7 +1361,7 @@ mod tests { ); assert_ne!( data.source, - store.latest_justified(), + store.latest_justified().expect("store has justified"), "source must not be the store's off-head global justified" ); } @@ -1311,7 +1372,7 @@ mod tests { #[test] fn validate_attestation_rejects_head_on_sibling_fork() { let mut store = new_test_store(); - let genesis = store.head(); + let genesis = store.head().expect("store head exists"); let base = H256([1u8; 32]); let fork_left = H256([2u8; 32]); @@ -1353,7 +1414,7 @@ mod tests { #[test] fn validate_attestation_rejects_source_on_sibling_fork() { let mut store = new_test_store(); - let genesis = store.head(); + let genesis = store.head().expect("store head exists"); let base = H256([1u8; 32]); let fork_left = H256([2u8; 32]); @@ -1398,7 +1459,7 @@ mod tests { #[test] fn validate_attestation_rejects_slot_before_head() { let mut store = new_test_store(); - let genesis = store.head(); + let genesis = store.head().expect("store head exists"); let b1 = H256([1u8; 32]); let b2 = H256([2u8; 32]); @@ -1442,7 +1503,7 @@ mod tests { #[test] fn validate_attestation_rejects_ceiling_slot_without_overflow() { let mut store = new_test_store(); - let genesis = store.head(); + let genesis = store.head().expect("store head exists"); let b1 = H256([1u8; 32]); let b2 = H256([2u8; 32]); @@ -1475,7 +1536,7 @@ mod tests { #[test] fn validate_attestation_accepts_proper_ancestor_chain() { let mut store = new_test_store(); - let genesis = store.head(); + let genesis = store.head().expect("store head exists"); let b1 = H256([1u8; 32]); let b2 = H256([2u8; 32]); diff --git a/crates/blockchain/tests/forkchoice_spectests.rs b/crates/blockchain/tests/forkchoice_spectests.rs index 0e1f2e9b..3ba670e5 100644 --- a/crates/blockchain/tests/forkchoice_spectests.rs +++ b/crates/blockchain/tests/forkchoice_spectests.rs @@ -238,7 +238,7 @@ fn validate_checks( // Validate time check: fixtures encode the expected store time in intervals // since genesis (matching `Store::time()`). if let Some(expected_time) = checks.time { - let actual_time = st.time(); + let actual_time = st.time().expect("store time is always set"); if actual_time != expected_time { return Err(format!( "Step {}: time mismatch: expected {}, got {}", @@ -284,7 +284,7 @@ fn validate_checks( } // Also validate the root matches a block at this slot - let blocks = st.get_live_chain(); + let blocks = st.get_live_chain().expect("live chain is not empty"); let block_found = blocks .iter() .any(|(root, (slot, _))| *slot == expected_slot && *root == target.root); @@ -305,10 +305,11 @@ fn validate_checks( // Validate headSlot if let Some(expected_slot) = checks.head_slot { - let head_root = st.head(); + let head_root = st.head().expect("head block exists"); let head_header = st .get_block_header(&head_root) - .ok_or_else(|| format!("Step {}: head block not found", step_idx))?; + .expect("block header not found") + .unwrap(); if head_header.slot != expected_slot { return Err(format!( "Step {}: headSlot mismatch: expected {}, got {}", @@ -320,7 +321,7 @@ fn validate_checks( // Validate headRoot (resolved from headRootLabel if headRoot not provided) if let Some(ref expected_root) = resolved_head_root { - let head_root = st.head(); + let head_root = st.head().expect("head block exists"); if head_root != *expected_root { return Err(format!( "Step {}: headRoot mismatch: expected {:?}, got {:?}", @@ -332,7 +333,7 @@ fn validate_checks( // Validate latestJustifiedSlot if let Some(expected_slot) = checks.latest_justified_slot { - let justified = st.latest_justified(); + let justified = st.latest_justified().expect("justified block exists"); if justified.slot != expected_slot { return Err(format!( "Step {}: latestJustifiedSlot mismatch: expected {}, got {}", @@ -344,7 +345,7 @@ fn validate_checks( // Validate latestJustifiedRoot (resolved from label if root not provided) if let Some(ref expected_root) = resolved_justified_root { - let justified = st.latest_justified(); + let justified = st.latest_justified().expect("justified block exists"); if justified.root != *expected_root { return Err(format!( "Step {}: latestJustifiedRoot mismatch: expected {:?}, got {:?}", @@ -356,7 +357,7 @@ fn validate_checks( // Validate latestFinalizedSlot if let Some(expected_slot) = checks.latest_finalized_slot { - let finalized = st.latest_finalized(); + let finalized = st.latest_finalized().expect("finalized block exists"); if finalized.slot != expected_slot { return Err(format!( "Step {}: latestFinalizedSlot mismatch: expected {}, got {}", @@ -368,7 +369,7 @@ fn validate_checks( // Validate latestFinalizedRoot (resolved from label if root not provided) if let Some(ref expected_root) = resolved_finalized_root { - let finalized = st.latest_finalized(); + let finalized = st.latest_finalized().expect("finalized block exists"); if finalized.root != *expected_root { return Err(format!( "Step {}: latestFinalizedRoot mismatch: expected {:?}, got {:?}", @@ -392,7 +393,7 @@ fn validate_checks( // Validate safeTarget root (resolved from label if root not provided) if let Some(ref expected_root) = resolved_safe_target_root { - let actual_root = st.safe_target(); + let actual_root = st.safe_target().expect("safe target exists"); if actual_root != *expected_root { return Err(format!( "Step {}: safeTarget mismatch: expected {:?}, got {:?}", @@ -507,7 +508,7 @@ fn validate_lexicographic_head_among( .into()); } - let blocks = st.get_live_chain(); + let blocks = st.get_live_chain().expect("live chain is not empty"); let known_attestations: HashMap = st.extract_latest_known_attestations(); // Resolve all fork labels to roots and compute their weights @@ -583,7 +584,7 @@ fn validate_lexicographic_head_among( .expect("fork_data is not empty"); // Verify the current head matches the lexicographically highest root - let actual_head_root = st.head(); + let actual_head_root = st.head().expect("head block exists"); if actual_head_root != expected_head_root { let highest_label = fork_data .iter() diff --git a/crates/net/p2p/src/req_resp/handlers.rs b/crates/net/p2p/src/req_resp/handlers.rs index 2a88a9bb..a2c0b51a 100644 --- a/crates/net/p2p/src/req_resp/handlers.rs +++ b/crates/net/p2p/src/req_resp/handlers.rs @@ -208,7 +208,7 @@ async fn handle_blocks_by_root_request( let mut blocks = Vec::new(); for root in request.roots.iter() { - if let Some(signed_block) = server.store.get_signed_block(root) { + if let Ok(Some(signed_block)) = server.store.get_signed_block(root) { blocks.push(signed_block); } // Missing blocks are silently skipped (per spec) @@ -270,10 +270,10 @@ fn canonical_blocks_by_range(store: &Store, start_slot: u64, count: u64) -> Vec< }; let mut roots_by_slot = HashMap::new(); - let mut current_root = store.head(); + let mut current_root = store.head().expect("head block exists"); while !current_root.is_zero() { - let Some(header) = store.get_block_header(¤t_root) else { + let Ok(Some(header)) = store.get_block_header(¤t_root) else { break; }; @@ -291,7 +291,7 @@ fn canonical_blocks_by_range(store: &Store, start_slot: u64, count: u64) -> Vec< (start_slot..=end_slot) .filter_map(|slot| { let root = roots_by_slot.get(&slot)?; - store.get_signed_block(root) + store.get_signed_block(root).ok().flatten() }) .collect() } @@ -393,11 +393,12 @@ async fn handle_blocks_by_range_response( /// Build a Status message from the current Store state. pub fn build_status(store: &Store) -> Status { - let finalized = store.latest_finalized(); - let head_root = store.head(); + let finalized = store.latest_finalized().expect("finalized block exists"); + let head_root = store.head().expect("head block exists"); let head_slot = store .get_block_header(&head_root) .expect("head block exists") + .unwrap() .slot; Status { finalized, @@ -625,7 +626,7 @@ mod tests { let backend = Arc::new(InMemoryBackend::new()); let mut store = Store::from_anchor_state(backend, State::from_genesis(0, vec![])); - let block_1 = signed_block(1, store.head()); + let block_1 = signed_block(1, store.head().expect("head block exists")); let root_1 = block_1.message.hash_tree_root(); store .insert_signed_block(root_1, block_1) diff --git a/crates/net/rpc/src/base.rs b/crates/net/rpc/src/base.rs index 578abd1c..0ccf47ed 100644 --- a/crates/net/rpc/src/base.rs +++ b/crates/net/rpc/src/base.rs @@ -22,10 +22,11 @@ pub(crate) fn routes() -> Router { pub(crate) async fn get_latest_finalized_state( axum::extract::State(store): axum::extract::State, ) -> impl IntoResponse { - let finalized = store.latest_finalized(); + let finalized = store.latest_finalized().expect("finalized block exists"); let mut state = store .get_state(&finalized.root) - .expect("finalized state exists"); + .expect("finalized state exists") + .unwrap(); // Zero state_root to match the canonical post-state representation. // The spec's state_transition sets state_root to zero during process_block_header, @@ -39,19 +40,22 @@ pub(crate) async fn get_latest_finalized_state( pub(crate) async fn get_latest_finalized_block( axum::extract::State(store): axum::extract::State, ) -> impl IntoResponse { - let finalized = store.latest_finalized(); + let finalized = store.latest_finalized().expect("finalized block exists"); // Genesis has no stored signature; `get_signed_block` synthesizes a // placeholder blank proof so this always returns 200. match store.get_signed_block(&finalized.root) { - Some(block) => ssz_response(block.to_ssz()), - None => axum::http::StatusCode::NOT_FOUND.into_response(), + Ok(Some(block)) => ssz_response(block.to_ssz()), + Ok(None) => axum::http::StatusCode::NOT_FOUND.into_response(), + Err(_) => axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response(), } } pub(crate) async fn get_latest_justified_checkpoint( axum::extract::State(store): axum::extract::State, ) -> impl IntoResponse { - let checkpoint = store.latest_justified(); + let checkpoint = store + .latest_justified() + .expect("justified checkpoint exists"); json_response(checkpoint) } diff --git a/crates/net/rpc/src/blocks.rs b/crates/net/rpc/src/blocks.rs index 8110b99b..9082b43d 100644 --- a/crates/net/rpc/src/blocks.rs +++ b/crates/net/rpc/src/blocks.rs @@ -30,8 +30,9 @@ pub(crate) async fn get_block( }; match store.get_block(&root) { - Some(block) => json_response(block), - None => BlockIdError::NotFound.into_response(), + Ok(Some(block)) => json_response(block), + Ok(None) => BlockIdError::NotFound.into_response(), + Err(_) => axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response(), } } @@ -46,8 +47,9 @@ pub(crate) async fn get_block_header( }; match store.get_block_header(&root) { - Some(header) => json_response(header), - None => BlockIdError::NotFound.into_response(), + Ok(Some(header)) => json_response(header), + Ok(None) => BlockIdError::NotFound.into_response(), + Err(_) => axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response(), } } diff --git a/crates/net/rpc/src/fork_choice.rs b/crates/net/rpc/src/fork_choice.rs index 19f368d4..1dd7dfba 100644 --- a/crates/net/rpc/src/fork_choice.rs +++ b/crates/net/rpc/src/fork_choice.rs @@ -36,17 +36,19 @@ pub struct ForkChoiceNode { pub(crate) async fn get_fork_choice( axum::extract::State(store): axum::extract::State, ) -> impl IntoResponse { - let blocks = store.get_live_chain(); + let blocks = store.get_live_chain().expect("live chain is not empty"); let attestations = store.extract_latest_known_attestations(); - let justified = store.latest_justified(); - let finalized = store.latest_finalized(); + let justified = store + .latest_justified() + .expect("justified checkpoint exists"); + let finalized = store.latest_finalized().expect("finalized block exists"); let start_slot = finalized.slot; let weights = ethlambda_fork_choice::compute_block_weights(start_slot, &blocks, &attestations); - let head = store.head(); - let safe_target = store.safe_target(); + let head = store.head().expect("head block exists"); + let safe_target = store.safe_target().expect("safe target exists"); let head_state = store.head_state(); let validator_count = head_state.validators.len() as u64; @@ -56,6 +58,8 @@ pub(crate) async fn get_fork_choice( .map(|(root, &(slot, parent_root))| { let proposer_index = store .get_block_header(root) + .ok() + .flatten() .map(|h| h.proposer_index) .unwrap_or(0); diff --git a/crates/net/rpc/src/genesis.rs b/crates/net/rpc/src/genesis.rs index 45638360..aa7472c9 100644 --- a/crates/net/rpc/src/genesis.rs +++ b/crates/net/rpc/src/genesis.rs @@ -11,7 +11,7 @@ struct GenesisResponse { } async fn get_genesis(State(store): State) -> impl IntoResponse { - let genesis_time = store.config().genesis_time; + let genesis_time = store.config().expect("config exists").genesis_time; // Lean validators are fixed at genesis (no churn), so the current head // state's validator registry always equals the genesis validator count. let validator_count = store.head_state().validators.len() as u64; diff --git a/crates/net/rpc/src/lib.rs b/crates/net/rpc/src/lib.rs index 6eec72a5..76fc9a1b 100644 --- a/crates/net/rpc/src/lib.rs +++ b/crates/net/rpc/src/lib.rs @@ -231,7 +231,9 @@ mod tests { let checkpoint: serde_json::Value = serde_json::from_slice(&body).unwrap(); // The justified checkpoint should match the store's latest justified - let expected = store.latest_justified(); + let expected = store + .latest_justified() + .expect("latest justified checkpoint exists"); assert_eq!( checkpoint, json!({ @@ -250,8 +252,13 @@ mod tests { let store = Store::from_anchor_state(backend, state); // Build expected SSZ with zeroed state_root (canonical post-state form) - let finalized = store.latest_finalized(); - let mut expected_state = store.get_state(&finalized.root).unwrap(); + let finalized = store + .latest_finalized() + .expect("latest finalized checkpoint exists"); + let mut expected_state = store + .get_state(&finalized.root) + .expect("expected state") + .unwrap(); expected_state.latest_block_header.state_root = H256::ZERO; let expected_ssz = expected_state.to_ssz(); @@ -439,7 +446,10 @@ mod tests { let block = Block { slot: 1, proposer_index: 0, - parent_root: store.latest_finalized().root, + parent_root: store + .latest_finalized() + .expect("latest finalized checkpoint exists") + .root, state_root: H256::ZERO, body: BlockBody::default(), }; @@ -505,8 +515,14 @@ mod tests { // matching the genesis header paired with the synthetic blank proof — // same shape `get_signed_block` builds in storage. let genesis_block = store - .get_signed_block(&store.latest_finalized().root) - .expect("genesis served via get_signed_block"); + .get_signed_block( + &store + .latest_finalized() + .expect("latest finalized checkpoint exists") + .root, + ) + .expect("genesis served via get_signed_block") + .unwrap(); let expected = SignedBlock { message: genesis_block.message.clone(), proof: MultiMessageAggregate::default(), diff --git a/crates/net/rpc/src/test_driver.rs b/crates/net/rpc/src/test_driver.rs index ea1bfa65..913370b4 100644 --- a/crates/net/rpc/src/test_driver.rs +++ b/crates/net/rpc/src/test_driver.rs @@ -347,7 +347,7 @@ async fn run_verify_signatures( fn apply_step(store: &mut Store, step: ForkChoiceStep) -> Result<(), String> { match step.step_type.as_str() { "tick" => { - let genesis_time = store.config().genesis_time; + let genesis_time = store.config().expect("config exists").genesis_time; let timestamp_ms = match (step.time, step.interval) { (Some(time_s), _) => time_s * 1000, (None, Some(interval)) => { @@ -367,7 +367,7 @@ fn apply_step(store: &mut Store, step: ForkChoiceStep) -> Result<(), String> { // before importing, unless the step delivers the block ahead of // the store clock. if step.tick_to_slot { - let block_time_ms = store.config().genesis_time * 1000 + let block_time_ms = store.config().expect("config exists").genesis_time * 1000 + signed_block.message.slot * MILLISECONDS_PER_SLOT; store::on_tick(store, block_time_ms, true); } @@ -462,11 +462,15 @@ fn post_summary(state: &State) -> StateTransitionPost { fn snapshot_store(store: &Store) -> DriverSnapshot { DriverSnapshot { head_slot: store.head_slot(), - head_root: store.head(), - time: store.time(), - justified_checkpoint: store.latest_justified(), - finalized_checkpoint: store.latest_finalized(), - safe_target: store.safe_target(), + head_root: store.head().expect("head exists"), + time: store.time().expect("store time exists"), + justified_checkpoint: store + .latest_justified() + .expect("latest justified checkpoint exists"), + finalized_checkpoint: store + .latest_finalized() + .expect("latest finalized checkpoint exists"), + safe_target: store.safe_target().expect("safe target exists"), } } @@ -490,8 +494,8 @@ mod tests { // Head, time, checkpoints all read without panicking; that's the // contract `init_fork_choice` relies on before the first reset. let _ = store.head(); - assert_eq!(store.time(), 0); - assert_eq!(store.latest_justified().slot, 0); - assert_eq!(store.latest_finalized().slot, 0); + assert_eq!(store.time().unwrap(), 0); + assert_eq!(store.latest_justified().unwrap().slot, 0); + assert_eq!(store.latest_finalized().unwrap().slot, 0); } } diff --git a/crates/net/rpc/tests/test_driver_e2e.rs b/crates/net/rpc/tests/test_driver_e2e.rs index 5591b47b..21132856 100644 --- a/crates/net/rpc/tests/test_driver_e2e.rs +++ b/crates/net/rpc/tests/test_driver_e2e.rs @@ -110,7 +110,7 @@ async fn init_with_genesis_anchor_returns_204_and_resets_store() { // The driver's store should now reflect the supplied genesis time. let guard = driver.read().await; - assert_eq!(guard.config().genesis_time, 1234); + assert_eq!(guard.config().expect("config exists").genesis_time, 1234); } #[tokio::test] diff --git a/crates/storage/src/error.rs b/crates/storage/src/error.rs index 8333a2cf..0e0c2d73 100644 --- a/crates/storage/src/error.rs +++ b/crates/storage/src/error.rs @@ -1,5 +1,14 @@ +use ethlambda_types::primitives::H256; #[derive(Debug, thiserror::Error)] pub enum Error { #[error("storage error: {0}")] Storage(#[from] crate::api::Error), + #[error("missing block header for root {0:?}")] + MissingBlockHeader(H256), + #[error("missing block body for root {0:?}")] + MissingBlockBody(H256), + #[error("missing state for root {0:?}")] + MissingState(H256), + #[error("missing metadata for key {0:?}")] + MissingMetadata(Vec), } diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index c916bfca..4b7de834 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -596,13 +596,17 @@ impl Store { pub fn from_db_state( backend: Arc, expected_genesis_time: u64, - ) -> Option { + ) -> Result, Error> { let persisted_config = { let view = backend.begin_read().expect("read view"); - let bytes = view.get(Table::Metadata, KEY_CONFIG).expect("get config")?; + let bytes = view + .get(Table::Metadata, KEY_CONFIG) + .expect("get config") + .ok_or(Error::MissingMetadata(KEY_CONFIG.to_vec()))?; // probe KEY_LATEST_FINALIZED view.get(Table::Metadata, KEY_LATEST_FINALIZED) - .expect("get latest finalized")?; + .expect("get latest finalized") + .ok_or(Error::MissingMetadata(KEY_LATEST_FINALIZED.to_vec()))?; ChainConfig::from_ssz_bytes(&bytes).expect("valid config") }; if persisted_config.genesis_time != expected_genesis_time { @@ -611,10 +615,10 @@ impl Store { expected_genesis_time, "Persisted DB has a different genesis_time; treating as empty" ); - return None; + return Ok(None); } info!("Loaded store from persisted DB state"); - Some(Self { + Ok(Some(Self { backend, new_payloads: Arc::new(Mutex::new(PayloadBuffer::new(NEW_PAYLOAD_CAP))), known_payloads: Arc::new(Mutex::new(PayloadBuffer::new(AGGREGATED_PAYLOAD_CAP))), @@ -622,7 +626,7 @@ impl Store { GOSSIP_SIGNATURE_CAP, ))), state_cache: new_state_cache(), - }) + })) } /// Internal helper to initialize the store with anchor data. @@ -727,13 +731,13 @@ impl Store { // ============ Metadata Helpers ============ - fn get_metadata(&self, key: &[u8]) -> T { + fn get_metadata(&self, key: &[u8]) -> Result { let view = self.backend.begin_read().expect("read view"); let bytes = view .get(Table::Metadata, key) .expect("get") .expect("metadata key exists"); - T::from_ssz_bytes(&bytes).expect("valid encoding") + Ok(T::from_ssz_bytes(&bytes).expect("valid encoding")) } fn set_metadata(&self, key: &[u8], value: &T) -> Result<(), Error> { @@ -752,7 +756,7 @@ impl Store { /// Each increment represents one 800ms interval. Derive slot/interval as: /// slot = time() / INTERVALS_PER_SLOT /// interval = time() % INTERVALS_PER_SLOT - pub fn time(&self) -> u64 { + pub fn time(&self) -> Result { self.get_metadata(KEY_TIME) } @@ -764,21 +768,21 @@ impl Store { // ============ Config ============ /// Returns the chain configuration. - pub fn config(&self) -> ChainConfig { + pub fn config(&self) -> Result { self.get_metadata(KEY_CONFIG) } // ============ Head ============ /// Returns the current head block root. - pub fn head(&self) -> H256 { + pub fn head(&self) -> Result { self.get_metadata(KEY_HEAD) } // ============ Safe Target ============ /// Returns the safe target block root for attestations. - pub fn safe_target(&self) -> H256 { + pub fn safe_target(&self) -> Result { self.get_metadata(KEY_SAFE_TARGET) } @@ -790,12 +794,12 @@ impl Store { // ============ Checkpoints ============ /// Returns the latest justified checkpoint. - pub fn latest_justified(&self) -> Checkpoint { + pub fn latest_justified(&self) -> Result { self.get_metadata(KEY_LATEST_JUSTIFIED) } /// Returns the latest finalized checkpoint. - pub fn latest_finalized(&self) -> Checkpoint { + pub fn latest_finalized(&self) -> Result { self.get_metadata(KEY_LATEST_FINALIZED) } @@ -810,7 +814,10 @@ impl Store { /// When finalization advances, prunes the LiveChain index. pub fn update_checkpoints(&mut self, checkpoints: ForkCheckpoints) -> Result<(), Error> { // Read old finalized slot before updating metadata - let old_finalized_slot = self.latest_finalized().slot; + let old_finalized_slot = self + .latest_finalized() + .expect("Failed to get latest finalized checkpoint") + .slot; let mut entries = vec![(KEY_HEAD.to_vec(), checkpoints.head.to_ssz())]; @@ -858,17 +865,23 @@ impl Store { /// /// This is separated from `update_checkpoints` so callers can defer heavy /// pruning until after a batch of blocks has been fully processed. - pub fn prune_old_data(&mut self) { - let finalized_slot = self.latest_finalized().slot; + pub fn prune_old_data(&mut self) -> Result<(), Error> { + let finalized_slot = self + .latest_finalized() + .expect("Failed to get latest finalized checkpoint") + .slot; let tip_slot = self - .get_block_header(&self.head()) - .map_or(finalized_slot, |header| header.slot); + .get_block_header(&self.head().expect("Failed to get head block root")) + .map_or(finalized_slot, |header| { + header.expect("Failed to get block header").slot + }); let pruned_signatures = self .prune_old_block_signatures(finalized_slot, tip_slot) .expect("prune old block signatures"); if pruned_signatures > 0 { info!(pruned_signatures, "Pruned old finalized block signatures"); } + Ok(()) } // ============ Blocks ============ @@ -877,9 +890,10 @@ impl Store { /// /// Iterates only the LiveChain table, avoiding Block deserialization. /// Returns only non-finalized blocks, automatically pruned on finalization. - pub fn get_live_chain(&self) -> HashMap { + pub fn get_live_chain(&self) -> Result, Error> { let view = self.backend.begin_read().expect("read view"); - view.prefix_iterator(Table::LiveChain, &[]) + Ok(view + .prefix_iterator(Table::LiveChain, &[]) .expect("iterator") .filter_map(|res| res.ok()) .map(|(k, v)| { @@ -887,32 +901,34 @@ impl Store { let parent_root = H256::from_ssz_bytes(&v).expect("valid parent_root"); (root, (slot, parent_root)) }) - .collect() + .collect()) } /// Return the highest slot in the live chain. - pub fn max_live_chain_slot(&self) -> Option { + pub fn max_live_chain_slot(&self) -> Result, Error> { let view = self.backend.begin_read().expect("read view"); - view.prefix_iterator(Table::LiveChain, &[]) + Ok(view + .prefix_iterator(Table::LiveChain, &[]) .expect("iterator") .filter_map(Result::ok) .map(|(key, _)| decode_slot_root_key(&key).0) - .max() + .max()) } /// Get all known block roots as HashSet. /// /// Useful for checking block existence without deserializing. - pub fn get_block_roots(&self) -> HashSet { + pub fn get_block_roots(&self) -> Result, Error> { let view = self.backend.begin_read().expect("read view"); - view.prefix_iterator(Table::LiveChain, &[]) + Ok(view + .prefix_iterator(Table::LiveChain, &[]) .expect("iterator") .filter_map(|res| res.ok()) .map(|(k, _)| { let (_, root) = decode_slot_root_key(&k); root }) - .collect() + .collect()) } /// Prune slot index entries with slot < finalized_slot. @@ -1024,11 +1040,12 @@ impl Store { } /// Get the block header by root. - pub fn get_block_header(&self, root: &H256) -> Option { + pub fn get_block_header(&self, root: &H256) -> Result, Error> { let view = self.backend.begin_read().expect("read view"); - view.get(Table::BlockHeaders, &root.to_ssz()) + Ok(view + .get(Table::BlockHeaders, &root.to_ssz()) .expect("get") - .map(|bytes| BlockHeader::from_ssz_bytes(&bytes).expect("valid header")) + .map(|bytes| BlockHeader::from_ssz_bytes(&bytes).expect("valid header"))) } // ============ Signed Blocks ============ @@ -1084,21 +1101,27 @@ impl Store { /// /// Unlike [`get_signed_block`](Self::get_signed_block), this works for the /// genesis block, which has no signature entry. - pub fn get_block(&self, root: &H256) -> Option { + pub fn get_block(&self, root: &H256) -> Result, Error> { let view = self.backend.begin_read().expect("read view"); let key = root.to_ssz(); - let header_bytes = view.get(Table::BlockHeaders, &key).expect("get")?; + let header_bytes = view + .get(Table::BlockHeaders, &key) + .expect("get") + .ok_or(Error::MissingBlockHeader(*root))?; let header = BlockHeader::from_ssz_bytes(&header_bytes).expect("valid header"); let body = if header.body_root == *EMPTY_BODY_ROOT { BlockBody::default() } else { - let body_bytes = view.get(Table::BlockBodies, &key).expect("get")?; + let body_bytes = view + .get(Table::BlockBodies, &key) + .expect("get") + .ok_or(Error::MissingBlockBody(*root))?; BlockBody::from_ssz_bytes(&body_bytes).expect("valid body") }; - Some(Block::from_header_and_body(header, body)) + Ok(Some(Block::from_header_and_body(header, body))) } /// Get a signed block by combining header, body, and the merged proof. @@ -1114,18 +1137,24 @@ impl Store { /// synthesize an empty proof for the slot-0 anchor only; for any other slot /// a missing signature surfaces as `None` (a pruned finalized block can no /// longer be served with its proof) rather than as a fabricated block. - pub fn get_signed_block(&self, root: &H256) -> Option { + pub fn get_signed_block(&self, root: &H256) -> Result, Error> { let view = self.backend.begin_read().expect("read view"); let key = root.to_ssz(); - let header_bytes = view.get(Table::BlockHeaders, &key).expect("get")?; + let header_bytes = view + .get(Table::BlockHeaders, &key) + .expect("get") + .ok_or(Error::MissingBlockHeader(*root))?; let header = BlockHeader::from_ssz_bytes(&header_bytes).expect("valid header"); // Use empty body if header indicates empty, otherwise fetch from DB let body = if header.body_root == *EMPTY_BODY_ROOT { BlockBody::default() } else { - let body_bytes = view.get(Table::BlockBodies, &key).expect("get")?; + let body_bytes = view + .get(Table::BlockBodies, &key) + .expect("get") + .ok_or(Error::MissingBlockBody(*root))?; BlockBody::from_ssz_bytes(&body_bytes).expect("valid body") }; @@ -1138,15 +1167,15 @@ impl Store { // other slot a missing proof (pruned finalized block, or genuine // corruption) surfaces as `None` rather than a fabricated block. None if header.slot == 0 => MultiMessageAggregate::default(), - None => return None, + None => return Ok(None), }; let block = Block::from_header_and_body(header, body); - Some(SignedBlock { + Ok(Some(SignedBlock { message: block, proof, - }) + })) } // ============ States ============ @@ -1157,10 +1186,10 @@ impl Store { /// reconstructed by walking parent-linked `StateDiffs` back to the nearest /// ancestor snapshot and replaying forward. Returns `None` if the diff chain /// is broken or the target block header is unavailable. - pub fn get_state(&self, root: &H256) -> Option { + pub fn get_state(&self, root: &H256) -> Result, Error> { // Memoized hot states first (states are immutable per root). if let Some(state) = self.state_cache.lock().unwrap().get(root) { - return Some(state.clone()); + return Ok(Some(state.clone())); } // Anchor snapshot in `States`, otherwise reconstruct from the diff chain. let snapshot = { @@ -1169,9 +1198,14 @@ impl Store { .expect("get") .map(|bytes| State::from_ssz_bytes(&bytes).expect("valid state")) }; - let state = snapshot.or_else(|| self.reconstruct_state(root))?; + let state = if let Some(s) = snapshot { + s + } else { + self.reconstruct_state(root)? + .ok_or(Error::MissingState(*root))? + }; self.state_cache.lock().unwrap().put(*root, state.clone()); - Some(state) + Ok(Some(state)) } /// Reconstruct a state from diffs and the nearest ancestor snapshot. @@ -1179,7 +1213,7 @@ impl Store { /// Walks `base_root` pointers back until a snapshot is found, fetches the /// target's block header, and delegates the assembly to /// [`state_diff::reconstruct`](crate::state_diff::reconstruct). - fn reconstruct_state(&self, root: &H256) -> Option { + fn reconstruct_state(&self, root: &H256) -> Result, Error> { // Walk back collecting diffs until we reach a snapshot. let view = self.backend.begin_read().expect("read view"); let mut diffs: Vec = Vec::new(); @@ -1190,7 +1224,8 @@ impl Store { } let diff_bytes = view .get(Table::StateDiffs, &cursor.to_ssz()) - .expect("get")?; + .expect("get") + .ok_or(Error::MissingState(*root))?; let diff = StateDiff::from_ssz_bytes(&diff_bytes).expect("valid state diff"); cursor = diff.base_root; diffs.push(diff); @@ -1202,23 +1237,26 @@ impl Store { // The latest block header lives in BlockHeaders; the stored state caches // the real state_root there, so it equals the header byte-for-byte. - let latest_block_header = self.get_block_header(root)?; + let latest_block_header = self + .get_block_header(root)? + .ok_or(Error::MissingBlockHeader(*root))?; - Some(crate::state_diff::reconstruct( + Ok(Some(crate::state_diff::reconstruct( snapshot, &diffs, latest_block_header, - )) + ))) } /// Returns whether a state is available for the given block root. /// /// True if a snapshot exists or the state can be reconstructed from a diff. - pub fn has_state(&self, root: &H256) -> bool { + pub fn has_state(&self, root: &H256) -> Result { let view = self.backend.begin_read().expect("read view"); let key = root.to_ssz(); - view.get(Table::States, &key).expect("get").is_some() - || view.get(Table::StateDiffs, &key).expect("get").is_some() + let states = view.get(Table::States, &key).expect("get"); + let diffs = view.get(Table::StateDiffs, &key).expect("get"); + Ok(states.is_some() || diffs.is_some()) } /// Persist a post-block state as a parent-linked diff, snapshotting at anchors. @@ -1247,7 +1285,8 @@ impl Store { let parent_root = state.latest_block_header.parent_root; let parent_state = self .get_state(&parent_root) - .expect("parent state must exist to diff against"); + .expect("parent state must exist to diff against") + .unwrap(); let is_anchor = state.slot / SNAPSHOT_ANCHOR_INTERVAL > parent_state.slot / SNAPSHOT_ANCHOR_INTERVAL; @@ -1492,22 +1531,25 @@ impl Store { /// Returns the slot of the current head block. pub fn head_slot(&self) -> u64 { - self.get_block_header(&self.head()) + self.get_block_header(&self.head().expect("head block exists")) .expect("head block exists") + .unwrap() .slot } /// Returns the slot of the current safe target block. pub fn safe_target_slot(&self) -> u64 { - self.get_block_header(&self.safe_target()) + self.get_block_header(&self.safe_target().expect("safe target exists")) .expect("safe target exists") + .unwrap() .slot } /// Returns a clone of the head state. pub fn head_state(&self) -> State { - self.get_state(&self.head()) + self.get_state(&self.head().expect("head block exists")) .expect("head state is always available") + .unwrap() } } @@ -1775,12 +1817,22 @@ mod tests { assert!(!has_key(backend.as_ref(), Table::States, &r1)); // Hot path: the just-imported state is memoized in the cache. - assert_eq!(store.get_state(&r1).unwrap().to_ssz(), s1.to_ssz()); + assert_eq!( + store + .get_state(&r1) + .expect("get state") + .expect("state exists") + .to_ssz(), + s1.to_ssz() + ); // A cold store (empty cache, shared backend) reconstructs from the diff, // byte-identically. let cold = Store::test_store_with_backend(backend.clone()); - let reconstructed = cold.get_state(&r1).expect("reconstructs from diff"); + let reconstructed = cold + .get_state(&r1) + .expect("reconstructs from diff") + .expect("state exists"); assert_eq!(reconstructed.to_ssz(), s1.to_ssz()); } @@ -1811,7 +1863,10 @@ mod tests { assert!(!has_key(backend.as_ref(), Table::States, &r1)); assert!(!has_key(backend.as_ref(), Table::States, &r2)); let cold = Store::test_store_with_backend(backend.clone()); - let reconstructed = cold.get_state(&r2).expect("reconstructs across diffs"); + let reconstructed = cold + .get_state(&r2) + .expect("reconstructs across diffs") + .expect("state exists"); assert_eq!(reconstructed.to_ssz(), s2.to_ssz()); } @@ -2592,9 +2647,10 @@ mod tests { let backend: Arc = Arc::new(InMemoryBackend::new()); let store = Store::from_anchor_state(backend, State::from_genesis(0, vec![])); - let head_root = store.head(); + let head_root = store.head().expect("head root must exist"); let signed = store .get_signed_block(&head_root) + .expect("genesis block must be retrievable with synthetic proof") .expect("genesis block must be retrievable with synthetic proof"); assert_eq!(signed.message.slot, 0); @@ -2627,7 +2683,12 @@ mod tests { batch.commit().expect("commit"); let store = Store::from_anchor_state(backend, State::from_genesis(0, vec![])); - assert!(store.get_signed_block(&root).is_none()); + assert!( + store + .get_signed_block(&root) + .expect("Failed to get signed block") + .is_none() + ); } /// The bootstrap anchor is stored as a full snapshot in `States`, the base of @@ -2637,7 +2698,7 @@ mod tests { let backend: Arc = Arc::new(InMemoryBackend::new()); let store = Store::from_anchor_state(backend.clone(), State::from_genesis(0, vec![])); - let anchor_root = store.head(); + let anchor_root = store.head().expect("Failed to get head block root"); assert!(has_key(backend.as_ref(), Table::States, &anchor_root)); } @@ -2646,7 +2707,11 @@ mod tests { #[test] fn from_db_state_returns_none_on_empty_backend() { let backend: Arc = Arc::new(InMemoryBackend::new()); - assert!(Store::from_db_state(backend, 12345).is_none()); + assert!( + Store::from_db_state(backend, 12345) + .expect("Failed to get store") + .is_none() + ); } #[test] @@ -2654,7 +2719,11 @@ mod tests { let backend: Arc = Arc::new(InMemoryBackend::new()); // Write an initial state to the backend. let _ = Store::from_anchor_state(backend.clone(), State::from_genesis(12345, vec![])); - assert!(Store::from_db_state(backend, 12345).is_some()); + assert!( + Store::from_db_state(backend, 12345) + .expect("Failed to get store") + .is_some() + ); } #[test] @@ -2662,7 +2731,11 @@ mod tests { let backend: Arc = Arc::new(InMemoryBackend::new()); // Write an initial state to the backend. let _ = Store::from_anchor_state(backend.clone(), State::from_genesis(12345, vec![])); - assert!(Store::from_db_state(backend, 99999).is_none()); + assert!( + Store::from_db_state(backend, 99999) + .expect("Failed to get store") + .is_none() + ); } #[test] @@ -2680,6 +2753,10 @@ mod tests { ) .expect("put config"); batch.commit().expect("commit"); - assert!(Store::from_db_state(backend, 12345).is_none()); + assert!( + Store::from_db_state(backend, 12345) + .expect("Failed to get store") + .is_none() + ); } }