Skip to main content

ipfrs_tensorlogic/
tensor_checksum.rs

1//! Tensor Checksum Engine
2//!
3//! Computes and verifies checksums for tensor data to detect corruption during
4//! storage or transmission. Supports multiple pure-Rust checksum algorithms
5//! with a stateful engine that tracks per-tensor records and verification stats.
6//!
7//! # Algorithms
8//!
9//! | Algorithm    | Width  | Speed  | Use-case                                 |
10//! |-------------|--------|--------|------------------------------------------|
11//! | `Fnv1a64`   | 64-bit | Fast   | General-purpose, non-cryptographic hash  |
12//! | `Adler32`   | 32-bit | Fast   | Data integrity, used in zlib             |
13//! | `Fletcher16`| 16-bit | Fast   | Lightweight embedded / small payloads    |
14//! | `XorFold`   | 64-bit | Fastest| Ultra-fast, low-collision large tensors  |
15//!
16//! # Examples
17//!
18//! ```
19//! use ipfrs_tensorlogic::tensor_checksum::{
20//!     ChecksumAlgorithm, TensorChecksumEngine,
21//! };
22//!
23//! let mut engine = TensorChecksumEngine::new();
24//! let data = b"hello tensor world";
25//! let record = engine.compute(1, "layer0".to_string(), data, ChecksumAlgorithm::Fnv1a64, 0);
26//! assert!(record.is_valid(data));
27//!
28//! let ok = engine.verify(1, data).expect("example: should succeed in docs");
29//! assert!(ok);
30//! ```
31
32use std::collections::HashMap;
33
34// ──────────────────────────────────────────────────────────────────────────────
35// ChecksumAlgorithm
36// ──────────────────────────────────────────────────────────────────────────────
37
38/// Checksum algorithm selection.
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum ChecksumAlgorithm {
41    /// FNV-1a 64-bit non-cryptographic hash.
42    Fnv1a64,
43    /// Adler-32 checksum (mod 65521), result cast to u64.
44    Adler32,
45    /// Fletcher-16 checksum, result cast to u64.
46    Fletcher16,
47    /// XOR all 8-byte chunks (zero-pad last chunk), fold to u64.
48    XorFold,
49}
50
51// ──────────────────────────────────────────────────────────────────────────────
52// Pure-Rust implementations
53// ──────────────────────────────────────────────────────────────────────────────
54
55const FNV_OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
56const FNV_PRIME: u64 = 1_099_511_628_211;
57
58/// Compute the FNV-1a 64-bit hash over arbitrary bytes.
59///
60/// Uses the standard parameters: offset basis `14695981039346656037`,
61/// prime `1099511628211`.
62///
63/// # Examples
64///
65/// ```
66/// use ipfrs_tensorlogic::tensor_checksum::fnv1a64;
67/// let h = fnv1a64(b"hello");
68/// assert_ne!(h, 0);
69/// assert_eq!(h, fnv1a64(b"hello")); // deterministic
70/// ```
71pub fn fnv1a64(data: &[u8]) -> u64 {
72    let mut hash = FNV_OFFSET_BASIS;
73    for &byte in data {
74        hash ^= u64::from(byte);
75        hash = hash.wrapping_mul(FNV_PRIME);
76    }
77    hash
78}
79
80/// Adler-32 checksum (pure Rust).
81///
82/// Uses modulus 65521 (the largest prime less than 65536) as defined in
83/// RFC 1950 / zlib spec. The 32-bit result is cast to u64.
84///
85/// # Examples
86///
87/// ```
88/// use ipfrs_tensorlogic::tensor_checksum::adler32;
89/// // Known value: "ABC" → 0x018D00C7
90/// assert_eq!(adler32(b"ABC"), 0x018D00C7);
91/// ```
92pub fn adler32(data: &[u8]) -> u64 {
93    const MOD_ADLER: u32 = 65521;
94    let mut a: u32 = 1;
95    let mut b: u32 = 0;
96    for &byte in data {
97        a = (a + u32::from(byte)) % MOD_ADLER;
98        b = (b + a) % MOD_ADLER;
99    }
100    u64::from((b << 16) | a)
101}
102
103/// Fletcher-16 checksum (pure Rust).
104///
105/// Processes bytes in pairs of sums, each taken modulo 255. The 16-bit
106/// result is cast to u64.
107///
108/// # Examples
109///
110/// ```
111/// use ipfrs_tensorlogic::tensor_checksum::fletcher16;
112/// let h = fletcher16(b"hello");
113/// assert_ne!(h, 0);
114/// ```
115pub fn fletcher16(data: &[u8]) -> u64 {
116    let mut sum1: u16 = 0;
117    let mut sum2: u16 = 0;
118    for &byte in data {
119        sum1 = (sum1 + u16::from(byte)) % 255;
120        sum2 = (sum2 + sum1) % 255;
121    }
122    u64::from((sum2 << 8) | sum1)
123}
124
125/// XOR-fold checksum.
126///
127/// Splits `data` into 8-byte chunks (the final chunk is zero-padded if
128/// shorter) and XORs all of them together to produce a u64.
129///
130/// # Examples
131///
132/// ```
133/// use ipfrs_tensorlogic::tensor_checksum::xor_fold;
134/// let h = xor_fold(b"12345678");
135/// assert_ne!(h, 0);
136/// assert_eq!(xor_fold(b""), 0);
137/// ```
138pub fn xor_fold(data: &[u8]) -> u64 {
139    let mut result: u64 = 0;
140    let mut idx = 0;
141    while idx + 8 <= data.len() {
142        let chunk = u64::from_le_bytes([
143            data[idx],
144            data[idx + 1],
145            data[idx + 2],
146            data[idx + 3],
147            data[idx + 4],
148            data[idx + 5],
149            data[idx + 6],
150            data[idx + 7],
151        ]);
152        result ^= chunk;
153        idx += 8;
154    }
155    // Handle remaining bytes (zero-padded)
156    let remainder = data.len() - idx;
157    if remainder > 0 {
158        let mut buf = [0u8; 8];
159        buf[..remainder].copy_from_slice(&data[idx..]);
160        result ^= u64::from_le_bytes(buf);
161    }
162    result
163}
164
165/// Dispatch to the correct algorithm implementation.
166fn compute_checksum(data: &[u8], algorithm: ChecksumAlgorithm) -> u64 {
167    match algorithm {
168        ChecksumAlgorithm::Fnv1a64 => fnv1a64(data),
169        ChecksumAlgorithm::Adler32 => adler32(data),
170        ChecksumAlgorithm::Fletcher16 => fletcher16(data),
171        ChecksumAlgorithm::XorFold => xor_fold(data),
172    }
173}
174
175// ──────────────────────────────────────────────────────────────────────────────
176// TensorChecksum
177// ──────────────────────────────────────────────────────────────────────────────
178
179/// A checksum value together with the metadata needed to re-verify data later.
180#[derive(Clone, Debug, PartialEq, Eq)]
181pub struct TensorChecksum {
182    /// Algorithm used to produce `value`.
183    pub algorithm: ChecksumAlgorithm,
184    /// The checksum value.
185    pub value: u64,
186    /// Length (in bytes) of the data that was checksummed.
187    pub data_len: usize,
188    /// Unix timestamp (seconds) at which the checksum was computed.
189    pub computed_at_secs: u64,
190}
191
192impl TensorChecksum {
193    /// Recompute the checksum for `data` using the same algorithm and compare
194    /// it against `self.value`.
195    ///
196    /// Returns `true` if the data is intact, `false` if it has been corrupted
197    /// (or if `data.len() != self.data_len`).
198    ///
199    /// # Examples
200    ///
201    /// ```
202    /// use ipfrs_tensorlogic::tensor_checksum::{ChecksumAlgorithm, TensorChecksum};
203    ///
204    /// let data = b"tensor payload";
205    /// let cs = TensorChecksum {
206    ///     algorithm: ChecksumAlgorithm::Fnv1a64,
207    ///     value: ipfrs_tensorlogic::tensor_checksum::fnv1a64(data),
208    ///     data_len: data.len(),
209    ///     computed_at_secs: 0,
210    /// };
211    /// assert!(cs.verify(data));
212    /// assert!(!cs.verify(b"corrupted payload"));
213    /// ```
214    pub fn verify(&self, data: &[u8]) -> bool {
215        if data.len() != self.data_len {
216            return false;
217        }
218        compute_checksum(data, self.algorithm) == self.value
219    }
220}
221
222// ──────────────────────────────────────────────────────────────────────────────
223// ChecksumRecord
224// ──────────────────────────────────────────────────────────────────────────────
225
226/// Associates a [`TensorChecksum`] with a specific tensor and layer name.
227#[derive(Clone, Debug, PartialEq, Eq)]
228pub struct ChecksumRecord {
229    /// Unique identifier of the tensor.
230    pub tensor_id: u64,
231    /// The checksum for this tensor's data.
232    pub checksum: TensorChecksum,
233    /// Human-readable name of the layer that owns this tensor.
234    pub layer_name: String,
235}
236
237impl ChecksumRecord {
238    /// Returns `true` if `data` matches the stored checksum.
239    ///
240    /// Delegates to [`TensorChecksum::verify`].
241    pub fn is_valid(&self, data: &[u8]) -> bool {
242        self.checksum.verify(data)
243    }
244}
245
246// ──────────────────────────────────────────────────────────────────────────────
247// ChecksumEngineStats
248// ──────────────────────────────────────────────────────────────────────────────
249
250/// Aggregate statistics for a [`TensorChecksumEngine`] instance.
251#[derive(Clone, Debug, Default, PartialEq, Eq)]
252pub struct ChecksumEngineStats {
253    /// Total number of checksums computed via [`TensorChecksumEngine::compute`].
254    pub total_computed: u64,
255    /// Total number of verifications attempted via [`TensorChecksumEngine::verify`].
256    pub total_verified: u64,
257    /// Total number of failed verifications (data mismatch or unknown id).
258    pub total_failures: u64,
259}
260
261impl ChecksumEngineStats {
262    /// Fraction of verifications that resulted in a failure.
263    ///
264    /// Returns `0.0` when `total_verified == 0` to avoid division by zero.
265    ///
266    /// # Examples
267    ///
268    /// ```
269    /// use ipfrs_tensorlogic::tensor_checksum::ChecksumEngineStats;
270    ///
271    /// let stats = ChecksumEngineStats {
272    ///     total_computed: 10,
273    ///     total_verified: 4,
274    ///     total_failures: 1,
275    /// };
276    /// assert!((stats.failure_rate() - 0.25).abs() < 1e-9);
277    /// ```
278    pub fn failure_rate(&self) -> f64 {
279        if self.total_verified == 0 {
280            return 0.0;
281        }
282        self.total_failures as f64 / self.total_verified as f64
283    }
284}
285
286// ──────────────────────────────────────────────────────────────────────────────
287// TensorChecksumEngine
288// ──────────────────────────────────────────────────────────────────────────────
289
290/// Stateful engine that manages per-tensor checksum records.
291///
292/// # Thread Safety
293///
294/// `TensorChecksumEngine` is **not** `Send`/`Sync` by default because it owns
295/// a `HashMap`. Wrap in `Arc<Mutex<…>>` or `parking_lot::Mutex` for shared
296/// access across threads.
297///
298/// # Examples
299///
300/// ```
301/// use ipfrs_tensorlogic::tensor_checksum::{ChecksumAlgorithm, TensorChecksumEngine};
302///
303/// let mut engine = TensorChecksumEngine::new();
304/// let data = b"layer weights";
305///
306/// engine.compute(42, "fc1".to_string(), data, ChecksumAlgorithm::Adler32, 1_000_000);
307///
308/// assert_eq!(engine.verify(42, data), Some(true));
309/// assert_eq!(engine.verify(99, data), None); // unknown id
310///
311/// assert!(engine.remove(42));
312/// assert!(!engine.remove(42)); // already gone
313/// ```
314pub struct TensorChecksumEngine {
315    /// Per-tensor checksum records, keyed by `tensor_id`.
316    pub records: HashMap<u64, ChecksumRecord>,
317    /// Cumulative operational statistics.
318    pub stats: ChecksumEngineStats,
319}
320
321impl TensorChecksumEngine {
322    /// Create a new, empty engine with zeroed statistics.
323    pub fn new() -> Self {
324        Self {
325            records: HashMap::new(),
326            stats: ChecksumEngineStats::default(),
327        }
328    }
329
330    /// Compute a checksum for `data`, store the resulting [`ChecksumRecord`],
331    /// increment `stats.total_computed`, and return a reference to the record.
332    ///
333    /// If a record already exists for `tensor_id`, it is replaced.
334    ///
335    /// # Parameters
336    ///
337    /// - `tensor_id`  — Unique identifier for the tensor.
338    /// - `layer_name` — Human-readable layer name (e.g. `"encoder.layer1"`).
339    /// - `data`       — Raw bytes of the tensor payload.
340    /// - `algorithm`  — Checksum algorithm to use.
341    /// - `now_secs`   — Current time as Unix seconds (caller-supplied for
342    ///   determinism in tests and embedded environments).
343    pub fn compute(
344        &mut self,
345        tensor_id: u64,
346        layer_name: String,
347        data: &[u8],
348        algorithm: ChecksumAlgorithm,
349        now_secs: u64,
350    ) -> &ChecksumRecord {
351        let value = compute_checksum(data, algorithm);
352        let record = ChecksumRecord {
353            tensor_id,
354            checksum: TensorChecksum {
355                algorithm,
356                value,
357                data_len: data.len(),
358                computed_at_secs: now_secs,
359            },
360            layer_name,
361        };
362        self.records.insert(tensor_id, record);
363        self.stats.total_computed += 1;
364        // SAFETY: we just inserted, so the key is guaranteed present.
365        self.records.get(&tensor_id).expect("record just inserted")
366    }
367
368    /// Verify the stored checksum for `tensor_id` against `data`.
369    ///
370    /// - Returns `None` if `tensor_id` is not registered.
371    /// - Returns `Some(true)` if the data matches.
372    /// - Returns `Some(false)` if the data does not match.
373    ///
374    /// Both `total_verified` and (on failure) `total_failures` are updated.
375    pub fn verify(&mut self, tensor_id: u64, data: &[u8]) -> Option<bool> {
376        let record = self.records.get(&tensor_id)?;
377        let ok = record.is_valid(data);
378        self.stats.total_verified += 1;
379        if !ok {
380            self.stats.total_failures += 1;
381        }
382        Some(ok)
383    }
384
385    /// Remove the record for `tensor_id`.
386    ///
387    /// Returns `true` if the record existed and was removed, `false` if no
388    /// record was found for the given id.
389    pub fn remove(&mut self, tensor_id: u64) -> bool {
390        self.records.remove(&tensor_id).is_some()
391    }
392
393    /// Return a reference to the current engine statistics.
394    pub fn stats(&self) -> &ChecksumEngineStats {
395        &self.stats
396    }
397}
398
399impl Default for TensorChecksumEngine {
400    fn default() -> Self {
401        Self::new()
402    }
403}
404
405// ──────────────────────────────────────────────────────────────────────────────
406// Tests
407// ──────────────────────────────────────────────────────────────────────────────
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412
413    // ── FNV-1a known value ────────────────────────────────────────────────────
414
415    #[test]
416    fn test_fnv1a64_empty() {
417        // The FNV-1a hash of the empty byte sequence equals the offset basis.
418        assert_eq!(fnv1a64(b""), FNV_OFFSET_BASIS);
419    }
420
421    #[test]
422    fn test_fnv1a64_known_value() {
423        // Verified against reference implementation:
424        // echo -n "hello" | fnv64 --type fnv1a
425        // FNV-1a("hello") = 0xa430d84680aabd0b
426        let expected: u64 = 0xa430d84680aabd0b;
427        assert_eq!(fnv1a64(b"hello"), expected);
428    }
429
430    #[test]
431    fn test_fnv1a64_deterministic() {
432        let h1 = fnv1a64(b"tensor data XYZ");
433        let h2 = fnv1a64(b"tensor data XYZ");
434        assert_eq!(h1, h2);
435    }
436
437    // ── Adler-32 known value ──────────────────────────────────────────────────
438
439    #[test]
440    fn test_adler32_abc_known_value() {
441        // Adler-32("ABC"):
442        //   a=1,b=0 → A(65): a=66,b=66 → B(66): a=132,b=198 → C(67): a=199,b=397
443        //   result = (397 << 16) | 199 = 0x018D00C7
444        assert_eq!(adler32(b"ABC"), 0x018D00C7);
445    }
446
447    #[test]
448    fn test_adler32_empty() {
449        // Adler-32 of the empty sequence: a=1, b=0 → (0 << 16) | 1 = 1
450        assert_eq!(adler32(b""), 1);
451    }
452
453    #[test]
454    fn test_adler32_deterministic() {
455        assert_eq!(adler32(b"hello world"), adler32(b"hello world"));
456    }
457
458    // ── Fletcher-16 ───────────────────────────────────────────────────────────
459
460    #[test]
461    fn test_fletcher16_empty() {
462        assert_eq!(fletcher16(b""), 0);
463    }
464
465    #[test]
466    fn test_fletcher16_non_zero() {
467        let h = fletcher16(b"abcde");
468        assert_ne!(h, 0);
469    }
470
471    #[test]
472    fn test_fletcher16_deterministic() {
473        assert_eq!(fletcher16(b"tensor"), fletcher16(b"tensor"));
474    }
475
476    #[test]
477    fn test_fletcher16_distinguishes_inputs() {
478        // Different inputs should (in practice) give different checksums.
479        assert_ne!(fletcher16(b"aaa"), fletcher16(b"bbb"));
480    }
481
482    // ── XorFold ───────────────────────────────────────────────────────────────
483
484    #[test]
485    fn test_xor_fold_empty() {
486        assert_eq!(xor_fold(b""), 0);
487    }
488
489    #[test]
490    fn test_xor_fold_exact_chunk() {
491        // 8 bytes — one chunk, result equals that u64.
492        let data = b"ABCDEFGH";
493        let expected = u64::from_le_bytes(*data);
494        assert_eq!(xor_fold(data), expected);
495    }
496
497    #[test]
498    fn test_xor_fold_two_chunks() {
499        let a = [0x01u8; 8];
500        let b = [0x02u8; 8];
501        let mut data = [0u8; 16];
502        data[..8].copy_from_slice(&a);
503        data[8..].copy_from_slice(&b);
504        let expected = u64::from_le_bytes(a) ^ u64::from_le_bytes(b);
505        assert_eq!(xor_fold(&data), expected);
506    }
507
508    #[test]
509    fn test_xor_fold_partial_chunk_zero_padded() {
510        // 9 bytes → one full chunk XOR one partial chunk (zero-padded).
511        let data = b"ABCDEFGHI"; // 9 bytes
512        let chunk1 = u64::from_le_bytes([b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H']);
513        let chunk2 = u64::from_le_bytes([b'I', 0, 0, 0, 0, 0, 0, 0]);
514        assert_eq!(xor_fold(data), chunk1 ^ chunk2);
515    }
516
517    // ── Compute + verify round-trip ───────────────────────────────────────────
518
519    fn round_trip(algorithm: ChecksumAlgorithm) {
520        let mut engine = TensorChecksumEngine::new();
521        let data = b"round trip test data for tensor";
522        engine.compute(1, "layer".to_string(), data, algorithm, 42);
523        assert_eq!(engine.verify(1, data), Some(true));
524    }
525
526    #[test]
527    fn test_round_trip_fnv1a64() {
528        round_trip(ChecksumAlgorithm::Fnv1a64);
529    }
530
531    #[test]
532    fn test_round_trip_adler32() {
533        round_trip(ChecksumAlgorithm::Adler32);
534    }
535
536    #[test]
537    fn test_round_trip_fletcher16() {
538        round_trip(ChecksumAlgorithm::Fletcher16);
539    }
540
541    #[test]
542    fn test_round_trip_xor_fold() {
543        round_trip(ChecksumAlgorithm::XorFold);
544    }
545
546    // ── Corruption detection ──────────────────────────────────────────────────
547
548    #[test]
549    fn test_verify_detects_corruption() {
550        let mut engine = TensorChecksumEngine::new();
551        let original = b"important tensor weights";
552        let corrupted = b"corrupted tensor weights";
553        engine.compute(
554            7,
555            "output".to_string(),
556            original,
557            ChecksumAlgorithm::Fnv1a64,
558            0,
559        );
560        assert_eq!(engine.verify(7, corrupted), Some(false));
561    }
562
563    #[test]
564    fn test_verify_detects_length_change() {
565        let mut engine = TensorChecksumEngine::new();
566        let data = b"full data payload";
567        engine.compute(8, "embed".to_string(), data, ChecksumAlgorithm::Adler32, 0);
568        // Truncate the data
569        assert_eq!(engine.verify(8, &data[..5]), Some(false));
570    }
571
572    // ── Unknown tensor_id returns None ────────────────────────────────────────
573
574    #[test]
575    fn test_verify_unknown_returns_none() {
576        let mut engine = TensorChecksumEngine::new();
577        assert_eq!(engine.verify(999, b"anything"), None);
578    }
579
580    // ── Remove ────────────────────────────────────────────────────────────────
581
582    #[test]
583    fn test_remove_existing() {
584        let mut engine = TensorChecksumEngine::new();
585        engine.compute(
586            5,
587            "fc".to_string(),
588            b"data",
589            ChecksumAlgorithm::Fletcher16,
590            0,
591        );
592        assert!(engine.remove(5));
593        // After removal, verify returns None
594        assert_eq!(engine.verify(5, b"data"), None);
595    }
596
597    #[test]
598    fn test_remove_nonexistent() {
599        let mut engine = TensorChecksumEngine::new();
600        assert!(!engine.remove(404));
601    }
602
603    // ── failure_rate ──────────────────────────────────────────────────────────
604
605    #[test]
606    fn test_failure_rate_no_verifications() {
607        let stats = ChecksumEngineStats::default();
608        assert_eq!(stats.failure_rate(), 0.0);
609    }
610
611    #[test]
612    fn test_failure_rate_calculation() {
613        let stats = ChecksumEngineStats {
614            total_computed: 10,
615            total_verified: 8,
616            total_failures: 2,
617        };
618        assert!((stats.failure_rate() - 0.25).abs() < 1e-9);
619    }
620
621    // ── stats.total_failures increments ──────────────────────────────────────
622
623    #[test]
624    fn test_stats_total_failures_increments() {
625        let mut engine = TensorChecksumEngine::new();
626        let data = b"good data";
627        engine.compute(10, "attn".to_string(), data, ChecksumAlgorithm::XorFold, 0);
628
629        // Successful verification — no failure increment
630        engine.verify(10, data);
631        assert_eq!(engine.stats().total_failures, 0);
632
633        // Corrupted data — failure increments
634        engine.verify(10, b"bad data XX");
635        assert_eq!(engine.stats().total_failures, 1);
636
637        // Another failure
638        engine.verify(10, b"also bad XX");
639        assert_eq!(engine.stats().total_failures, 2);
640    }
641
642    #[test]
643    fn test_stats_total_computed_and_verified() {
644        let mut engine = TensorChecksumEngine::new();
645        let data = b"weights";
646        engine.compute(1, "l1".to_string(), data, ChecksumAlgorithm::Fnv1a64, 0);
647        engine.compute(2, "l2".to_string(), data, ChecksumAlgorithm::Adler32, 0);
648        assert_eq!(engine.stats().total_computed, 2);
649
650        engine.verify(1, data);
651        engine.verify(2, data);
652        assert_eq!(engine.stats().total_verified, 2);
653        assert_eq!(engine.stats().total_failures, 0);
654    }
655
656    // ── ChecksumRecord::is_valid delegates to TensorChecksum::verify ─────────
657
658    #[test]
659    fn test_checksum_record_is_valid() {
660        let data = b"record test payload";
661        let record = ChecksumRecord {
662            tensor_id: 99,
663            checksum: TensorChecksum {
664                algorithm: ChecksumAlgorithm::Fletcher16,
665                value: fletcher16(data),
666                data_len: data.len(),
667                computed_at_secs: 1_000,
668            },
669            layer_name: "norm".to_string(),
670        };
671        assert!(record.is_valid(data));
672        assert!(!record.is_valid(b"wrong data!!"));
673    }
674}