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}