Skip to main content

ipfrs_storage/
block_verifier.rs

1//! Storage block integrity verifier using FNV-1a checksums.
2//!
3//! Tracks verification results and produces detailed reports for
4//! auditing and self-healing workflows.
5
6use std::collections::HashMap;
7
8// ---------------------------------------------------------------------------
9// FNV-1a checksum (64-bit)
10// ---------------------------------------------------------------------------
11
12#[inline]
13fn fnv1a_checksum(data: &[u8]) -> u64 {
14    let mut hash: u64 = 0xcbf29ce484222325;
15    for &byte in data {
16        hash ^= byte as u64;
17        hash = hash.wrapping_mul(0x100000001b3);
18    }
19    hash
20}
21
22// ---------------------------------------------------------------------------
23// VerificationResult
24// ---------------------------------------------------------------------------
25
26/// The outcome of verifying a single storage block.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub enum VerificationResult {
29    /// Checksum matches — block is intact.
30    Ok,
31    /// Checksum mismatch — block content has changed since registration.
32    Corrupted {
33        /// The FNV-1a checksum recorded at write time.
34        expected: u64,
35        /// The FNV-1a checksum computed from the current content.
36        actual: u64,
37    },
38    /// Block was not found in the registry, or content was absent.
39    Missing,
40}
41
42// ---------------------------------------------------------------------------
43// BlockRecord
44// ---------------------------------------------------------------------------
45
46/// Metadata and last verification state for a single registered block.
47#[derive(Clone, Debug)]
48pub struct BlockRecord {
49    /// Unique block identifier assigned by the verifier.
50    pub block_id: u64,
51    /// Content Identifier (CID) string for this block.
52    pub cid: String,
53    /// FNV-1a checksum of the block content at registration time.
54    pub expected_checksum: u64,
55    /// Size of the block content in bytes.
56    pub size_bytes: u64,
57    /// Logical tick at which the last verification was performed, if any.
58    pub verified_at_tick: Option<u64>,
59    /// Result of the most recent verification, if any.
60    pub last_result: Option<VerificationResult>,
61}
62
63// ---------------------------------------------------------------------------
64// VerificationReport
65// ---------------------------------------------------------------------------
66
67/// Summary of a batch verification run.
68#[derive(Clone, Debug)]
69pub struct VerificationReport {
70    /// Total number of blocks checked in this batch.
71    pub total_checked: usize,
72    /// Number of blocks whose checksums matched.
73    pub ok_count: usize,
74    /// Number of blocks with checksum mismatches.
75    pub corrupted_count: usize,
76    /// Number of missing blocks (not registered or content absent).
77    pub missing_count: usize,
78    /// Block IDs of corrupted blocks, sorted ascending.
79    pub corrupted_ids: Vec<u64>,
80    /// Block IDs of missing blocks, sorted ascending.
81    pub missing_ids: Vec<u64>,
82}
83
84impl VerificationReport {
85    /// Returns `true` when no corruption or missing blocks were found.
86    #[inline]
87    pub fn is_clean(&self) -> bool {
88        self.corrupted_count == 0 && self.missing_count == 0
89    }
90}
91
92// ---------------------------------------------------------------------------
93// VerifierStats
94// ---------------------------------------------------------------------------
95
96/// Cumulative statistics across all verification operations.
97#[derive(Clone, Debug, Default)]
98pub struct VerifierStats {
99    /// Total number of distinct blocks currently registered.
100    pub total_blocks_registered: usize,
101    /// Total number of individual block verifications performed.
102    pub total_verifications_run: u64,
103    /// Total verifications that returned `Ok`.
104    pub total_ok: u64,
105    /// Total verifications that returned `Corrupted`.
106    pub total_corrupted: u64,
107    /// Total verifications that returned `Missing`.
108    pub total_missing: u64,
109}
110
111// ---------------------------------------------------------------------------
112// StorageBlockVerifier
113// ---------------------------------------------------------------------------
114
115/// Verifies storage block integrity using FNV-1a checksums.
116///
117/// Tracks per-block verification history and accumulates lifetime statistics
118/// suitable for auditing and self-healing pipelines.
119pub struct StorageBlockVerifier {
120    records: HashMap<u64, BlockRecord>,
121    next_block_id: u64,
122    stats: VerifierStats,
123}
124
125impl StorageBlockVerifier {
126    /// Creates a new, empty verifier.
127    pub fn new() -> Self {
128        Self {
129            records: HashMap::new(),
130            next_block_id: 0,
131            stats: VerifierStats::default(),
132        }
133    }
134
135    /// Registers a block, computing its FNV-1a checksum from `content`.
136    ///
137    /// Returns the newly assigned `block_id`.
138    pub fn register(&mut self, cid: String, content: &[u8]) -> u64 {
139        let block_id = self.next_block_id;
140        self.next_block_id += 1;
141
142        let expected_checksum = fnv1a_checksum(content);
143        let record = BlockRecord {
144            block_id,
145            cid,
146            expected_checksum,
147            size_bytes: content.len() as u64,
148            verified_at_tick: None,
149            last_result: None,
150        };
151
152        self.records.insert(block_id, record);
153        self.stats.total_blocks_registered += 1;
154
155        block_id
156    }
157
158    /// Verifies a single block against its registered checksum.
159    ///
160    /// - If `block_id` is unknown → `Missing` (stats updated, **no** record mutation).
161    /// - If `content` is `None` → `Missing` (record updated).
162    /// - Otherwise computes the FNV-1a checksum and returns `Ok` or `Corrupted`.
163    ///
164    /// Always increments `stats.total_verifications_run`.
165    pub fn verify_block(
166        &mut self,
167        block_id: u64,
168        content: Option<&[u8]>,
169        current_tick: u64,
170    ) -> VerificationResult {
171        // Block not registered at all.
172        if !self.records.contains_key(&block_id) {
173            self.stats.total_missing += 1;
174            self.stats.total_verifications_run += 1;
175            return VerificationResult::Missing;
176        }
177
178        self.stats.total_verifications_run += 1;
179
180        let content_bytes = match content {
181            None => {
182                self.stats.total_missing += 1;
183                let record = self
184                    .records
185                    .get_mut(&block_id)
186                    .expect("key existence checked above");
187                record.verified_at_tick = Some(current_tick);
188                record.last_result = Some(VerificationResult::Missing);
189                return VerificationResult::Missing;
190            }
191            Some(bytes) => bytes,
192        };
193
194        let record = self
195            .records
196            .get_mut(&block_id)
197            .expect("key existence checked above");
198
199        let actual = fnv1a_checksum(content_bytes);
200        let result = if actual == record.expected_checksum {
201            self.stats.total_ok += 1;
202            VerificationResult::Ok
203        } else {
204            self.stats.total_corrupted += 1;
205            VerificationResult::Corrupted {
206                expected: record.expected_checksum,
207                actual,
208            }
209        };
210
211        record.verified_at_tick = Some(current_tick);
212        record.last_result = Some(result.clone());
213
214        result
215    }
216
217    /// Verifies a batch of `(block_id, content)` pairs and returns a summary report.
218    ///
219    /// `corrupted_ids` and `missing_ids` in the returned report are sorted ascending.
220    pub fn verify_batch(
221        &mut self,
222        batch: Vec<(u64, Option<Vec<u8>>)>,
223        current_tick: u64,
224    ) -> VerificationReport {
225        let mut ok_count = 0usize;
226        let mut corrupted_count = 0usize;
227        let mut missing_count = 0usize;
228        let mut corrupted_ids: Vec<u64> = Vec::new();
229        let mut missing_ids: Vec<u64> = Vec::new();
230
231        let total_checked = batch.len();
232
233        for (block_id, content) in batch {
234            let result = self.verify_block(block_id, content.as_deref(), current_tick);
235            match result {
236                VerificationResult::Ok => ok_count += 1,
237                VerificationResult::Corrupted { .. } => {
238                    corrupted_count += 1;
239                    corrupted_ids.push(block_id);
240                }
241                VerificationResult::Missing => {
242                    missing_count += 1;
243                    missing_ids.push(block_id);
244                }
245            }
246        }
247
248        corrupted_ids.sort_unstable();
249        missing_ids.sort_unstable();
250
251        VerificationReport {
252            total_checked,
253            ok_count,
254            corrupted_count,
255            missing_count,
256            corrupted_ids,
257            missing_ids,
258        }
259    }
260
261    /// Returns a reference to the record for `block_id`, if it exists.
262    pub fn get_record(&self, block_id: u64) -> Option<&BlockRecord> {
263        self.records.get(&block_id)
264    }
265
266    /// Returns a reference to the cumulative statistics.
267    pub fn stats(&self) -> &VerifierStats {
268        &self.stats
269    }
270
271    /// Returns block IDs that have never been verified, sorted ascending.
272    pub fn unverified_blocks(&self) -> Vec<u64> {
273        let mut ids: Vec<u64> = self
274            .records
275            .values()
276            .filter(|r| r.verified_at_tick.is_none())
277            .map(|r| r.block_id)
278            .collect();
279        ids.sort_unstable();
280        ids
281    }
282
283    /// Returns block IDs whose most recent verification was `Corrupted`, sorted ascending.
284    pub fn corrupted_blocks(&self) -> Vec<u64> {
285        let mut ids: Vec<u64> = self
286            .records
287            .values()
288            .filter(|r| matches!(&r.last_result, Some(VerificationResult::Corrupted { .. })))
289            .map(|r| r.block_id)
290            .collect();
291        ids.sort_unstable();
292        ids
293    }
294}
295
296impl Default for StorageBlockVerifier {
297    fn default() -> Self {
298        Self::new()
299    }
300}
301
302// ---------------------------------------------------------------------------
303// Tests
304// ---------------------------------------------------------------------------
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    // -----------------------------------------------------------------------
311    // Helper
312    // -----------------------------------------------------------------------
313
314    fn make_verifier() -> StorageBlockVerifier {
315        StorageBlockVerifier::new()
316    }
317
318    fn expected_checksum(data: &[u8]) -> u64 {
319        fnv1a_checksum(data)
320    }
321
322    // -----------------------------------------------------------------------
323    // register
324    // -----------------------------------------------------------------------
325
326    #[test]
327    fn test_register_computes_correct_checksum() {
328        let mut v = make_verifier();
329        let content = b"hello world";
330        let id = v.register("cid-1".into(), content);
331        let record = v.get_record(id).expect("record must exist");
332        assert_eq!(record.expected_checksum, expected_checksum(content));
333    }
334
335    #[test]
336    fn test_register_stores_size() {
337        let mut v = make_verifier();
338        let content = b"abcde";
339        let id = v.register("cid-size".into(), content);
340        let record = v.get_record(id).expect("record must exist");
341        assert_eq!(record.size_bytes, 5);
342    }
343
344    #[test]
345    fn test_register_stores_cid() {
346        let mut v = make_verifier();
347        let id = v.register("my-cid".into(), b"data");
348        let record = v.get_record(id).expect("record must exist");
349        assert_eq!(record.cid, "my-cid");
350    }
351
352    #[test]
353    fn test_register_increments_block_id() {
354        let mut v = make_verifier();
355        let id0 = v.register("cid-0".into(), b"a");
356        let id1 = v.register("cid-1".into(), b"b");
357        let id2 = v.register("cid-2".into(), b"c");
358        assert_eq!(id0, 0);
359        assert_eq!(id1, 1);
360        assert_eq!(id2, 2);
361    }
362
363    #[test]
364    fn test_register_increments_stats() {
365        let mut v = make_verifier();
366        v.register("a".into(), b"x");
367        v.register("b".into(), b"y");
368        assert_eq!(v.stats().total_blocks_registered, 2);
369    }
370
371    #[test]
372    fn test_register_no_verified_at_tick() {
373        let mut v = make_verifier();
374        let id = v.register("cid".into(), b"data");
375        let record = v.get_record(id).expect("record must exist");
376        assert!(record.verified_at_tick.is_none());
377        assert!(record.last_result.is_none());
378    }
379
380    // -----------------------------------------------------------------------
381    // verify_block — Ok
382    // -----------------------------------------------------------------------
383
384    #[test]
385    fn test_verify_block_ok_when_content_matches() {
386        let mut v = make_verifier();
387        let content = b"consistent data";
388        let id = v.register("cid".into(), content);
389        let result = v.verify_block(id, Some(content), 1);
390        assert_eq!(result, VerificationResult::Ok);
391    }
392
393    #[test]
394    fn test_verify_block_ok_updates_record() {
395        let mut v = make_verifier();
396        let content = b"data";
397        let id = v.register("cid".into(), content);
398        v.verify_block(id, Some(content), 42);
399        let record = v.get_record(id).expect("record must exist");
400        assert_eq!(record.verified_at_tick, Some(42));
401        assert_eq!(record.last_result, Some(VerificationResult::Ok));
402    }
403
404    // -----------------------------------------------------------------------
405    // verify_block — Corrupted
406    // -----------------------------------------------------------------------
407
408    #[test]
409    fn test_verify_block_corrupted_when_content_changed() {
410        let mut v = make_verifier();
411        let id = v.register("cid".into(), b"original");
412        let result = v.verify_block(id, Some(b"modified"), 1);
413        assert!(matches!(result, VerificationResult::Corrupted { .. }));
414    }
415
416    #[test]
417    fn test_verify_block_corrupted_expected_and_actual() {
418        let mut v = make_verifier();
419        let original = b"original";
420        let modified = b"modified!!";
421        let id = v.register("cid".into(), original);
422        let expected_cs = expected_checksum(original);
423        let actual_cs = expected_checksum(modified);
424        let result = v.verify_block(id, Some(modified), 1);
425        assert_eq!(
426            result,
427            VerificationResult::Corrupted {
428                expected: expected_cs,
429                actual: actual_cs,
430            }
431        );
432    }
433
434    #[test]
435    fn test_verify_block_corrupted_updates_record() {
436        let mut v = make_verifier();
437        let id = v.register("cid".into(), b"a");
438        v.verify_block(id, Some(b"b"), 7);
439        let record = v.get_record(id).expect("record must exist");
440        assert_eq!(record.verified_at_tick, Some(7));
441        assert!(matches!(
442            record.last_result,
443            Some(VerificationResult::Corrupted { .. })
444        ));
445    }
446
447    // -----------------------------------------------------------------------
448    // verify_block — Missing
449    // -----------------------------------------------------------------------
450
451    #[test]
452    fn test_verify_block_missing_when_not_registered() {
453        let mut v = make_verifier();
454        let result = v.verify_block(999, Some(b"some data"), 1);
455        assert_eq!(result, VerificationResult::Missing);
456    }
457
458    #[test]
459    fn test_verify_block_missing_when_content_none() {
460        let mut v = make_verifier();
461        let id = v.register("cid".into(), b"data");
462        let result = v.verify_block(id, None, 5);
463        assert_eq!(result, VerificationResult::Missing);
464    }
465
466    #[test]
467    fn test_verify_block_none_updates_record() {
468        let mut v = make_verifier();
469        let id = v.register("cid".into(), b"data");
470        v.verify_block(id, None, 10);
471        let record = v.get_record(id).expect("record must exist");
472        assert_eq!(record.verified_at_tick, Some(10));
473        assert_eq!(record.last_result, Some(VerificationResult::Missing));
474    }
475
476    // -----------------------------------------------------------------------
477    // stats accumulation
478    // -----------------------------------------------------------------------
479
480    #[test]
481    fn test_stats_total_verifications_run() {
482        let mut v = make_verifier();
483        let id = v.register("cid".into(), b"data");
484        v.verify_block(id, Some(b"data"), 1);
485        v.verify_block(id, Some(b"wrong"), 2);
486        v.verify_block(id, None, 3);
487        v.verify_block(9999, Some(b"x"), 4); // missing id
488        assert_eq!(v.stats().total_verifications_run, 4);
489    }
490
491    #[test]
492    fn test_stats_total_ok() {
493        let mut v = make_verifier();
494        let id = v.register("cid".into(), b"data");
495        v.verify_block(id, Some(b"data"), 1);
496        v.verify_block(id, Some(b"data"), 2);
497        assert_eq!(v.stats().total_ok, 2);
498    }
499
500    #[test]
501    fn test_stats_total_corrupted() {
502        let mut v = make_verifier();
503        let id = v.register("cid".into(), b"data");
504        v.verify_block(id, Some(b"bad"), 1);
505        v.verify_block(id, Some(b"worse"), 2);
506        assert_eq!(v.stats().total_corrupted, 2);
507    }
508
509    #[test]
510    fn test_stats_total_missing() {
511        let mut v = make_verifier();
512        let id = v.register("cid".into(), b"data");
513        v.verify_block(id, None, 1); // missing (None)
514        v.verify_block(9999, Some(b"x"), 2); // missing (unknown id)
515        assert_eq!(v.stats().total_missing, 2);
516    }
517
518    #[test]
519    fn test_stats_accumulate_across_calls() {
520        let mut v = make_verifier();
521        let id = v.register("cid".into(), b"hello");
522        v.verify_block(id, Some(b"hello"), 1); // ok
523        v.verify_block(id, Some(b"world"), 2); // corrupted
524        v.verify_block(id, None, 3); // missing
525        let s = v.stats();
526        assert_eq!(s.total_ok, 1);
527        assert_eq!(s.total_corrupted, 1);
528        assert_eq!(s.total_missing, 1);
529        assert_eq!(s.total_verifications_run, 3);
530    }
531
532    // -----------------------------------------------------------------------
533    // verify_batch
534    // -----------------------------------------------------------------------
535
536    #[test]
537    fn test_verify_batch_report_counts() {
538        let mut v = make_verifier();
539        let id_ok = v.register("ok".into(), b"good");
540        let id_bad = v.register("bad".into(), b"original");
541        let id_reg = v.register("none".into(), b"data");
542
543        let batch = vec![
544            (id_ok, Some(b"good".to_vec())),
545            (id_bad, Some(b"corrupted".to_vec())),
546            (id_reg, None),
547            (9999, Some(b"x".to_vec())), // unregistered → missing
548        ];
549
550        let report = v.verify_batch(batch, 1);
551        assert_eq!(report.total_checked, 4);
552        assert_eq!(report.ok_count, 1);
553        assert_eq!(report.corrupted_count, 1);
554        assert_eq!(report.missing_count, 2);
555    }
556
557    #[test]
558    fn test_verify_batch_sorted_ids() {
559        let mut v = make_verifier();
560        // Register three blocks; ids will be 0, 1, 2.
561        let id0 = v.register("c0".into(), b"a");
562        let id1 = v.register("c1".into(), b"b");
563        let id2 = v.register("c2".into(), b"c");
564
565        // Submit in reverse order with corrupted/missing content.
566        let batch = vec![
567            (id2, Some(b"z".to_vec())), // corrupted
568            (id0, Some(b"z".to_vec())), // corrupted
569            (id1, None),                // missing
570        ];
571        let report = v.verify_batch(batch, 1);
572        assert_eq!(report.corrupted_ids, vec![id0, id2]);
573        assert_eq!(report.missing_ids, vec![id1]);
574    }
575
576    // -----------------------------------------------------------------------
577    // is_clean
578    // -----------------------------------------------------------------------
579
580    #[test]
581    fn test_report_is_clean_true() {
582        let mut v = make_verifier();
583        let id = v.register("cid".into(), b"data");
584        let report = v.verify_batch(vec![(id, Some(b"data".to_vec()))], 1);
585        assert!(report.is_clean());
586    }
587
588    #[test]
589    fn test_report_is_clean_false_when_corrupted() {
590        let mut v = make_verifier();
591        let id = v.register("cid".into(), b"data");
592        let report = v.verify_batch(vec![(id, Some(b"wrong".to_vec()))], 1);
593        assert!(!report.is_clean());
594    }
595
596    #[test]
597    fn test_report_is_clean_false_when_missing() {
598        let mut v = make_verifier();
599        let id = v.register("cid".into(), b"data");
600        let report = v.verify_batch(vec![(id, None)], 1);
601        assert!(!report.is_clean());
602    }
603
604    // -----------------------------------------------------------------------
605    // unverified_blocks
606    // -----------------------------------------------------------------------
607
608    #[test]
609    fn test_unverified_blocks_returns_all_initially() {
610        let mut v = make_verifier();
611        v.register("a".into(), b"1");
612        v.register("b".into(), b"2");
613        v.register("c".into(), b"3");
614        let unverified = v.unverified_blocks();
615        assert_eq!(unverified, vec![0, 1, 2]);
616    }
617
618    #[test]
619    fn test_unverified_blocks_excludes_verified() {
620        let mut v = make_verifier();
621        let id0 = v.register("a".into(), b"1");
622        v.register("b".into(), b"2");
623        v.verify_block(id0, Some(b"1"), 1);
624        let unverified = v.unverified_blocks();
625        assert_eq!(unverified, vec![1]);
626    }
627
628    #[test]
629    fn test_unverified_blocks_empty_when_all_verified() {
630        let mut v = make_verifier();
631        let id0 = v.register("a".into(), b"x");
632        let id1 = v.register("b".into(), b"y");
633        v.verify_block(id0, Some(b"x"), 1);
634        v.verify_block(id1, Some(b"y"), 1);
635        assert!(v.unverified_blocks().is_empty());
636    }
637
638    // -----------------------------------------------------------------------
639    // corrupted_blocks
640    // -----------------------------------------------------------------------
641
642    #[test]
643    fn test_corrupted_blocks_returns_only_corrupted() {
644        let mut v = make_verifier();
645        let id0 = v.register("a".into(), b"original");
646        let id1 = v.register("b".into(), b"good");
647        let id2 = v.register("c".into(), b"also original");
648
649        v.verify_block(id0, Some(b"changed"), 1); // corrupted
650        v.verify_block(id1, Some(b"good"), 1); // ok
651        v.verify_block(id2, Some(b"also changed"), 1); // corrupted
652
653        let corrupted = v.corrupted_blocks();
654        assert_eq!(corrupted, vec![id0, id2]);
655    }
656
657    #[test]
658    fn test_corrupted_blocks_empty_initially() {
659        let mut v = make_verifier();
660        v.register("a".into(), b"x");
661        assert!(v.corrupted_blocks().is_empty());
662    }
663
664    #[test]
665    fn test_corrupted_blocks_not_including_missing() {
666        let mut v = make_verifier();
667        let id = v.register("a".into(), b"data");
668        v.verify_block(id, None, 1); // missing, not corrupted
669        assert!(v.corrupted_blocks().is_empty());
670    }
671
672    // -----------------------------------------------------------------------
673    // batch with mixed results
674    // -----------------------------------------------------------------------
675
676    #[test]
677    fn test_batch_mixed_results_all_categories() {
678        let mut v = make_verifier();
679        let id_ok = v.register("ok".into(), b"abc");
680        let id_bad = v.register("bad".into(), b"xyz");
681        let id_none = v.register("none".into(), b"zzz");
682
683        let batch = vec![
684            (id_ok, Some(b"abc".to_vec())),
685            (id_bad, Some(b"XYZ-TAMPERED".to_vec())),
686            (id_none, None),
687            (42, Some(b"phantom".to_vec())), // unknown block id
688        ];
689
690        let report = v.verify_batch(batch, 100);
691        assert_eq!(report.total_checked, 4);
692        assert_eq!(report.ok_count, 1);
693        assert_eq!(report.corrupted_count, 1);
694        assert_eq!(report.missing_count, 2);
695        assert!(!report.is_clean());
696
697        // Stats should reflect totals
698        let s = v.stats();
699        assert_eq!(s.total_ok, 1);
700        assert_eq!(s.total_corrupted, 1);
701        assert_eq!(s.total_missing, 2);
702        assert_eq!(s.total_verifications_run, 4);
703    }
704
705    #[test]
706    fn test_empty_batch_produces_clean_report() {
707        let mut v = make_verifier();
708        let report = v.verify_batch(vec![], 1);
709        assert_eq!(report.total_checked, 0);
710        assert!(report.is_clean());
711    }
712
713    #[test]
714    fn test_fnv1a_deterministic() {
715        let data = b"determinism test";
716        let a = fnv1a_checksum(data);
717        let b = fnv1a_checksum(data);
718        assert_eq!(a, b);
719    }
720
721    #[test]
722    fn test_fnv1a_different_inputs_differ() {
723        let a = fnv1a_checksum(b"alpha");
724        let b = fnv1a_checksum(b"beta");
725        assert_ne!(a, b);
726    }
727
728    #[test]
729    fn test_get_record_returns_none_for_unknown() {
730        let v = make_verifier();
731        assert!(v.get_record(0).is_none());
732    }
733}