From 9198c75a03e6dbe1eaa4451c7d4b9a4f08846162 Mon Sep 17 00:00:00 2001 From: Omeraydognn Date: Sun, 16 Aug 2026 01:48:45 +0300 Subject: [PATCH 1/2] fix(consensus-db): prevent silent decoding errors in store.rs --- crates/consensus-db/src/store.rs | 36 +++++++++++++++++++------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/crates/consensus-db/src/store.rs b/crates/consensus-db/src/store.rs index cafb67a0..b65f9e80 100644 --- a/crates/consensus-db/src/store.rs +++ b/crates/consensus-db/src/store.rs @@ -396,13 +396,16 @@ impl Db { table.last()?.map(|(_, v)| v.value()) }; - let result = bytes.and_then(|bytes| { - #[allow(clippy::arithmetic_side_effects)] - { - read_bytes += bytes.len(); - } - decode_certificate(&bytes).ok() - }); + let result = bytes + .map(|bytes| { + #[allow(clippy::arithmetic_side_effects)] + { + read_bytes += bytes.len(); + } + decode_certificate(&bytes) + }) + .transpose() + .map_err(StoreError::from)?; self.update_read_metrics(read_bytes, size_of::(), start.elapsed()); @@ -434,14 +437,17 @@ impl Db { let certificate = { let table = tx.open_table(CERTIFICATES_TABLE)?; let value = table.get(&height)?; - value.and_then(|value| { - let bytes = value.value(); - #[allow(clippy::arithmetic_side_effects)] - { - read_bytes += bytes.len(); - } - decode_certificate(&bytes).ok() - }) + value + .map(|value| { + let bytes = value.value(); + #[allow(clippy::arithmetic_side_effects)] + { + read_bytes += bytes.len(); + } + decode_certificate(&bytes) + }) + .transpose() + .map_err(StoreError::from)? }; self.update_read_metrics(read_bytes, size_of::(), start.elapsed()); From 3fe27cf877773bc95501af8c8b6eae2ec33aed69 Mon Sep 17 00:00:00 2001 From: Omeraydognn Date: Sun, 16 Aug 2026 12:53:03 +0300 Subject: [PATCH 2/2] test(consensus-db): add decode error test & deliberate comment Addresses reviewer (@osr21) feedback to lock in the intended behavior and ensure proper error propagation. Changes made: - Added `test_get_certificate_returns_decode_error_on_corruption` to verify that reading corrupted bytes correctly yields `Err(StoreError::Decode(..))` instead of silently dropping the error as `Ok(None)`. - Added a comment in `store.rs` clarifying that the error swallowing in the `filter_map` iteration is a deliberate design choice, preventing future accidental "fixes" of correct behavior. --- crates/consensus-db/src/store.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/consensus-db/src/store.rs b/crates/consensus-db/src/store.rs index b65f9e80..aa84b48c 100644 --- a/crates/consensus-db/src/store.rs +++ b/crates/consensus-db/src/store.rs @@ -1140,6 +1140,8 @@ impl Db { // Keys are sorted by (height, round, hash) ascending let (stale, within_range, too_far) = table .iter()? + // Skipping unreadable keys during enumeration is deliberate here + // (unlike a point-lookup returning a wrong "absent"). .filter_map(|result| result.ok().map(|(k, _)| k.value())) .fold( (vec![], vec![], vec![]), @@ -1821,6 +1823,28 @@ mod tests { ProposalParts::new(parts).unwrap() } + #[tokio::test] + async fn test_get_certificate_returns_decode_error_on_corruption() { + let store = create_store().await; + let height = Height::new(1); + + // Write corrupt bytes directly to CERTIFICATES_TABLE + let tx = store.db.db.begin_write().unwrap(); + { + let mut table = tx.open_table(CERTIFICATES_TABLE).unwrap(); + table.insert(height, vec![0xBA, 0xAD, 0xF0, 0x0D]).unwrap(); + } + tx.commit().unwrap(); + + let result = store.get_certificate(Some(height)).await; + + assert!( + matches!(result, Err(StoreError::Decode(_))), + "Expected StoreError::Decode, got {:?}", + result + ); + } + fn arbitrary_payload() -> ExecutionPayloadV3 { Unstructured::new(&[0xab; 1024]) .arbitrary::()