-
Notifications
You must be signed in to change notification settings - Fork 26
Skip malformed live chain keys #443
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -469,10 +469,14 @@ fn encode_live_chain_key(slot: u64, root: &H256) -> Vec<u8> { | |
| } | ||
|
|
||
| /// 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) | ||
| }) | ||
|
Comment on lines
871
to
875
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Prompt To Fix With AIThis is a comment left during a code review.
Path: crates/storage/src/store.rs
Line: 871-875
Comment:
**`unwrap_or(true)` silently deletes malformed keys during any prune**
When `decode_live_chain_key` returns `None`, `unwrap_or(true)` passes the predicate, so malformed keys are included in `keys_to_delete` and deleted — even if `finalized_slot` is 0 or the key sorts well after the finalization boundary. The PR description says read paths "skip" malformed entries, but in `prune_live_chain` they are always deleted. This is probably fine (garbage-collecting corrupt keys), but a comment clarifying the intent would help future readers understand why `true` (rather than `false`) is the fallback here.
How can I resolve this? If you propose a fix, please make it concise. |
||
| .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 { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Malformed keys are silently skipped across all three read paths (
get_live_chain,max_live_chain_slot,get_block_roots) and silently deleted inprune_live_chain. If corrupt data reaches the store due to a bug in a write path, there is no observable signal in logs or metrics. Atracing::warn!(or equivalent) insidedecode_live_chain_keyon theNonebranch would surface the anomaly without changing observable behavior for callers.Prompt To Fix With AI
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!