From 2e58caf2d604e9873c8840941a69b74f4b7e616a Mon Sep 17 00:00:00 2001 From: Yaroslav Vorobiov Date: Tue, 29 Sep 2026 22:12:59 +0200 Subject: [PATCH 1/6] fix(storage): propagate backend storage errors in direct store methods Return backend read and write failures from the nine Result-returning Store methods instead of panicking. Propagate LiveChain iterator errors rather than returning an incomplete view of the chain. --- crates/storage/src/store.rs | 109 ++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 54 deletions(-) diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index 56ea970e..90b682ca 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -1040,11 +1040,13 @@ 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) -> Result, Error> { - let view = self.backend.begin_read().expect("read view"); - Ok(view - .prefix_iterator(Table::LiveChain, &[]) - .expect("iterator") - .filter_map(|res| res.ok()) + let view = self.backend.begin_read()?; + let entries: Vec<_> = view + .prefix_iterator(Table::LiveChain, &[])? + .collect::>()?; + + Ok(entries + .into_iter() .map(|(k, v)| { let (slot, root) = decode_slot_root_key(&k); let parent_root = H256::from_ssz_bytes(&v).expect("valid parent_root"); @@ -1055,11 +1057,12 @@ impl Store { /// Return the highest slot in the live chain. pub fn max_live_chain_slot(&self) -> Result, Error> { - let view = self.backend.begin_read().expect("read view"); - Ok(view - .prefix_iterator(Table::LiveChain, &[]) - .expect("iterator") - .filter_map(Result::ok) + let view = self.backend.begin_read()?; + let entries: Vec<_> = view + .prefix_iterator(Table::LiveChain, &[])? + .collect::>()?; + Ok(entries + .into_iter() .map(|(key, _)| decode_slot_root_key(&key).0) .max()) } @@ -1068,11 +1071,12 @@ impl Store { /// /// Useful for checking block existence without deserializing. pub fn get_block_roots(&self) -> Result, Error> { - let view = self.backend.begin_read().expect("read view"); - Ok(view - .prefix_iterator(Table::LiveChain, &[]) - .expect("iterator") - .filter_map(|res| res.ok()) + let view = self.backend.begin_read()?; + let entries: Vec<_> = view + .prefix_iterator(Table::LiveChain, &[])? + .collect::>()?; + Ok(entries + .into_iter() .map(|(k, _)| { let (_, root) = decode_slot_root_key(&k); root @@ -1087,32 +1091,33 @@ impl Store { /// /// Returns the number of entries pruned. pub fn prune_live_chain(&mut self, finalized_slot: u64) -> Result { - let view = self.backend.begin_read().expect("read view"); - // Collect keys to delete - stop once we hit finalized_slot // Keys are sorted by slot (big-endian encoding) so we can stop early - let keys_to_delete: Vec<_> = view - .prefix_iterator(Table::LiveChain, &[]) - .expect("iterator") - .filter_map(|res| res.ok()) - .take_while(|(k, _)| { - let (slot, _) = decode_slot_root_key(k); - slot < finalized_slot - }) - .map(|(k, _)| k.to_vec()) - .collect(); - drop(view); + let keys_to_delete: Vec<_> = { + let view = self.backend.begin_read()?; + + let entries: Vec<_> = view + .prefix_iterator(Table::LiveChain, &[])? + .take_while(|res| match res { + Ok((k, _)) => { + let (slot, _) = decode_slot_root_key(k); + slot < finalized_slot + } + _ => true, + }) + .collect::>()?; + + entries.into_iter().map(|(k, _)| k.to_vec()).collect() + }; let count = keys_to_delete.len(); if count == 0 { return Ok(0); } - let mut batch = self.backend.begin_write().expect("write batch"); - batch - .delete_batch(Table::LiveChain, keys_to_delete) - .expect("delete non-finalized chain entries"); - batch.commit().expect("commit"); + let mut batch = self.backend.begin_write()?; + batch.delete_batch(Table::LiveChain, keys_to_delete)?; + batch.commit()?; Ok(count) } @@ -1171,25 +1176,22 @@ impl Store { // before it, and keys at the cutoff sort after it (they extend it with // a root). A single range delete drops them all without reading the // table (and without walking the tombstones left by earlier prunes). - let mut batch = self.backend.begin_write().expect("write batch"); - batch - .delete_range( - Table::BlockProof, - &0u64.to_be_bytes(), - &cutoff.to_be_bytes(), - ) - .expect("delete finalized block proofs"); - batch.commit().expect("commit"); + let mut batch = self.backend.begin_write()?; + batch.delete_range( + Table::BlockProof, + &0u64.to_be_bytes(), + &cutoff.to_be_bytes(), + )?; + batch.commit()?; Ok(cutoff) } /// Get the block header by root. pub fn get_block_header(&self, root: &H256) -> Result, Error> { - let view = self.backend.begin_read().expect("read view"); + let view = self.backend.begin_read()?; Ok(view - .get(Table::BlockHeaders, &root.to_ssz()) - .expect("get") + .get(Table::BlockHeaders, &root.to_ssz())? .map(|bytes| BlockHeader::from_ssz_bytes(&bytes).expect("valid header"))) } @@ -1248,10 +1250,10 @@ 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) -> Result, Error> { - let view = self.backend.begin_read().expect("read view"); + let view = self.backend.begin_read()?; let key = root.to_ssz(); - let Some(header_bytes) = view.get(Table::BlockHeaders, &key).expect("get") else { + let Some(header_bytes) = view.get(Table::BlockHeaders, &key)? else { return Ok(None); }; let header = BlockHeader::from_ssz_bytes(&header_bytes).expect("valid header"); @@ -1259,7 +1261,7 @@ impl Store { let body = if header.body_root == *EMPTY_BODY_ROOT { BlockBody::default() } else { - let Some(body_bytes) = view.get(Table::BlockBodies, &key).expect("get") else { + let Some(body_bytes) = view.get(Table::BlockBodies, &key)? else { return Ok(None); }; BlockBody::from_ssz_bytes(&body_bytes).expect("valid body") @@ -1333,10 +1335,9 @@ impl Store { /// bootstrapped from. Callers that use this to *reject* something must treat /// `None` as "unknown" rather than "not canonical". pub fn canonical_root_at_slot(&self, slot: u64) -> Result, Error> { - let view = self.backend.begin_read().expect("read view"); + let view = self.backend.begin_read()?; Ok(view - .get(Table::BlockRoots, &encode_block_root_key(slot)) - .expect("get block root") + .get(Table::BlockRoots, &encode_block_root_key(slot))? .map(|bytes| H256::from_ssz_bytes(&bytes).expect("valid block root"))) } @@ -1449,10 +1450,10 @@ impl Store { /// /// True if a snapshot exists or the state can be reconstructed from a diff. pub fn has_state(&self, root: &H256) -> Result { - let view = self.backend.begin_read().expect("read view"); + let view = self.backend.begin_read()?; let key = root.to_ssz(); - let states = view.get(Table::States, &key).expect("get"); - let diffs = view.get(Table::StateDiffs, &key).expect("get"); + let states = view.get(Table::States, &key)?; + let diffs = view.get(Table::StateDiffs, &key)?; Ok(states.is_some() || diffs.is_some()) } From 1f2ba6738464f9a0fe20f4aaa586f852bcabe1f6 Mon Sep 17 00:00:00 2001 From: Yaroslav Vorobiov Date: Thu, 1 Oct 2026 16:00:58 +0200 Subject: [PATCH 2/6] fix(storage): propagate metadata read and write errors; add missing metadata error Propagate backend failures through the shared metadata helpers and return MissingMetadata when a required key is absent. --- crates/storage/src/error.rs | 2 ++ crates/storage/src/store.rs | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/crates/storage/src/error.rs b/crates/storage/src/error.rs index 7837cc14..313d1681 100644 --- a/crates/storage/src/error.rs +++ b/crates/storage/src/error.rs @@ -4,6 +4,8 @@ use ethlambda_types::{genesis::GenesisMismatch, primitives::H256}; pub enum Error { #[error("storage error: {0}")] Storage(#[from] crate::api::Error), + #[error("missing metadata key: {0}")] + MissingMetadata(String), #[error("unexpected missing block header for root {0}")] UnexpectedMissingBlockHeader(H256), #[error("unexpected missing state for root {0}")] diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index 8a3c3d1c..80819302 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -854,20 +854,21 @@ impl Store { // ============ Metadata Helpers ============ 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"); + let view = self.backend.begin_read()?; + let bytes = view.get(Table::Metadata, key)?.ok_or_else(|| { + let key_str = std::str::from_utf8(key) + .expect("keys are valid UTF-8 string") + .to_string(); + + Error::MissingMetadata(key_str) + })?; Ok(T::from_ssz_bytes(&bytes).expect("valid encoding")) } fn set_metadata(&self, key: &[u8], value: &T) -> Result<(), Error> { - let mut batch = self.backend.begin_write().expect("write batch"); - batch - .put_batch(Table::Metadata, vec![(key.to_vec(), value.to_ssz())]) - .expect("put metadata"); - batch.commit().expect("commit"); + let mut batch = self.backend.begin_write()?; + batch.put_batch(Table::Metadata, vec![(key.to_vec(), value.to_ssz())])?; + batch.commit()?; Ok(()) } From a110289e0a8c164a5e22bfada22cc43de04024dc Mon Sep 17 00:00:00 2001 From: Yaroslav Vorobiov Date: Thu, 1 Oct 2026 16:29:29 +0200 Subject: [PATCH 3/6] fix(storage): return checkpoint and pruning errors Propagate backend write and live-chain pruning failures from update_checkpoints instead of panicking. The checkpoint is still committed before live-chain pruning, so a pruning failure now returns an error after that commit. Propagate read and proof-pruning failures from prune_old_data instead of panicking or using the finalized slot as a fallback. Return UnexpectedMissingBlockHeader when the stored head has no header. --- crates/storage/src/store.rs | 36 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index 80819302..0d8371a9 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -960,15 +960,11 @@ impl Store { entries.push((KEY_LATEST_FINALIZED.to_vec(), finalized.to_ssz())); } - let mut batch = self.backend.begin_write().expect("write batch"); - batch.put_batch(Table::Metadata, entries).expect("put"); - batch - .delete_batch(Table::BlockRoots, block_root_deletes) - .expect("delete old canonical block roots"); - batch - .put_batch(Table::BlockRoots, block_root_entries) - .expect("put canonical block roots"); - batch.commit().expect("commit"); + let mut batch = self.backend.begin_write()?; + batch.put_batch(Table::Metadata, entries)?; + batch.delete_batch(Table::BlockRoots, block_root_deletes)?; + batch.put_batch(Table::BlockRoots, block_root_entries)?; + batch.commit()?; // Lightweight pruning that should happen immediately on finalization advance: // live chain index, signatures, and attestation data. These are cheap and @@ -977,9 +973,7 @@ impl Store { if let Some(finalized) = checkpoints.finalized && finalized.slot > old_finalized_slot { - let pruned_chain = self - .prune_live_chain(finalized.slot) - .expect("prune live chain"); + let pruned_chain = self.prune_live_chain(finalized.slot)?; let pruned_sigs = self.prune_gossip_signatures(finalized.slot); let pruned_payloads = self.prune_stale_aggregated_payloads(finalized.slot); @@ -1003,18 +997,14 @@ 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) -> Result<(), Error> { - let finalized_slot = self - .latest_finalized() - .expect("Failed to get latest finalized checkpoint") - .slot; + let finalized_slot = self.latest_finalized()?.slot; + let head = self.head()?; let tip_slot = self - .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_below_slot = self - .prune_old_block_proofs(finalized_slot, tip_slot) - .expect("prune old block proofs"); + .get_block_header(&head)? + .ok_or(Error::UnexpectedMissingBlockHeader(head))? + .slot; + + let pruned_below_slot = self.prune_old_block_proofs(finalized_slot, tip_slot)?; if pruned_below_slot > 0 { info!(pruned_below_slot, "Pruned old finalized block proofs"); } From a67f90fcd3379284cc5d0ce67e222a598d45c10f Mon Sep 17 00:00:00 2001 From: Yaroslav Vorobiov Date: Thu, 1 Oct 2026 16:50:10 +0200 Subject: [PATCH 4/6] fix(storage): propagate signed block storage errors Return backend failures from pending and signed block writes, single-block reads, and range reads instead of panicking. Preserve existing behavior for missing blocks and pruned proofs. --- crates/storage/src/store.rs | 66 ++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index 0d8371a9..293dd7d2 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -1231,9 +1231,9 @@ impl Store { root: H256, signed_block: SignedBlock, ) -> Result<(), Error> { - let mut batch = self.backend.begin_write().expect("write batch"); - write_signed_block(batch.as_mut(), &root, signed_block); - batch.commit().expect("commit"); + let mut batch = self.backend.begin_write()?; + write_signed_block(batch.as_mut(), &root, signed_block)?; + batch.commit()?; Ok(()) } @@ -1249,18 +1249,16 @@ impl Store { root: H256, signed_block: SignedBlock, ) -> Result<(), Error> { - let mut batch = self.backend.begin_write().expect("write batch"); - let block = write_signed_block(batch.as_mut(), &root, signed_block); + let mut batch = self.backend.begin_write()?; + let block = write_signed_block(batch.as_mut(), &root, signed_block)?; let index_entries = vec![( encode_slot_root_key(block.slot, &root), block.parent_root.to_ssz(), )]; - batch - .put_batch(Table::LiveChain, index_entries) - .expect("put non-finalized chain index"); + batch.put_batch(Table::LiveChain, index_entries)?; - batch.commit().expect("commit"); + batch.commit()?; self.record_known_attestation_votes(&block.body.attestations); Ok(()) } @@ -1304,26 +1302,34 @@ impl Store { /// a missing proof 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) -> Result, Error> { - let view = self.backend.begin_read().expect("read view"); - Ok(Self::signed_block_from_view(view.as_ref(), root)) + let view = self.backend.begin_read()?; + let block = Self::signed_block_from_view(view.as_ref(), root)?; + Ok(block) } - fn signed_block_from_view(view: &dyn StorageReadView, root: &H256) -> Option { + fn signed_block_from_view( + view: &dyn StorageReadView, + root: &H256, + ) -> Result, Error> { let key = root.to_ssz(); - let header_bytes = view.get(Table::BlockHeaders, &key).expect("get")?; + let Some(header_bytes) = view.get(Table::BlockHeaders, &key)? else { + return Ok(None); + }; 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 Some(body_bytes) = view.get(Table::BlockBodies, &key)? else { + return Ok(None); + }; BlockBody::from_ssz_bytes(&body_bytes).expect("valid body") }; let sig_key = encode_slot_root_key(header.slot, root); - let proof = match view.get(Table::BlockProof, &sig_key).expect("get") { + let proof = match view.get(Table::BlockProof, &sig_key)? { Some(proof_bytes) => { MultiMessageAggregate::from_ssz_bytes(&proof_bytes).expect("valid block proof") } @@ -1331,15 +1337,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, - }) + })) } /// Return the canonical block root at `slot`, or `None` when the canonical @@ -1371,21 +1377,19 @@ impl Store { start_slot: u64, end_slot: u64, ) -> Result, Error> { - let view = self.backend.begin_read().expect("read view"); + let view = self.backend.begin_read()?; let mut blocks = Vec::new(); for slot in start_slot..=end_slot { // Read the index through this range's own view rather than via // `canonical_root_at_slot`, which opens a fresh one per call: a // range must be served from a single snapshot so a head change // partway through cannot splice two branches into one response. - let Some(root_bytes) = view - .get(Table::BlockRoots, &encode_block_root_key(slot)) - .expect("get block root") + let Some(root_bytes) = view.get(Table::BlockRoots, &encode_block_root_key(slot))? else { continue; }; let root = H256::from_ssz_bytes(&root_bytes).expect("valid block root"); - if let Some(block) = Self::signed_block_from_view(view.as_ref(), &root) { + if let Some(block) = Self::signed_block_from_view(view.as_ref(), &root)? { blocks.push(block); } } @@ -1904,7 +1908,7 @@ fn write_signed_block( batch: &mut dyn StorageWriteBatch, root: &H256, signed_block: SignedBlock, -) -> Block { +) -> Result { let SignedBlock { message: block, proof, @@ -1914,26 +1918,20 @@ fn write_signed_block( let root_bytes = root.to_ssz(); let header_entries = vec![(root_bytes.clone(), header.to_ssz())]; - batch - .put_batch(Table::BlockHeaders, header_entries) - .expect("put block header"); + batch.put_batch(Table::BlockHeaders, header_entries)?; // Skip storing empty bodies - they can be reconstructed from the header's body_root if header.body_root != *EMPTY_BODY_ROOT { let body_entries = vec![(root_bytes.clone(), block.body.to_ssz())]; - batch - .put_batch(Table::BlockBodies, body_entries) - .expect("put block body"); + batch.put_batch(Table::BlockBodies, body_entries)?; } // Store the merged multi-message aggregate proof blob, keyed by slot||root // so proof pruning can scan in slot order and stop early. let proof_entries = vec![(encode_slot_root_key(header.slot, root), proof.to_ssz())]; - batch - .put_batch(Table::BlockProof, proof_entries) - .expect("put block proof"); + batch.put_batch(Table::BlockProof, proof_entries)?; - block + Ok(block) } #[cfg(test)] From 843be3e36217ad1bc288f12f273d372d5d245685 Mon Sep 17 00:00:00 2001 From: Yaroslav Vorobiov Date: Thu, 1 Oct 2026 17:06:45 +0200 Subject: [PATCH 5/6] fix(storage): propagate state storage errors Return backend read and write failures from state retrieval and insertion. Cache inserted states only after the write commits successfully. --- crates/storage/src/store.rs | 37 ++++++++++++++----------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index 293dd7d2..0c70ff53 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -1411,9 +1411,8 @@ impl Store { } // Anchor snapshot in `States`, otherwise reconstruct from the diff chain. let snapshot = { - let view = self.backend.begin_read().expect("read view"); - view.get(Table::States, &root.to_ssz()) - .expect("get") + let view = self.backend.begin_read()?; + view.get(Table::States, &root.to_ssz())? .map(|bytes| State::from_ssz_bytes(&bytes).expect("valid state")) }; let state = if let Some(s) = snapshot { @@ -1437,15 +1436,14 @@ impl Store { /// Returns `Ok(None)` when the root is unknown or the diff chain is broken. 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 view = self.backend.begin_read()?; let mut diffs: Vec = Vec::new(); let mut cursor = *root; let snapshot = loop { - if let Some(bytes) = view.get(Table::States, &cursor.to_ssz()).expect("get") { + if let Some(bytes) = view.get(Table::States, &cursor.to_ssz())? { break State::from_ssz_bytes(&bytes).expect("valid state"); } - let Some(diff_bytes) = view.get(Table::StateDiffs, &cursor.to_ssz()).expect("get") - else { + let Some(diff_bytes) = view.get(Table::StateDiffs, &cursor.to_ssz())? else { return Ok(None); }; let diff = StateDiff::from_ssz_bytes(&diff_bytes).expect("valid state diff"); @@ -1505,33 +1503,26 @@ impl Store { // The post-state's latest_block_header is the block's own header, so its // parent_root identifies the parent (base) state to diff against. 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") - .unwrap(); + let parent_state = self.get_state(&parent_root)?.unwrap(); let is_anchor = state.slot / SNAPSHOT_ANCHOR_INTERVAL > parent_state.slot / SNAPSHOT_ANCHOR_INTERVAL; // Snapshot only at anchors; serialize before `state` is consumed. let snapshot_bytes = is_anchor.then(|| state.to_ssz()); - // Memoize the post-state for fast reads, then move it into the diff so - // its multi-MB justification fields are not cloned again. - self.state_cache.lock().unwrap().put(root, state.clone()); - let diff_bytes = StateDiff::from_states(&parent_state, state) + let diff_bytes = StateDiff::from_states(&parent_state, state.clone()) .expect("state transition produced a non-append historical_block_hashes") .to_ssz(); let key = root.to_ssz(); - let mut batch = self.backend.begin_write().expect("write batch"); - batch - .put_batch(Table::StateDiffs, vec![(key.clone(), diff_bytes)]) - .expect("put state diff"); + let mut batch = self.backend.begin_write()?; + batch.put_batch(Table::StateDiffs, vec![(key.clone(), diff_bytes)])?; if let Some(snapshot_bytes) = snapshot_bytes { - batch - .put_batch(Table::States, vec![(key, snapshot_bytes)]) - .expect("put state snapshot"); + batch.put_batch(Table::States, vec![(key, snapshot_bytes)])?; } - batch.commit().expect("commit"); + batch.commit()?; + + // Cache the state only after it has been persisted successfully. + self.state_cache.lock().unwrap().put(root, state); Ok(()) } From 2f88cd9561ea7914c1a33101c614b86683fbb5a3 Mon Sep 17 00:00:00 2001 From: Yaroslav Vorobiov Date: Thu, 1 Oct 2026 17:24:41 +0200 Subject: [PATCH 6/6] fix(storage): propagate store initialization errors Return backend failures from store restoration and anchor initialization instead of panicking. Add a Store variant to GetForkchoiceStoreError and a StoreInit variant to CheckpointSyncError so checkpoint startup reports storage failures separately from anchor-pair mismatches. --- bin/ethlambda/src/checkpoint_sync.rs | 2 + bin/ethlambda/src/main.rs | 10 ++++- crates/storage/src/store.rs | 58 +++++++++++----------------- 3 files changed, 32 insertions(+), 38 deletions(-) diff --git a/bin/ethlambda/src/checkpoint_sync.rs b/bin/ethlambda/src/checkpoint_sync.rs index 535dc631..104eb4ac 100644 --- a/bin/ethlambda/src/checkpoint_sync.rs +++ b/bin/ethlambda/src/checkpoint_sync.rs @@ -67,6 +67,8 @@ pub enum CheckpointSyncError { BlockHeaderJustifiedRootMismatch, #[error("anchor block does not match anchor state")] AnchorPairingMismatch, + #[error("failed to initialize store from checkpoint: {0}")] + StoreInit(ethlambda_storage::Error), #[error("no checkpoint urls configured")] NoCheckpointUrls, #[error("failed to insert anchor signed block into store")] diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index 92a1fd89..97f5d097 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -60,7 +60,8 @@ use tracing_subscriber::{EnvFilter, Layer, Registry, layer::SubscriberExt}; use ethlambda_blockchain::{BlockChain, BlockChainConfig, EventBus, SyncStatusController}; use ethlambda_rpc::RpcConfig; use ethlambda_storage::{ - MAX_RESUMABLE_DB_STATE_AGE, StorageBackend, Store, backend::RocksDBBackend, + GetForkchoiceStoreError, MAX_RESUMABLE_DB_STATE_AGE, StorageBackend, Store, + backend::RocksDBBackend, }; fn main() -> eyre::Result<()> { @@ -861,7 +862,12 @@ async fn fetch_initial_state( genesis.milliseconds_per_slot, ) .inspect_err(|err| error!(%err, "Failed to initialize store from anchor state and block")) - .map_err(|_| checkpoint_sync::CheckpointSyncError::AnchorPairingMismatch)?; + .map_err(|err| match err { + GetForkchoiceStoreError::AnchorPairInconsistent { .. } => { + checkpoint_sync::CheckpointSyncError::AnchorPairingMismatch + } + GetForkchoiceStoreError::Store(err) => checkpoint_sync::CheckpointSyncError::StoreInit(err), + })?; store .insert_signed_block(anchor_root, signed_block) .inspect_err(|err| error!(%err, "Failed to insert anchor signed block into store")) diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index 0c70ff53..46c73dd1 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -41,6 +41,8 @@ pub enum GetForkchoiceStoreError { anchor_state: Box, anchor_block: Box, }, + #[error("store initialization failed: {0}")] + Store(#[from] crate::error::Error), } /// The tree hash root of an empty block body. @@ -641,13 +643,13 @@ impl Store { }); } - Ok(Self::init_store( + Self::init_store( backend, anchor_state, Some(anchor_block.body), milliseconds_per_slot, ) - .expect("store initialization should succeed in get_forkchoice_store")) + .map_err(Into::into) } /// Build a Store from the state already persisted in the storage backend. @@ -670,15 +672,11 @@ impl Store { let persisted_config = { // Both keys are written by `init_store`, so a backend missing // either has never held a chain. - let view = backend.begin_read().expect("read view"); - let Some(bytes) = view.get(Table::Metadata, KEY_CONFIG).expect("get config") else { + let view = backend.begin_read()?; + let Some(bytes) = view.get(Table::Metadata, KEY_CONFIG)? else { return Ok(None); }; - if view - .get(Table::Metadata, KEY_LATEST_FINALIZED) - .expect("get latest finalized") - .is_none() - { + if view.get(Table::Metadata, KEY_LATEST_FINALIZED)?.is_none() { return Ok(None); } ChainConfig::from_persisted_ssz_bytes(&bytes).expect("valid config") @@ -774,7 +772,7 @@ impl Store { // Insert initial data { - let mut batch = backend.begin_write().expect("write batch"); + let mut batch = backend.begin_write()?; // Metadata let metadata_entries = vec![ @@ -785,55 +783,43 @@ impl Store { (KEY_LATEST_JUSTIFIED.to_vec(), anchor_checkpoint.to_ssz()), (KEY_LATEST_FINALIZED.to_vec(), anchor_checkpoint.to_ssz()), ]; - batch - .put_batch(Table::Metadata, metadata_entries) - .expect("put metadata"); + batch.put_batch(Table::Metadata, metadata_entries)?; // Block header let header_entries = vec![( anchor_block_root.to_ssz(), anchor_state.latest_block_header.to_ssz(), )]; - batch - .put_batch(Table::BlockHeaders, header_entries) - .expect("put block header"); - - batch - .put_batch( - Table::BlockRoots, - vec![( - encode_block_root_key(anchor_state.latest_block_header.slot), - anchor_block_root.to_ssz(), - )], - ) - .expect("put block root index"); + batch.put_batch(Table::BlockHeaders, header_entries)?; + + batch.put_batch( + Table::BlockRoots, + vec![( + encode_block_root_key(anchor_state.latest_block_header.slot), + anchor_block_root.to_ssz(), + )], + )?; // Block body (if provided) if let Some(body) = anchor_body { let body_entries = vec![(anchor_block_root.to_ssz(), body.to_ssz())]; - batch - .put_batch(Table::BlockBodies, body_entries) - .expect("put block body"); + batch.put_batch(Table::BlockBodies, body_entries)?; } // State snapshot. The anchor has no parent in the store, so it is // the base of every diff chain: store it as a full snapshot in // `States` (never pruned) so reconstruction always terminates here. let state_entries = vec![(anchor_block_root.to_ssz(), anchor_state.to_ssz())]; - batch - .put_batch(Table::States, state_entries) - .expect("put state"); + batch.put_batch(Table::States, state_entries)?; // Live chain index let index_entries = vec![( encode_slot_root_key(anchor_state.latest_block_header.slot, &anchor_block_root), anchor_state.latest_block_header.parent_root.to_ssz(), )]; - batch - .put_batch(Table::LiveChain, index_entries) - .expect("put live chain index"); + batch.put_batch(Table::LiveChain, index_entries)?; - batch.commit().expect("commit"); + batch.commit()?; } info!(%anchor_state_root, %anchor_block_root, "Initialized store");