From 9b805bad77a93174a8c10e7412a01e06319aaa43 Mon Sep 17 00:00:00 2001 From: samsamtrum Date: Fri, 19 Jun 2026 01:25:47 +0700 Subject: [PATCH 1/2] Skip malformed live chain keys --- crates/storage/src/store.rs | 53 ++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index fb06021f..710c6d4d 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -469,10 +469,14 @@ fn encode_live_chain_key(slot: u64, root: &H256) -> Vec { } /// Decode a LiveChain key from bytes. -fn decode_live_chain_key(bytes: &[u8]) -> (u64, H256) { +fn decode_live_chain_key(bytes: &[u8]) -> Option<(u64, H256)> { + if bytes.len() != 40 { + return None; + } + let slot = u64::from_be_bytes(bytes[..8].try_into().expect("valid slot bytes")); let root = H256::from_slice(&bytes[8..]); - (slot, root) + Some((slot, root)) } /// Fork choice store backed by a pluggable storage backend. @@ -819,10 +823,10 @@ impl Store { view.prefix_iterator(Table::LiveChain, &[]) .expect("iterator") .filter_map(|res| res.ok()) - .map(|(k, v)| { - let (slot, root) = decode_live_chain_key(&k); + .filter_map(|(k, v)| { + let (slot, root) = decode_live_chain_key(&k)?; let parent_root = H256::from_ssz_bytes(&v).expect("valid parent_root"); - (root, (slot, parent_root)) + Some((root, (slot, parent_root))) }) .collect() } @@ -833,7 +837,7 @@ impl Store { view.prefix_iterator(Table::LiveChain, &[]) .expect("iterator") .filter_map(Result::ok) - .map(|(key, _)| decode_live_chain_key(&key).0) + .filter_map(|(key, _)| decode_live_chain_key(&key).map(|(slot, _)| slot)) .max() } @@ -845,10 +849,7 @@ impl Store { view.prefix_iterator(Table::LiveChain, &[]) .expect("iterator") .filter_map(|res| res.ok()) - .map(|(k, _)| { - let (_, root) = decode_live_chain_key(&k); - root - }) + .filter_map(|(k, _)| decode_live_chain_key(&k).map(|(_, root)| root)) .collect() } @@ -868,8 +869,9 @@ impl Store { .expect("iterator") .filter_map(|res| res.ok()) .take_while(|(k, _)| { - let (slot, _) = decode_live_chain_key(k); - slot < finalized_slot + decode_live_chain_key(k) + .map(|(slot, _)| slot < finalized_slot) + .unwrap_or(true) }) .map(|(k, _)| k.to_vec()) .collect(); @@ -1420,6 +1422,33 @@ mod tests { use super::*; use crate::backend::InMemoryBackend; + #[test] + fn live_chain_queries_skip_malformed_keys() { + let backend = Arc::new(InMemoryBackend::new()); + let store = Store::test_store_with_backend(backend.clone()); + let root = root(1); + let parent = H256::ZERO; + + let mut batch = backend.begin_write().expect("write batch"); + batch + .put_batch( + Table::LiveChain, + vec![ + (vec![1, 2, 3], parent.to_ssz()), + (encode_live_chain_key(42, &root), parent.to_ssz()), + ], + ) + .expect("put live chain"); + batch.commit().expect("commit"); + + assert_eq!(store.max_live_chain_slot(), Some(42)); + assert_eq!(store.get_block_roots(), HashSet::from([root])); + + let live_chain = store.get_live_chain(); + assert_eq!(live_chain.len(), 1); + assert_eq!(live_chain.get(&root), Some(&(42, parent))); + } + /// Insert a block header (and dummy body + signature) for a given root and slot. fn insert_header(backend: &dyn StorageBackend, root: H256, slot: u64) { let header = BlockHeader { From e7cceeb80609896b9293091ae3c8c1298eb326af Mon Sep 17 00:00:00 2001 From: samsamtrum Date: Fri, 26 Jun 2026 20:07:38 +0700 Subject: [PATCH 2/2] Return Result for live chain key decode --- crates/storage/src/store.rs | 53 ++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index 710c6d4d..6630c53e 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -468,15 +468,23 @@ fn encode_live_chain_key(slot: u64, root: &H256) -> Vec { result } +#[derive(Debug, Error)] +enum DecodeLiveChainKeyError { + #[error("invalid LiveChain key length: expected 40 bytes, got {actual}")] + InvalidLength { actual: usize }, +} + /// Decode a LiveChain key from bytes. -fn decode_live_chain_key(bytes: &[u8]) -> Option<(u64, H256)> { +fn decode_live_chain_key(bytes: &[u8]) -> Result<(u64, H256), DecodeLiveChainKeyError> { if bytes.len() != 40 { - return None; + return Err(DecodeLiveChainKeyError::InvalidLength { + actual: bytes.len(), + }); } let slot = u64::from_be_bytes(bytes[..8].try_into().expect("valid slot bytes")); let root = H256::from_slice(&bytes[8..]); - Some((slot, root)) + Ok((slot, root)) } /// Fork choice store backed by a pluggable storage backend. @@ -823,10 +831,15 @@ impl Store { view.prefix_iterator(Table::LiveChain, &[]) .expect("iterator") .filter_map(|res| res.ok()) - .filter_map(|(k, v)| { - let (slot, root) = decode_live_chain_key(&k)?; - let parent_root = H256::from_ssz_bytes(&v).expect("valid parent_root"); - Some((root, (slot, parent_root))) + .filter_map(|(k, v)| match decode_live_chain_key(&k) { + Ok((slot, root)) => { + let parent_root = H256::from_ssz_bytes(&v).expect("valid parent_root"); + Some((root, (slot, parent_root))) + } + Err(error) => { + warn!(%error, "Skipping malformed LiveChain key"); + None + } }) .collect() } @@ -837,7 +850,13 @@ impl Store { view.prefix_iterator(Table::LiveChain, &[]) .expect("iterator") .filter_map(Result::ok) - .filter_map(|(key, _)| decode_live_chain_key(&key).map(|(slot, _)| slot)) + .filter_map(|(key, _)| match decode_live_chain_key(&key) { + Ok((slot, _)) => Some(slot), + Err(error) => { + warn!(%error, "Skipping malformed LiveChain key"); + None + } + }) .max() } @@ -849,7 +868,13 @@ impl Store { view.prefix_iterator(Table::LiveChain, &[]) .expect("iterator") .filter_map(|res| res.ok()) - .filter_map(|(k, _)| decode_live_chain_key(&k).map(|(_, root)| root)) + .filter_map(|(k, _)| match decode_live_chain_key(&k) { + Ok((_, root)) => Some(root), + Err(error) => { + warn!(%error, "Skipping malformed LiveChain key"); + None + } + }) .collect() } @@ -868,10 +893,12 @@ impl Store { .prefix_iterator(Table::LiveChain, &[]) .expect("iterator") .filter_map(|res| res.ok()) - .take_while(|(k, _)| { - decode_live_chain_key(k) - .map(|(slot, _)| slot < finalized_slot) - .unwrap_or(true) + .take_while(|(k, _)| match decode_live_chain_key(k) { + Ok((slot, _)) => slot < finalized_slot, + Err(error) => { + warn!(%error, "Pruning malformed LiveChain key"); + true + } }) .map(|(k, _)| k.to_vec()) .collect();