//! A homomorphic hash function that enables efficient incremental updates. //! //! [LtHash] is an additive homomorphic hash function over [crate::Blake3], meaning that the //! hash of a sum equals the sum of the hashes: `H(a + b) = H(a) + H(b)`. This useful property //! enables the efficient addition or removal of elements from some hashed set without recomputing //! the entire hash from scratch. This unlocks the ability to compare set equality without revealing //! the entire set or requiring items be added in a specific order. //! //! # Properties //! //! - **Homomorphic**: Supports addition and subtraction of hashes (H(a ± b) = H(a) ± H(b)) //! - **Commutative**: Operation order doesn't matter (H(a) + H(b) = H(b) + H(a)) //! - **Incremental**: Update existing hashes in O(1) time instead of rehashing everything //! //! _If your application requires a (probabilistic) membership check, consider using //! [crate::BloomFilter] instead._ //! //! # Security //! //! [LtHash]'s state consists of 1024 16-bit unsigned integers (2048 bytes), as recommended in //! "Securing Update Propagation with Homomorphic Hashing". This provides (by their estimates) at //! least 200 bits of security. //! //! # Warning //! //! This construction has a known vulnerability: adding the same element 2^16 times //! will cause overflow and result in the same hash as not adding it at all. For //! applications where this is a concern, consider adding unique metadata (like indices //! or timestamps) to each element. //! //! # Example //! //! ```rust //! use commonware_cryptography::lthash::LtHash; //! //! // Demonstrate the homomorphic property //! let mut lthash = LtHash::new(); //! //! // Add elements to our set //! lthash.add(b"alice"); //! lthash.add(b"bob"); //! lthash.add(b"charlie"); //! //! // Remove an element (homomorphic subtraction) //! lthash.subtract(b"bob"); //! //! // This is equivalent to just adding alice and charlie //! let mut lthash2 = LtHash::new(); //! lthash2.add(b"alice"); //! lthash2.add(b"charlie"); //! //! assert_eq!(lthash.checksum(), lthash2.checksum()); //! //! // Order doesn't matter (commutative property) //! let mut lthash3 = LtHash::new(); //! lthash3.add(b"charlie"); //! lthash3.add(b"alice"); //! //! assert_eq!(lthash2.checksum(), lthash3.checksum()); //! ``` //! //! # Acknowledgements //! //! The following resources were used as references when implementing this crate: //! //! * : A new paradigm for collision-free hashing: Incrementality at reduced cost //! * : Incremental Cryptography: The Case of Hashing and Signing //! * : Generalized compact knapsacks, cyclic lattices, and efficient one-way functions //! * : Generating hard instances of lattice problems //! * : Securing Update Propagation with Homomorphic Hashing //! * : Open-sourcing homomorphic hashing to secure update propagation //! * : An open-source C++ library developed and used at Facebook. //! * : Homomorphic Hashing of Account State use crate::{ blake3::{Blake3, CoreBlake3, Digest}, Hasher as _, }; use bytes::{Buf, BufMut}; use commonware_codec::{Error as CodecError, FixedSize, Read, ReadExt, Write}; /// Size of the internal [LtHash] state in bytes. const LTHASH_SIZE: usize = 2048; /// Number of 16-bit integers in the [LtHash] state. const LTHASH_ELEMENTS: usize = LTHASH_SIZE / 2; // each u16 is 2 bytes /// An additive homomorphic hash function over [crate::Blake3]. #[derive(Clone)] pub struct LtHash { /// Internal state as 1024 16-bit unsigned integers state: [u16; LTHASH_ELEMENTS], } impl LtHash { /// Create a new [LtHash] instance with zero state. pub fn new() -> Self { Self { state: [0u16; LTHASH_ELEMENTS], } } /// Add data. /// /// The order of additions doesn't matter. Each element is expanded to 1024 16-bit /// integers and added component-wise with modular arithmetic (mod 2^16). pub fn add(&mut self, data: &[u8]) { // Hash the input data to expand it to LTHASH_ELEMENTS u16s let expanded = Self::expand_to_state(data); // Add the expanded hash to our state with 16-bit wrapping arithmetic for (i, val) in expanded.iter().enumerate() { self.state[i] = self.state[i].wrapping_add(*val); } } /// Subtract data. /// /// This allows removing previously added data from the hash state. Uses 16-bit /// modular subtraction. pub fn subtract(&mut self, data: &[u8]) { // Hash the input data to expand it to LTHASH_ELEMENTS u16s let expanded = Self::expand_to_state(data); // Subtract the expanded hash from our state with 16-bit wrapping arithmetic for (i, val) in expanded.iter().enumerate() { self.state[i] = self.state[i].wrapping_sub(*val); } } /// Combine two [LtHash] states by addition. pub fn combine(&mut self, other: &Self) { for (i, val) in other.state.iter().enumerate() { self.state[i] = self.state[i].wrapping_add(*val); } } /// Return the [Digest] of the current state. pub fn checksum(&self) -> Digest { let mut hasher = Blake3::new(); // Convert u16 array to bytes in little-endian order for &val in &self.state { hasher.update(&val.to_le_bytes()); } hasher.finalize() } /// Reset the [LtHash] to the initial zero state. pub fn reset(&mut self) { self.state = [0u16; LTHASH_ELEMENTS]; } /// Check if the [LtHash] is in the zero state. pub fn is_zero(&self) -> bool { self.state.iter().all(|&val| val == 0) } /// Expand input data to an array of u16s using [Blake3] as an XOF. fn expand_to_state(data: &[u8]) -> [u16; LTHASH_ELEMENTS] { let mut result = [0u16; LTHASH_ELEMENTS]; let mut bytes = [0u8; LTHASH_SIZE]; // Use Blake3 in XOF mode to expand the data to LTHASH_SIZE bytes let mut hasher = CoreBlake3::new(); hasher.update(data); let mut output_reader = hasher.finalize_xof(); output_reader.fill(&mut bytes); // Convert bytes to u16 array using little-endian interpretation for (i, chunk) in bytes.chunks(2).enumerate() { result[i] = u16::from_le_bytes([chunk[0], chunk[1]]); } result } } impl Default for LtHash { fn default() -> Self { Self::new() } } impl Write for LtHash { fn write(&self, buf: &mut impl BufMut) { for &val in &self.state { val.write(buf); } } } impl Read for LtHash { type Cfg = (); fn read_cfg(buf: &mut impl Buf, _: &()) -> Result { let mut state = [0u16; LTHASH_ELEMENTS]; for val in state.iter_mut() { *val = u16::read(buf)?; } Ok(Self { state }) } } impl FixedSize for LtHash { const SIZE: usize = LTHASH_SIZE; } #[cfg(test)] mod tests { use super::*; use crate::Hasher; #[test] fn test_new() { let lthash = LtHash::new(); assert!(lthash.is_zero()); } #[test] fn test_add() { let mut lthash = LtHash::new(); lthash.add(b"hello"); assert!(!lthash.is_zero()); } #[test] fn test_commutativity() { // Test that a + b = b + a let mut lthash1 = LtHash::new(); lthash1.add(b"hello"); lthash1.add(b"world"); let hash1 = lthash1.checksum(); let mut lthash2 = LtHash::new(); lthash2.add(b"world"); lthash2.add(b"hello"); let hash2 = lthash2.checksum(); assert_eq!(hash1, hash2); } #[test] fn test_associativity() { // Test that (a + b) + c = a + (b + c) let mut lthash1 = LtHash::new(); lthash1.add(b"a"); lthash1.add(b"b"); lthash1.add(b"c"); let hash1 = lthash1.checksum(); let mut lthash2 = LtHash::new(); let mut temp = LtHash::new(); temp.add(b"b"); temp.add(b"c"); lthash2.add(b"a"); lthash2.combine(&temp); let hash2 = lthash2.checksum(); assert_eq!(hash1, hash2); } #[test] fn test_subtraction() { // Test that (a + b) - b = a let mut lthash1 = LtHash::new(); lthash1.add(b"hello"); let hash1 = lthash1.checksum(); let mut lthash2 = LtHash::new(); lthash2.add(b"hello"); lthash2.add(b"world"); lthash2.subtract(b"world"); let hash2 = lthash2.checksum(); assert_eq!(hash1, hash2); } #[test] fn test_empty() { let lthash = LtHash::new(); let empty_hash = lthash.checksum(); // Empty state should produce the hash of all zero u16s in little-endian let mut hasher = Blake3::new(); for _ in 0..LTHASH_ELEMENTS { hasher.update(&0u16.to_le_bytes()); } let expected = hasher.finalize(); assert_eq!(empty_hash, expected); } #[test] fn test_reset() { let mut lthash = LtHash::new(); lthash.add(b"hello"); assert!(!lthash.is_zero()); lthash.reset(); assert!(lthash.is_zero()); } #[test] fn test_deterministic() { let mut lthash = LtHash::new(); lthash.add(b"test"); let mut lthash2 = LtHash::new(); lthash2.add(b"test"); assert_eq!(lthash.checksum(), lthash2.checksum()); } #[test] fn test_large_data() { let mut lthash = LtHash::new(); let large_data = vec![0xAB; 10000]; lthash.add(&large_data); lthash.checksum(); } #[test] fn test_snake() { let mut lthash1 = LtHash::new(); for i in 0..100u32 { lthash1.add(&i.to_le_bytes()); } let hash1 = lthash1.checksum(); // Add in reverse order let mut lthash2 = LtHash::new(); for i in (0..100u32).rev() { lthash2.add(&i.to_le_bytes()); } let hash2 = lthash2.checksum(); // Should be equal due to commutativity assert_eq!(hash1, hash2); } #[test] fn test_codec() { let mut lthash = LtHash::new(); lthash.add(b"hello"); let hash = lthash.checksum(); let mut buf = Vec::new(); lthash.write(&mut buf); let lthash2 = LtHash::read_cfg(&mut &buf[..], &()).unwrap(); let hash2 = lthash2.checksum(); assert_eq!(hash, hash2); } }