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(Clone)]
90pub struct LtHash {
91    /// Internal state as 1024 16-bit unsigned integers
92    state: [u16; LTHASH_ELEMENTS],
93}
94
95impl LtHash {
96    /// Create a new [LtHash] instance with zero state.
97    pub fn new() -> Self {
98        Self {
99            state: [0u16; LTHASH_ELEMENTS],
100        }
101    }
102
103    /// Add data.
104    ///
105    /// The order of additions doesn't matter. Each element is expanded to 1024 16-bit
106    /// integers and added component-wise with modular arithmetic (mod 2^16).
107    pub fn add(&mut self, data: &[u8]) {
108        // Hash the input data to expand it to LTHASH_ELEMENTS u16s
109        let expanded = Self::expand_to_state(data);
110
111        // Add the expanded hash to our state with 16-bit wrapping arithmetic
112        for (i, val) in expanded.iter().enumerate() {
113            self.state[i] = self.state[i].wrapping_add(*val);
114        }
115    }
116
117    /// Subtract data.
118    ///
119    /// This allows removing previously added data from the hash state. Uses 16-bit
120    /// modular subtraction.
121    pub fn subtract(&mut self, data: &[u8]) {
122        // Hash the input data to expand it to LTHASH_ELEMENTS u16s
123        let expanded = Self::expand_to_state(data);
124
125        // Subtract the expanded hash from our state with 16-bit wrapping arithmetic
126        for (i, val) in expanded.iter().enumerate() {
127            self.state[i] = self.state[i].wrapping_sub(*val);
128        }
129    }
130
131    /// Combine two [LtHash] states by addition.
132    pub fn combine(&mut self, other: &Self) {
133        for (i, val) in other.state.iter().enumerate() {
134            self.state[i] = self.state[i].wrapping_add(*val);
135        }
136    }
137
138    /// Return the [Digest] of the current state.
139    pub fn checksum(&self) -> Digest {
140        let mut hasher = Blake3::new();
141
142        // Convert u16 array to bytes in little-endian order
143        for &val in &self.state {
144            hasher.update(&val.to_le_bytes());
145        }
146
147        hasher.finalize()
148    }
149
150    /// Reset the [LtHash] to the initial zero state.
151    pub fn reset(&mut self) {
152        self.state = [0u16; LTHASH_ELEMENTS];
153    }
154
155    /// Check if the [LtHash] is in the zero state.
156    pub fn is_zero(&self) -> bool {
157        self.state.iter().all(|&val| val == 0)
158    }
159
160    /// Expand input data to an array of u16s using [Blake3] as an XOF.
161    fn expand_to_state(data: &[u8]) -> [u16; LTHASH_ELEMENTS] {
162        let mut result = [0u16; LTHASH_ELEMENTS];
163        let mut bytes = [0u8; LTHASH_SIZE];
164
165        // Use Blake3 in XOF mode to expand the data to LTHASH_SIZE bytes
166        let mut hasher = CoreBlake3::new();
167        hasher.update(data);
168        let mut output_reader = hasher.finalize_xof();
169        output_reader.fill(&mut bytes);
170
171        // Convert bytes to u16 array using little-endian interpretation
172        for (i, chunk) in bytes.chunks(2).enumerate() {
173            result[i] = u16::from_le_bytes([chunk[0], chunk[1]]);
174        }
175
176        result
177    }
178}
179
180impl Default for LtHash {
181    fn default() -> Self {
182        Self::new()
183    }
184}
185
186impl Write for LtHash {
187    fn write(&self, buf: &mut impl BufMut) {
188        for &val in &self.state {
189            val.write(buf);
190        }
191    }
192}
193
194impl Read for LtHash {
195    type Cfg = ();
196
197    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
198        let mut state = [0u16; LTHASH_ELEMENTS];
199        for val in state.iter_mut() {
200            *val = u16::read(buf)?;
201        }
202        Ok(Self { state })
203    }
204}
205
206impl FixedSize for LtHash {
207    const SIZE: usize = LTHASH_SIZE;
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use crate::Hasher;
214
215    #[test]
216    fn test_new() {
217        let lthash = LtHash::new();
218        assert!(lthash.is_zero());
219    }
220
221    #[test]
222    fn test_add() {
223        let mut lthash = LtHash::new();
224        lthash.add(b"hello");
225        assert!(!lthash.is_zero());
226    }
227
228    #[test]
229    fn test_commutativity() {
230        // Test that a + b = b + a
231        let mut lthash1 = LtHash::new();
232        lthash1.add(b"hello");
233        lthash1.add(b"world");
234        let hash1 = lthash1.checksum();
235
236        let mut lthash2 = LtHash::new();
237        lthash2.add(b"world");
238        lthash2.add(b"hello");
239        let hash2 = lthash2.checksum();
240
241        assert_eq!(hash1, hash2);
242    }
243
244    #[test]
245    fn test_associativity() {
246        // Test that (a + b) + c = a + (b + c)
247        let mut lthash1 = LtHash::new();
248        lthash1.add(b"a");
249        lthash1.add(b"b");
250        lthash1.add(b"c");
251        let hash1 = lthash1.checksum();
252
253        let mut lthash2 = LtHash::new();
254        let mut temp = LtHash::new();
255        temp.add(b"b");
256        temp.add(b"c");
257        lthash2.add(b"a");
258        lthash2.combine(&temp);
259        let hash2 = lthash2.checksum();
260
261        assert_eq!(hash1, hash2);
262    }
263
264    #[test]
265    fn test_subtraction() {
266        // Test that (a + b) - b = a
267        let mut lthash1 = LtHash::new();
268        lthash1.add(b"hello");
269        let hash1 = lthash1.checksum();
270
271        let mut lthash2 = LtHash::new();
272        lthash2.add(b"hello");
273        lthash2.add(b"world");
274        lthash2.subtract(b"world");
275        let hash2 = lthash2.checksum();
276
277        assert_eq!(hash1, hash2);
278    }
279
280    #[test]
281    fn test_empty() {
282        let lthash = LtHash::new();
283        let empty_hash = lthash.checksum();
284
285        // Empty state should produce the hash of all zero u16s in little-endian
286        let mut hasher = Blake3::new();
287        for _ in 0..LTHASH_ELEMENTS {
288            hasher.update(&0u16.to_le_bytes());
289        }
290        let expected = hasher.finalize();
291
292        assert_eq!(empty_hash, expected);
293    }
294
295    #[test]
296    fn test_reset() {
297        let mut lthash = LtHash::new();
298        lthash.add(b"hello");
299        assert!(!lthash.is_zero());
300
301        lthash.reset();
302        assert!(lthash.is_zero());
303    }
304
305    #[test]
306    fn test_deterministic() {
307        let mut lthash = LtHash::new();
308        lthash.add(b"test");
309
310        let mut lthash2 = LtHash::new();
311        lthash2.add(b"test");
312        assert_eq!(lthash.checksum(), lthash2.checksum());
313    }
314
315    #[test]
316    fn test_large_data() {
317        let mut lthash = LtHash::new();
318        let large_data = vec![0xAB; 10000];
319        lthash.add(&large_data);
320        lthash.checksum();
321    }
322
323    #[test]
324    fn test_snake() {
325        let mut lthash1 = LtHash::new();
326        for i in 0..100u32 {
327            lthash1.add(&i.to_le_bytes());
328        }
329        let hash1 = lthash1.checksum();
330
331        // Add in reverse order
332        let mut lthash2 = LtHash::new();
333        for i in (0..100u32).rev() {
334            lthash2.add(&i.to_le_bytes());
335        }
336        let hash2 = lthash2.checksum();
337
338        // Should be equal due to commutativity
339        assert_eq!(hash1, hash2);
340    }
341
342    #[test]
343    fn test_codec() {
344        let mut lthash = LtHash::new();
345        lthash.add(b"hello");
346        let hash = lthash.checksum();
347
348        let mut buf = Vec::new();
349        lthash.write(&mut buf);
350        let lthash2 = LtHash::read_cfg(&mut &buf[..], &()).unwrap();
351        let hash2 = lthash2.checksum();
352        assert_eq!(hash, hash2);
353    }
354}