Skip to main content

khive_types/
hash.rs

1//! 256-bit content hash for checkpoint integrity verification.
2//!
3//! # Formal proof reference
4//!
5//! `proofs/Retrieval/HNSW.lean` — hash identity used in checkpoint
6//! compatibility checks (khive.Retrieval.HNSW.checkpoint_correctness).
7
8use core::fmt;
9
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13/// 256-bit (32-byte) content hash.
14///
15/// Used as a content-addressed identifier for HNSW checkpoints and other
16/// snapshot artifacts. The underlying algorithm is caller-defined; the type
17/// carries the raw bytes without encoding assumptions.
18#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
19#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
20#[cfg_attr(feature = "serde", serde(transparent))]
21pub struct Hash32([u8; 32]);
22
23impl Hash32 {
24    /// Zero hash (nil value).
25    pub const ZERO: Self = Self([0u8; 32]);
26
27    /// Construct from raw bytes.
28    #[inline]
29    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
30        Self(bytes)
31    }
32
33    /// Return the raw byte representation.
34    #[inline]
35    pub const fn as_bytes(&self) -> &[u8; 32] {
36        &self.0
37    }
38
39    /// Compute a BLAKE3 hash over the given byte slice.
40    ///
41    /// Requires the `blake3` feature.
42    #[cfg(feature = "blake3")]
43    #[inline]
44    pub fn from_blake3(data: &[u8]) -> Self {
45        let hash = blake3::hash(data);
46        Self(*hash.as_bytes())
47    }
48
49    /// Constant-time equality check.
50    ///
51    /// Accumulates XOR over all 32 bytes without early exit so the comparison
52    /// takes the same number of iterations regardless of where bytes differ.
53    /// Suitable for integrity comparisons where timing side-channels are a
54    /// concern.  The `#[inline(never)]` attribute discourages the compiler from
55    /// inlining and optimising away the full-loop traversal.
56    #[inline(never)]
57    pub fn eq_ct(&self, other: &Self) -> bool {
58        let diff = self
59            .0
60            .iter()
61            .zip(other.0.iter())
62            .fold(0u8, |acc, (a, b)| acc | (a ^ b));
63        diff == 0
64    }
65}
66
67impl From<[u8; 32]> for Hash32 {
68    #[inline]
69    fn from(bytes: [u8; 32]) -> Self {
70        Self(bytes)
71    }
72}
73
74impl fmt::Debug for Hash32 {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        write!(f, "Hash32(")?;
77        for b in &self.0 {
78            write!(f, "{b:02x}")?;
79        }
80        write!(f, ")")
81    }
82}
83
84impl fmt::Display for Hash32 {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        for b in &self.0 {
87            write!(f, "{b:02x}")?;
88        }
89        Ok(())
90    }
91}