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    blake3::{Blake3, CoreBlake3, Digest},
77    Hasher as _,
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 hasher = Blake3::new();
142
143        // Convert u16 array to bytes in little-endian order
144        for &val in &self.state {
145            hasher.update(&val.to_le_bytes());
146        }
147
148        hasher.finalize()
149    }
150
151    /// Reset the [LtHash] to the initial zero state.
152    pub const fn reset(&mut self) {
153        self.state = [0u16; LTHASH_ELEMENTS];
154    }
155
156    /// Check if the [LtHash] is in the zero state.
157    pub fn is_zero(&self) -> bool {
158        self.state.iter().all(|&val| val == 0)
159    }
160
161    /// Expand input data to an array of u16s using [Blake3] as an XOF.
162    fn expand_to_state(data: &[u8]) -> [u16; LTHASH_ELEMENTS] {
163        let mut result = [0u16; LTHASH_ELEMENTS];
164        let mut bytes = [0u8; LTHASH_SIZE];
165
166        // Use Blake3 in XOF mode to expand the data to LTHASH_SIZE bytes
167        let mut hasher = CoreBlake3::new();
168        hasher.update(data);
169        let mut output_reader = hasher.finalize_xof();
170        output_reader.fill(&mut bytes);
171
172        // Convert bytes to u16 array using little-endian interpretation
173        for (i, chunk) in bytes.chunks(2).enumerate() {
174            result[i] = u16::from_le_bytes([chunk[0], chunk[1]]);
175        }
176
177        result
178    }
179}
180
181impl Default for LtHash {
182    fn default() -> Self {
183        Self::new()
184    }
185}
186
187impl Write for LtHash {
188    fn write(&self, buf: &mut impl BufMut) {
189        for &val in &self.state {
190            val.write(buf);
191        }
192    }
193}
194
195impl Read for LtHash {
196    type Cfg = ();
197
198    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
199        let mut state = [0u16; LTHASH_ELEMENTS];
200        for val in state.iter_mut() {
201            *val = u16::read(buf)?;
202        }
203        Ok(Self { state })
204    }
205}
206
207impl FixedSize for LtHash {
208    const SIZE: usize = LTHASH_SIZE;
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use crate::Hasher;
215
216    #[test]
217    fn test_new() {
218        let lthash = LtHash::new();
219        assert!(lthash.is_zero());
220    }
221
222    #[test]
223    fn test_add() {
224        let mut lthash = LtHash::new();
225        lthash.add(b"hello");
226        assert!(!lthash.is_zero());
227    }
228
229    #[test]
230    fn test_commutativity() {
231        // Test that a + b = b + a
232        let mut lthash1 = LtHash::new();
233        lthash1.add(b"hello");
234        lthash1.add(b"world");
235        let hash1 = lthash1.checksum();
236
237        let mut lthash2 = LtHash::new();
238        lthash2.add(b"world");
239        lthash2.add(b"hello");
240        let hash2 = lthash2.checksum();
241
242        assert_eq!(hash1, hash2);
243    }
244
245    #[test]
246    fn test_associativity() {
247        // Test that (a + b) + c = a + (b + c)
248        let mut lthash1 = LtHash::new();
249        lthash1.add(b"a");
250        lthash1.add(b"b");
251        lthash1.add(b"c");
252        let hash1 = lthash1.checksum();
253
254        let mut lthash2 = LtHash::new();
255        let mut temp = LtHash::new();
256        temp.add(b"b");
257        temp.add(b"c");
258        lthash2.add(b"a");
259        lthash2.combine(&temp);
260        let hash2 = lthash2.checksum();
261
262        assert_eq!(hash1, hash2);
263    }
264
265    #[test]
266    fn test_subtraction() {
267        // Test that (a + b) - b = a
268        let mut lthash1 = LtHash::new();
269        lthash1.add(b"hello");
270        let hash1 = lthash1.checksum();
271
272        let mut lthash2 = LtHash::new();
273        lthash2.add(b"hello");
274        lthash2.add(b"world");
275        lthash2.subtract(b"world");
276        let hash2 = lthash2.checksum();
277
278        assert_eq!(hash1, hash2);
279    }
280
281    #[test]
282    fn test_empty() {
283        let lthash = LtHash::new();
284        let empty_hash = lthash.checksum();
285
286        // Empty state should produce the hash of all zero u16s in little-endian
287        let mut hasher = Blake3::new();
288        for _ in 0..LTHASH_ELEMENTS {
289            hasher.update(&0u16.to_le_bytes());
290        }
291        let expected = hasher.finalize();
292
293        assert_eq!(empty_hash, expected);
294    }
295
296    #[test]
297    fn test_reset() {
298        let mut lthash = LtHash::new();
299        lthash.add(b"hello");
300        assert!(!lthash.is_zero());
301
302        lthash.reset();
303        assert!(lthash.is_zero());
304    }
305
306    #[test]
307    fn test_deterministic() {
308        let mut lthash = LtHash::new();
309        lthash.add(b"test");
310
311        let mut lthash2 = LtHash::new();
312        lthash2.add(b"test");
313        assert_eq!(lthash.checksum(), lthash2.checksum());
314    }
315
316    #[test]
317    fn test_large_data() {
318        let mut lthash = LtHash::new();
319        let large_data = vec![0xAB; 10000];
320        lthash.add(&large_data);
321        lthash.checksum();
322    }
323
324    #[test]
325    fn test_snake() {
326        let mut lthash1 = LtHash::new();
327        for i in 0..100u32 {
328            lthash1.add(&i.to_le_bytes());
329        }
330        let hash1 = lthash1.checksum();
331
332        // Add in reverse order
333        let mut lthash2 = LtHash::new();
334        for i in (0..100u32).rev() {
335            lthash2.add(&i.to_le_bytes());
336        }
337        let hash2 = lthash2.checksum();
338
339        // Should be equal due to commutativity
340        assert_eq!(hash1, hash2);
341    }
342
343    #[test]
344    fn test_codec() {
345        let mut lthash = LtHash::new();
346        lthash.add(b"hello");
347        let hash = lthash.checksum();
348
349        let mut buf = Vec::new();
350        lthash.write(&mut buf);
351        let lthash2 = LtHash::read_cfg(&mut &buf[..], &()).unwrap();
352        let hash2 = lthash2.checksum();
353        assert_eq!(hash, hash2);
354    }
355
356    #[cfg(feature = "arbitrary")]
357    mod conformance {
358        use super::*;
359        use commonware_codec::conformance::CodecConformance;
360
361        commonware_conformance::conformance_tests! {
362            CodecConformance<LtHash>,
363        }
364    }
365}