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}