commonware_cryptography/bloomfilter/
mod.rs1#[cfg(all(test, feature = "arbitrary"))]
4mod conformance;
5
6use crate::{Hasher, sha256::Sha256};
7use bytes::{Buf, BufMut};
8use commonware_codec::{
9 EncodeSize, FixedSize,
10 codec::{Read, Write},
11 error::Error as CodecError,
12};
13use commonware_utils::bitmap::BitMap;
14use core::{
15 marker::PhantomData,
16 num::{NonZeroU8, NonZeroU64, NonZeroUsize},
17};
18#[cfg(feature = "std")]
19use {
20 commonware_utils::rational::BigRationalExt,
21 num_rational::BigRational,
22 num_traits::{One, ToPrimitive, Zero},
23};
24
25#[cfg(feature = "std")]
27const LN2: (u64, u64) = (14397, 20769);
28
29#[cfg(feature = "std")]
31const LN2_INV: (u64, u64) = (29145, 20201);
32
33#[derive(Clone, Debug)]
60pub struct BloomFilter<H: Hasher = Sha256> {
61 hashers: NonZeroU8,
62 bits: BitMap,
63 _marker: PhantomData<H>,
64}
65
66impl<H: Hasher> PartialEq for BloomFilter<H> {
67 fn eq(&self, other: &Self) -> bool {
68 self.hashers == other.hashers && self.bits == other.bits
69 }
70}
71
72impl<H: Hasher> Eq for BloomFilter<H> {}
73
74impl<H: Hasher> BloomFilter<H> {
75 const _ASSERT_DIGEST_AT_LEAST_16_BYTES: () = assert!(
77 <H::Digest as FixedSize>::SIZE >= 16,
78 "digest must be at least 128 bits (16 bytes)"
79 );
80
81 pub fn new(hashers: NonZeroU8, bits: NonZeroUsize) -> Self {
86 let bits = bits
87 .get()
88 .checked_next_power_of_two()
89 .unwrap_or(1 << (usize::BITS - 1));
90 Self {
91 hashers,
92 bits: BitMap::zeroes(bits as u64),
93 _marker: PhantomData,
94 }
95 }
96
97 #[cfg(feature = "std")]
111 pub fn with_rate(expected_items: NonZeroUsize, fp_rate: BigRational) -> Self {
112 let bits = Self::optimal_bits(expected_items.get(), &fp_rate);
113 let hashers = Self::optimal_hashers(expected_items.get(), bits);
114 Self {
115 hashers,
116 bits: BitMap::zeroes(bits as u64),
117 _marker: PhantomData,
118 }
119 }
120
121 pub const fn hashers(&self) -> NonZeroU8 {
123 self.hashers
124 }
125
126 pub const fn bits(&self) -> NonZeroUsize {
128 NonZeroUsize::new(self.bits.len() as usize).expect("bits is never zero")
129 }
130
131 fn indices(&self, item: &[u8]) -> impl Iterator<Item = u64> + use<H> {
133 #[allow(path_statements)]
134 Self::_ASSERT_DIGEST_AT_LEAST_16_BYTES;
135
136 let digest = H::hash(&[item]);
138 let h1 = u64::from_be_bytes(digest[0..8].try_into().unwrap());
139 let mut h2 = u64::from_be_bytes(digest[8..16].try_into().unwrap());
140
141 h2 |= 1;
144
145 let hashers = self.hashers.get() as u64;
149 let mask = self.bits.len() - 1;
150 (0..hashers).map(move |hasher| h1.wrapping_add(hasher.wrapping_mul(h2)) & mask)
151 }
152
153 pub fn insert(&mut self, item: &[u8]) {
155 let indices = self.indices(item);
156 for index in indices {
157 self.bits.set(index, true);
158 }
159 }
160
161 pub fn contains(&self, item: &[u8]) -> bool {
165 let indices = self.indices(item);
166 for index in indices {
167 if !self.bits.get(index) {
168 return false;
169 }
170 }
171 true
172 }
173
174 #[cfg(feature = "std")]
181 pub fn estimated_false_positive_rate(&self) -> BigRational {
182 let ones = self.bits.count_ones();
183 let len = self.bits.len();
184 let fill_ratio = BigRational::new(ones.into(), len.into());
185 fill_ratio.pow(self.hashers.get() as i32)
186 }
187
188 #[cfg(feature = "std")]
195 pub fn estimated_count(&self) -> BigRational {
196 let m = self.bits.len();
197 let x = self.bits.count_ones();
198 let k = self.hashers.get() as u64;
199 if x >= m {
200 return BigRational::from_usize(usize::MAX);
201 }
202
203 let one_minus_fill = BigRational::new((m - x).into(), m.into());
205 let log2_val = one_minus_fill.log2_floor(16);
206 let ln2 = BigRational::from_frac_u64(LN2.0, LN2.1);
207 let ln_result = &log2_val * &ln2;
208
209 let m_over_k = BigRational::new(m.into(), k.into());
211 -m_over_k * ln_result
212 }
213
214 #[cfg(feature = "std")]
219 pub fn optimal_hashers(expected_items: usize, bits: usize) -> NonZeroU8 {
220 if expected_items == 0 {
221 return NonZeroU8::MIN;
222 }
223
224 let ln2 = BigRational::from_frac_u64(LN2.0, LN2.1);
226 let k_ratio = BigRational::from_usize(bits) * ln2 / BigRational::from_usize(expected_items);
227 let hashers = k_ratio.to_integer().to_u8().unwrap_or(16).clamp(1, 16);
228 NonZeroU8::new(hashers).expect("clamped to at least 1")
229 }
230
231 #[cfg(feature = "std")]
243 pub fn optimal_bits(expected_items: usize, fp_rate: &BigRational) -> usize {
244 assert!(
245 fp_rate > &BigRational::zero() && fp_rate < &BigRational::one(),
246 "false positive rate must be in (0, 1)"
247 );
248
249 let log2_p = fp_rate.log2_floor(16);
252
253 let n = BigRational::from_usize(expected_items);
256 let ln2_inv = BigRational::from_frac_u64(LN2_INV.0, LN2_INV.1);
257 let bits_rational = -(&n * &log2_p * &ln2_inv);
258
259 let raw = bits_rational.ceil_to_u128().unwrap_or(1) as usize;
260 raw.max(1)
261 .checked_next_power_of_two()
262 .unwrap_or(1 << (usize::BITS - 1))
263 }
264}
265
266impl<H: Hasher> Write for BloomFilter<H> {
267 fn write(&self, buf: &mut impl BufMut) {
268 self.hashers.get().write(buf);
269 self.bits.write(buf);
270 }
271}
272
273impl<H: Hasher> Read for BloomFilter<H> {
274 type Cfg = (NonZeroU8, NonZeroU64);
276
277 fn read_cfg(
278 buf: &mut impl Buf,
279 (hashers_cfg, bits_cfg): &Self::Cfg,
280 ) -> Result<Self, CodecError> {
281 if !bits_cfg.get().is_power_of_two() {
282 return Err(CodecError::Invalid(
283 "BloomFilter",
284 "bits must be a power of 2",
285 ));
286 }
287 let hashers = u8::read_cfg(buf, &())?;
288 if hashers != hashers_cfg.get() {
289 return Err(CodecError::Invalid(
290 "BloomFilter",
291 "hashers doesn't match config",
292 ));
293 }
294 let bits = BitMap::read_cfg(buf, &bits_cfg.get())?;
295 if bits.len() != bits_cfg.get() {
296 return Err(CodecError::Invalid(
297 "BloomFilter",
298 "bitmap length doesn't match config",
299 ));
300 }
301 Ok(Self {
302 hashers: *hashers_cfg,
303 bits,
304 _marker: PhantomData,
305 })
306 }
307}
308
309impl<H: Hasher> EncodeSize for BloomFilter<H> {
310 fn encode_size(&self) -> usize {
311 self.hashers.get().encode_size() + self.bits.encode_size()
312 }
313}
314
315#[cfg(feature = "arbitrary")]
316impl<H: Hasher> arbitrary::Arbitrary<'_> for BloomFilter<H> {
317 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
318 let hashers = NonZeroU8::arbitrary(u).unwrap_or(NonZeroU8::MIN);
320 let bits_len = u.int_in_range(0..=u16::MAX as u64)?.next_power_of_two();
322 let mut bits = BitMap::with_capacity(bits_len);
323 for _ in 0..bits_len {
324 bits.push(u.arbitrary::<bool>()?);
325 }
326 Ok(Self {
327 hashers,
328 bits,
329 _marker: PhantomData,
330 })
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use commonware_codec::{Decode, Encode};
338 use commonware_utils::{NZU8, NZU64, NZUsize};
339
340 #[test]
341 fn test_insert_and_contains() {
342 let mut bf = BloomFilter::<Sha256>::new(NZU8!(10), NZUsize!(1000));
343 let item1 = b"hello";
344 let item2 = b"world";
345 let item3 = b"bloomfilter";
346
347 bf.insert(item1);
348 bf.insert(item2);
349
350 assert!(bf.contains(item1));
351 assert!(bf.contains(item2));
352 assert!(!bf.contains(item3));
353 }
354
355 #[test]
356 fn test_empty() {
357 let bf = BloomFilter::<Sha256>::new(NZU8!(5), NZUsize!(100));
358 assert!(!bf.contains(b"anything"));
359 }
360
361 #[test]
362 fn test_false_positives() {
363 let mut bf = BloomFilter::<Sha256>::new(NZU8!(10), NZUsize!(100));
364 for i in 0..10usize {
365 bf.insert(&i.to_be_bytes());
366 }
367
368 for i in 0..10usize {
370 assert!(bf.contains(&i.to_be_bytes()));
371 }
372
373 let mut false_positives = 0;
375 for i in 100..1100usize {
376 if bf.contains(&i.to_be_bytes()) {
377 false_positives += 1;
378 }
379 }
380
381 assert!(false_positives > 0);
384 assert!(false_positives < 1000);
385 }
386
387 #[test]
388 fn test_codec_roundtrip() {
389 let mut bf = BloomFilter::<Sha256>::new(NZU8!(5), NZUsize!(128));
390 bf.insert(b"test1");
391 bf.insert(b"test2");
392
393 let cfg = (NZU8!(5), NZU64!(128));
394
395 let encoded = bf.encode();
396 let decoded = BloomFilter::<Sha256>::decode_cfg(encoded, &cfg).unwrap();
397
398 assert_eq!(bf, decoded);
399 }
400
401 #[test]
402 fn test_codec_empty() {
403 let bf = BloomFilter::<Sha256>::new(NZU8!(4), NZUsize!(128));
404 let cfg = (NZU8!(4), NZU64!(128));
405 let encoded = bf.encode();
406 let decoded = BloomFilter::<Sha256>::decode_cfg(encoded, &cfg).unwrap();
407 assert_eq!(bf, decoded);
408 }
409
410 #[test]
411 fn test_codec_with_invalid_hashers() {
412 let mut bf = BloomFilter::<Sha256>::new(NZU8!(5), NZUsize!(128));
413 bf.insert(b"test1");
414 let encoded = bf.encode();
415
416 let cfg = (NZU8!(10), NZU64!(128));
418 let decoded = BloomFilter::<Sha256>::decode_cfg(encoded.clone(), &cfg);
419 assert!(matches!(
420 decoded,
421 Err(CodecError::Invalid(
422 "BloomFilter",
423 "hashers doesn't match config"
424 ))
425 ));
426
427 let cfg = (NZU8!(4), NZU64!(128));
429 let decoded = BloomFilter::<Sha256>::decode_cfg(encoded, &cfg);
430 assert!(matches!(
431 decoded,
432 Err(CodecError::Invalid(
433 "BloomFilter",
434 "hashers doesn't match config"
435 ))
436 ));
437 }
438
439 #[test]
440 fn test_codec_with_invalid_bits() {
441 let mut bf = BloomFilter::<Sha256>::new(NZU8!(5), NZUsize!(128));
442 bf.insert(b"test1");
443 let encoded = bf.encode();
444
445 let cfg = (NZU8!(5), NZU64!(64));
447 let result = BloomFilter::<Sha256>::decode_cfg(encoded.clone(), &cfg);
448 assert!(matches!(result, Err(CodecError::InvalidLength(128))));
449
450 let cfg = (NZU8!(5), NZU64!(256));
451 let result = BloomFilter::<Sha256>::decode_cfg(encoded.clone(), &cfg);
452 assert!(matches!(
453 result,
454 Err(CodecError::Invalid(
455 "BloomFilter",
456 "bitmap length doesn't match config"
457 ))
458 ));
459
460 let cfg = (NZU8!(5), NZU64!(100));
462 let result = BloomFilter::<Sha256>::decode_cfg(encoded, &cfg);
463 assert!(matches!(
464 result,
465 Err(CodecError::Invalid(
466 "BloomFilter",
467 "bits must be a power of 2"
468 ))
469 ));
470 }
471
472 #[test]
473 fn test_statistics() {
474 let mut bf = BloomFilter::<Sha256>::new(NZU8!(7), NZUsize!(1024));
475
476 assert_eq!(bf.estimated_count(), BigRational::zero());
478 assert_eq!(bf.estimated_false_positive_rate(), BigRational::zero());
479
480 for i in 0..100usize {
482 bf.insert(&i.to_be_bytes());
483 }
484
485 let estimated = bf.estimated_count();
487 let lower = BigRational::from_usize(75);
488 let upper = BigRational::from_usize(125);
489 assert!(estimated > lower && estimated < upper);
490
491 assert!(bf.estimated_false_positive_rate() > BigRational::zero());
493 assert!(bf.estimated_false_positive_rate() < BigRational::one());
494 }
495
496 #[test]
497 fn test_with_rate() {
498 let fp_rate = BigRational::from_frac_u64(1, 100);
500 let mut bf = BloomFilter::<Sha256>::with_rate(NZUsize!(1000), fp_rate.clone());
501
502 let expected_bits = BloomFilter::<Sha256>::optimal_bits(1000, &fp_rate);
504 let expected_hashers = BloomFilter::<Sha256>::optimal_hashers(1000, expected_bits);
505 assert_eq!(bf.bits().get(), expected_bits);
506 assert_eq!(bf.hashers(), expected_hashers);
507
508 for i in 0..1000usize {
510 bf.insert(&i.to_be_bytes());
511 }
512
513 for i in 0..1000usize {
515 assert!(bf.contains(&i.to_be_bytes()));
516 }
517
518 let mut false_positives = 0;
520 for i in 1000..2000usize {
521 if bf.contains(&i.to_be_bytes()) {
522 false_positives += 1;
523 }
524 }
525
526 assert!(false_positives < 20);
529 }
530
531 #[test]
532 fn test_optimal_hashers() {
533 let k = BloomFilter::<Sha256>::optimal_hashers(1000, 10000);
536 assert_eq!(k.get(), 6);
537
538 let k = BloomFilter::<Sha256>::optimal_hashers(100, 1000);
541 assert_eq!(k.get(), 6);
542
543 let k = BloomFilter::<Sha256>::optimal_hashers(1000, 100);
545 assert_eq!(k.get(), 1);
546
547 let k = BloomFilter::<Sha256>::optimal_hashers(100, 100000);
549 assert_eq!(k.get(), 16);
550
551 let k = BloomFilter::<Sha256>::optimal_hashers(0, 1000);
553 assert_eq!(k.get(), 1);
554
555 let k = BloomFilter::<Sha256>::optimal_hashers(1 << 48, 1000);
558 assert_eq!(k.get(), 1);
559 let k = BloomFilter::<Sha256>::optimal_hashers(usize::MAX, usize::MAX);
560 assert!((1..=16).contains(&k.get()));
561 }
562
563 #[test]
564 fn test_optimal_bits() {
565 let fp_1pct = BigRational::from_frac_u64(1, 100);
569 let bits = BloomFilter::<Sha256>::optimal_bits(1000, &fp_1pct);
570 assert_eq!(bits, 16384);
571 assert!(bits.is_power_of_two());
572
573 let fp_001pct = BigRational::from_frac_u64(1, 100_000);
577 let bits_lower_fp = BloomFilter::<Sha256>::optimal_bits(10000, &fp_001pct);
578 assert_eq!(bits_lower_fp, 262144);
579 assert!(bits_lower_fp.is_power_of_two());
580 }
581
582 #[test]
583 fn test_bits_extreme_values() {
584 let fp_001pct = BigRational::from_frac_u64(1, 10_000);
585 let fp_1pct = BigRational::from_frac_u64(1, 100);
586
587 let bits = BloomFilter::<Sha256>::optimal_bits(usize::MAX / 2, &fp_001pct);
589 assert!(bits.is_power_of_two());
590 assert!(bits > 0);
591
592 let bits = BloomFilter::<Sha256>::optimal_bits(1_000_000_000, &fp_001pct);
594 assert!(bits.is_power_of_two());
595
596 let bits = BloomFilter::<Sha256>::optimal_bits(0, &fp_1pct);
598 assert!(bits.is_power_of_two());
599 assert_eq!(bits, 1); }
601
602 #[test]
603 fn test_with_rate_deterministic() {
604 let fp_rate = BigRational::from_frac_u64(1, 100);
605 let bf1 = BloomFilter::<Sha256>::with_rate(NZUsize!(1000), fp_rate.clone());
606 let bf2 = BloomFilter::<Sha256>::with_rate(NZUsize!(1000), fp_rate);
607 assert_eq!(bf1.bits(), bf2.bits());
608 assert_eq!(bf1.hashers(), bf2.hashers());
609 }
610
611 #[test]
612 fn test_optimal_bits_matches_formula() {
613 let fp_rate = BigRational::from_frac_u64(1, 100);
617 let bits = BloomFilter::<Sha256>::optimal_bits(1000, &fp_rate);
618 assert_eq!(bits, 16384);
619 }
620}