Skip to main content

commonware_cryptography/lthash/
mod.rs

1//! A homomorphic hash function that enables efficient incremental updates.
2//!
3//! [LtHash] is an additive homomorphic hash function over [crate::Blake3], meaning that the
4//! hash of a sum equals the sum of the hashes: `H(a + b) = H(a) + H(b)`. This useful property
5//! enables the efficient addition or removal of elements from some hashed set without recomputing
6//! the entire hash from scratch. This unlocks the ability to compare set equality without revealing
7//! the entire set or requiring items be added in a specific order.
8//!
9//! # Properties
10//!
11//! - **Homomorphic**: Supports addition and subtraction of hashes (H(a ± b) = H(a) ± H(b))
12//! - **Commutative**: Operation order doesn't matter (H(a) + H(b) = H(b) + H(a))
13//! - **Incremental**: Update existing hashes in O(1) time instead of rehashing everything
14//!
15//! _If your application requires a (probabilistic) membership check, consider using
16//! [crate::BloomFilter] instead._
17//!
18//! # Security
19//!
20//! [LtHash]'s state consists of 1024 16-bit unsigned integers (2048 bytes), as recommended in
21//! "Securing Update Propagation with Homomorphic Hashing". This provides (by their estimates) at
22//! least 200 bits of security.
23//!
24//! # Warning
25//!
26//! This construction has a known vulnerability: adding the same element 2^16 times
27//! will cause overflow and result in the same hash as not adding it at all. For
28//! applications where this is a concern, consider adding unique metadata (like indices
29//! or timestamps) to each element.
30//!
31//! # Example
32//!
33//! ```rust
34//! use commonware_cryptography::lthash::LtHash;
35//!
36//! // Demonstrate the homomorphic property
37//! let mut lthash = LtHash::new();
38//!
39//! // Add elements to our set
40//! lthash.add(b"alice");
41//! lthash.add(b"bob");
42//! lthash.add(b"charlie");
43//!
44//! // Remove an element (homomorphic subtraction)
45//! lthash.subtract(b"bob");
46//!
47//! // This is equivalent to just adding alice and charlie
48//! let mut lthash2 = LtHash::new();
49//! lthash2.add(b"alice");
50//! lthash2.add(b"charlie");
51//!
52//! assert_eq!(lthash.checksum(), lthash2.checksum());
53//!
54//! // Order doesn't matter (commutative property)
55//! let mut lthash3 = LtHash::new();
56//! lthash3.add(b"charlie");
57//! lthash3.add(b"alice");
58//!
59//! assert_eq!(lthash2.checksum(), lthash3.checksum());
60//! ```
61//!
62//! # Acknowledgements
63//!
64//! The following resources were used as references when implementing this crate:
65//!
66//! * <https://cseweb.ucsd.edu/~daniele/papers/IncHash.html>: A new paradigm for collision-free hashing: Incrementality at reduced cost
67//! * <https://cseweb.ucsd.edu/~mihir/papers/inc1.pdf>: Incremental Cryptography: The Case of Hashing and Signing
68//! * <https://cseweb.ucsd.edu/~daniele/papers/Cyclic.pdf>: Generalized compact knapsacks, cyclic lattices, and efficient one-way functions
69//! * <https://dl.acm.org/doi/10.1145/237814.237838>: Generating hard instances of lattice problems
70//! * <https://eprint.iacr.org/2019/227>: Securing Update Propagation with Homomorphic Hashing
71//! * <https://engineering.fb.com/2019/03/01/security/homomorphic-hashing/>: Open-sourcing homomorphic hashing to secure update propagation
72//! * <https://github.com/facebook/folly/blob/main/folly/crypto/LtHash.cpp>: An open-source C++ library developed and used at Facebook.
73//! * <https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0215-accounts-lattice-hash.md>: Homomorphic Hashing of Account State
74
75use crate::{
76    Hasher as _,
77    blake3::{Blake3, CoreBlake3, Digest},
78};
79use bytes::{Buf, BufMut};
80use commonware_codec::{Error as CodecError, FixedSize, Read, ReadExt, Write};
81
82/// Size of the internal [LtHash] state in bytes.
83const LTHASH_SIZE: usize = 2048;
84
85/// Number of 16-bit integers in the [LtHash] state.
86const LTHASH_ELEMENTS: usize = LTHASH_SIZE / 2; // each u16 is 2 bytes
87
88/// An additive homomorphic hash function over [crate::Blake3].
89#[derive(Debug, Clone)]
90#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
91pub struct LtHash {
92    /// Internal state as 1024 16-bit unsigned integers
93    state: [u16; LTHASH_ELEMENTS],
94}
95
96impl LtHash {
97    /// Create a new [LtHash] instance with zero state.
98    pub const fn new() -> Self {
99        Self {
100            state: [0u16; LTHASH_ELEMENTS],
101        }
102    }
103
104    /// Add data.
105    ///
106    /// The order of additions doesn't matter. Each element is expanded to 1024 16-bit
107    /// integers and added component-wise with modular arithmetic (mod 2^16).
108    pub fn add(&mut self, data: &[u8]) {
109        // Hash the input data to expand it to LTHASH_ELEMENTS u16s
110        let expanded = Self::expand_to_state(data);
111
112        // Add the expanded hash to our state with 16-bit wrapping arithmetic
113        for (i, val) in expanded.iter().enumerate() {
114            self.state[i] = self.state[i].wrapping_add(*val);
115        }
116    }
117
118    /// Subtract data.
119    ///
120    /// This allows removing previously added data from the hash state. Uses 16-bit
121    /// modular subtraction.
122    pub fn subtract(&mut self, data: &[u8]) {
123        // Hash the input data to expand it to LTHASH_ELEMENTS u16s
124        let expanded = Self::expand_to_state(data);
125
126        // Subtract the expanded hash from our state with 16-bit wrapping arithmetic
127        for (i, val) in expanded.iter().enumerate() {
128            self.state[i] = self.state[i].wrapping_sub(*val);
129        }
130    }
131
132    /// Combine two [LtHash] states by addition.
133    pub fn combine(&mut self, other: &Self) {
134        for (i, val) in other.state.iter().enumerate() {
135            self.state[i] = self.state[i].wrapping_add(*val);
136        }
137    }
138
139    /// Return the [Digest] of the current state.
140    pub fn checksum(&self) -> Digest {
141        let mut bytes = [0u8; LTHASH_SIZE];
142        for (chunk, val) in bytes.as_chunks_mut::<2>().0.iter_mut().zip(&self.state) {
143            *chunk = val.to_le_bytes();
144        }
145        Blake3::hash(&[&bytes])
146    }
147
148    /// Reset the [LtHash] to the initial zero state.
149    pub const fn reset(&mut self) {
150        self.state = [0u16; LTHASH_ELEMENTS];
151    }
152
153    /// Check if the [LtHash] is in the zero state.
154    pub fn is_zero(&self) -> bool {
155        self.state.iter().all(|&val| val == 0)
156    }
157
158    /// Expand input data to an array of u16s using [Blake3] as an XOF.
159    fn expand_to_state(data: &[u8]) -> [u16; LTHASH_ELEMENTS] {
160        let mut result = [0u16; LTHASH_ELEMENTS];
161        let mut bytes = [0u8; LTHASH_SIZE];
162
163        // Use Blake3 in XOF mode to expand the data to LTHASH_SIZE bytes
164        let mut hasher = CoreBlake3::new();
165        hasher.update(data);
166        let mut output_reader = hasher.finalize_xof();
167        output_reader.fill(&mut bytes);
168
169        // Convert bytes to u16 array using little-endian interpretation
170        for (i, chunk) in bytes.chunks(2).enumerate() {
171            result[i] = u16::from_le_bytes([chunk[0], chunk[1]]);
172        }
173
174        result
175    }
176}
177
178impl Default for LtHash {
179    fn default() -> Self {
180        Self::new()
181    }
182}
183
184impl Write for LtHash {
185    fn write(&self, buf: &mut impl BufMut) {
186        for &val in &self.state {
187            val.write(buf);
188        }
189    }
190}
191
192impl Read for LtHash {
193    type Cfg = ();
194
195    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
196        let mut state = [0u16; LTHASH_ELEMENTS];
197        for val in state.iter_mut() {
198            *val = u16::read(buf)?;
199        }
200        Ok(Self { state })
201    }
202}
203
204impl FixedSize for LtHash {
205    const SIZE: usize = LTHASH_SIZE;
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::Hasher;
212
213    #[test]
214    fn test_new() {
215        let lthash = LtHash::new();
216        assert!(lthash.is_zero());
217    }
218
219    #[test]
220    fn test_add() {
221        let mut lthash = LtHash::new();
222        lthash.add(b"hello");
223        assert!(!lthash.is_zero());
224    }
225
226    #[test]
227    fn test_commutativity() {
228        // Test that a + b = b + a
229        let mut lthash1 = LtHash::new();
230        lthash1.add(b"hello");
231        lthash1.add(b"world");
232        let hash1 = lthash1.checksum();
233
234        let mut lthash2 = LtHash::new();
235        lthash2.add(b"world");
236        lthash2.add(b"hello");
237        let hash2 = lthash2.checksum();
238
239        assert_eq!(hash1, hash2);
240    }
241
242    #[test]
243    fn test_associativity() {
244        // Test that (a + b) + c = a + (b + c)
245        let mut lthash1 = LtHash::new();
246        lthash1.add(b"a");
247        lthash1.add(b"b");
248        lthash1.add(b"c");
249        let hash1 = lthash1.checksum();
250
251        let mut lthash2 = LtHash::new();
252        let mut temp = LtHash::new();
253        temp.add(b"b");
254        temp.add(b"c");
255        lthash2.add(b"a");
256        lthash2.combine(&temp);
257        let hash2 = lthash2.checksum();
258
259        assert_eq!(hash1, hash2);
260    }
261
262    #[test]
263    fn test_subtraction() {
264        // Test that (a + b) - b = a
265        let mut lthash1 = LtHash::new();
266        lthash1.add(b"hello");
267        let hash1 = lthash1.checksum();
268
269        let mut lthash2 = LtHash::new();
270        lthash2.add(b"hello");
271        lthash2.add(b"world");
272        lthash2.subtract(b"world");
273        let hash2 = lthash2.checksum();
274
275        assert_eq!(hash1, hash2);
276    }
277
278    #[test]
279    fn test_empty() {
280        let lthash = LtHash::new();
281        let empty_hash = lthash.checksum();
282
283        // Empty state should produce the hash of all zero u16s in little-endian
284        let mut hasher = Blake3::default();
285        for _ in 0..LTHASH_ELEMENTS {
286            hasher.update(&0u16.to_le_bytes());
287        }
288        let (_, expected) = hasher.finalize();
289
290        assert_eq!(empty_hash, expected);
291    }
292
293    #[test]
294    fn test_reset() {
295        let mut lthash = LtHash::new();
296        lthash.add(b"hello");
297        assert!(!lthash.is_zero());
298
299        lthash.reset();
300        assert!(lthash.is_zero());
301    }
302
303    #[test]
304    fn test_deterministic() {
305        let mut lthash = LtHash::new();
306        lthash.add(b"test");
307
308        let mut lthash2 = LtHash::new();
309        lthash2.add(b"test");
310        assert_eq!(lthash.checksum(), lthash2.checksum());
311    }
312
313    #[test]
314    fn test_large_data() {
315        let mut lthash = LtHash::new();
316        let large_data = vec![0xAB; 10000];
317        lthash.add(&large_data);
318        lthash.checksum();
319    }
320
321    #[test]
322    fn test_snake() {
323        let mut lthash1 = LtHash::new();
324        for i in 0..100u32 {
325            lthash1.add(&i.to_le_bytes());
326        }
327        let hash1 = lthash1.checksum();
328
329        // Add in reverse order
330        let mut lthash2 = LtHash::new();
331        for i in (0..100u32).rev() {
332            lthash2.add(&i.to_le_bytes());
333        }
334        let hash2 = lthash2.checksum();
335
336        // Should be equal due to commutativity
337        assert_eq!(hash1, hash2);
338    }
339
340    #[test]
341    fn test_codec() {
342        let mut lthash = LtHash::new();
343        lthash.add(b"hello");
344        let hash = lthash.checksum();
345
346        let mut buf = Vec::new();
347        lthash.write(&mut buf);
348        let lthash2 = LtHash::read_cfg(&mut &buf[..], &()).unwrap();
349        let hash2 = lthash2.checksum();
350        assert_eq!(hash, hash2);
351    }
352
353    #[cfg(feature = "arbitrary")]
354    mod conformance {
355        use super::*;
356        use commonware_codec::conformance::CodecConformance;
357
358        commonware_conformance::conformance_tests! {
359            CodecConformance<LtHash>,
360        }
361    }
362}