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}