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}