commonware_cryptography/bloomfilter/
mod.rs1#[cfg(all(test, feature = "arbitrary"))]
4mod conformance;
5
6use crate::{Hasher, sha256::Sha256};
7use bytes::BufMut;
8use commonware_codec::{
9 Buf, EncodeSize, FixedSize, RangeCfg,
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, &RangeCfg::exact(bits_cfg.get()))?;
295 Ok(Self {
296 hashers: *hashers_cfg,
297 bits,
298 _marker: PhantomData,
299 })
300 }
301}
302
303impl<H: Hasher> EncodeSize for BloomFilter<H> {
304 fn encode_size(&self) -> usize {
305 self.hashers.get().encode_size() + self.bits.encode_size()
306 }
307}
308
309#[cfg(feature = "arbitrary")]
310impl<H: Hasher> arbitrary::Arbitrary<'_> for BloomFilter<H> {
311 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
312 let hashers = NonZeroU8::arbitrary(u).unwrap_or(NonZeroU8::MIN);
314 let bits_len = u.int_in_range(0..=u16::MAX as u64)?.next_power_of_two();
316 let mut bits = BitMap::with_capacity(bits_len);
317 for _ in 0..bits_len {
318 bits.push(u.arbitrary::<bool>()?);
319 }
320 Ok(Self {
321 hashers,
322 bits,
323 _marker: PhantomData,
324 })
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use commonware_codec::{Decode, Encode};
332 use commonware_utils::{NZU8, NZU64, NZUsize};
333
334 #[test]
335 fn test_insert_and_contains() {
336 let mut bf = BloomFilter::<Sha256>::new(NZU8!(10), NZUsize!(1000));
337 let item1 = b"hello";
338 let item2 = b"world";
339 let item3 = b"bloomfilter";
340
341 bf.insert(item1);
342 bf.insert(item2);
343
344 assert!(bf.contains(item1));
345 assert!(bf.contains(item2));
346 assert!(!bf.contains(item3));
347 }
348
349 #[test]
350 fn test_empty() {
351 let bf = BloomFilter::<Sha256>::new(NZU8!(5), NZUsize!(100));
352 assert!(!bf.contains(b"anything"));
353 }
354
355 #[test]
356 fn test_false_positives() {
357 let mut bf = BloomFilter::<Sha256>::new(NZU8!(10), NZUsize!(100));
358 for i in 0..10usize {
359 bf.insert(&i.to_be_bytes());
360 }
361
362 for i in 0..10usize {
364 assert!(bf.contains(&i.to_be_bytes()));
365 }
366
367 let mut false_positives = 0;
369 for i in 100..1100usize {
370 if bf.contains(&i.to_be_bytes()) {
371 false_positives += 1;
372 }
373 }
374
375 assert!(false_positives > 0);
378 assert!(false_positives < 1000);
379 }
380
381 #[test]
382 fn test_codec_roundtrip() {
383 let mut bf = BloomFilter::<Sha256>::new(NZU8!(5), NZUsize!(128));
384 bf.insert(b"test1");
385 bf.insert(b"test2");
386
387 let cfg = (NZU8!(5), NZU64!(128));
388
389 let encoded = bf.encode();
390 let decoded = BloomFilter::<Sha256>::decode_cfg(encoded, &cfg).unwrap();
391
392 assert_eq!(bf, decoded);
393 }
394
395 #[test]
396 fn test_codec_empty() {
397 let bf = BloomFilter::<Sha256>::new(NZU8!(4), NZUsize!(128));
398 let cfg = (NZU8!(4), NZU64!(128));
399 let encoded = bf.encode();
400 let decoded = BloomFilter::<Sha256>::decode_cfg(encoded, &cfg).unwrap();
401 assert_eq!(bf, decoded);
402 }
403
404 #[test]
405 fn test_codec_with_invalid_hashers() {
406 let mut bf = BloomFilter::<Sha256>::new(NZU8!(5), NZUsize!(128));
407 bf.insert(b"test1");
408 let encoded = bf.encode();
409
410 let cfg = (NZU8!(10), NZU64!(128));
412 let decoded = BloomFilter::<Sha256>::decode_cfg(encoded.clone(), &cfg);
413 assert!(matches!(
414 decoded,
415 Err(CodecError::Invalid(
416 "BloomFilter",
417 "hashers doesn't match config"
418 ))
419 ));
420
421 let cfg = (NZU8!(4), NZU64!(128));
423 let decoded = BloomFilter::<Sha256>::decode_cfg(encoded, &cfg);
424 assert!(matches!(
425 decoded,
426 Err(CodecError::Invalid(
427 "BloomFilter",
428 "hashers doesn't match config"
429 ))
430 ));
431 }
432
433 #[test]
434 fn test_codec_with_invalid_bits() {
435 let mut bf = BloomFilter::<Sha256>::new(NZU8!(5), NZUsize!(128));
436 bf.insert(b"test1");
437 let encoded = bf.encode();
438
439 let cfg = (NZU8!(5), NZU64!(64));
441 let result = BloomFilter::<Sha256>::decode_cfg(encoded.clone(), &cfg);
442 assert!(matches!(result, Err(CodecError::InvalidLength(128))));
443
444 let cfg = (NZU8!(5), NZU64!(256));
445 let result = BloomFilter::<Sha256>::decode_cfg(encoded.clone(), &cfg);
446 assert!(matches!(result, Err(CodecError::InvalidLength(128))));
447
448 let cfg = (NZU8!(5), NZU64!(100));
450 let result = BloomFilter::<Sha256>::decode_cfg(encoded, &cfg);
451 assert!(matches!(
452 result,
453 Err(CodecError::Invalid(
454 "BloomFilter",
455 "bits must be a power of 2"
456 ))
457 ));
458 }
459
460 #[cfg(feature = "std")]
461 #[test]
462 fn test_statistics() {
463 let mut bf = BloomFilter::<Sha256>::new(NZU8!(7), NZUsize!(1024));
464
465 assert_eq!(bf.estimated_count(), BigRational::zero());
467 assert_eq!(bf.estimated_false_positive_rate(), BigRational::zero());
468
469 for i in 0..100usize {
471 bf.insert(&i.to_be_bytes());
472 }
473
474 let estimated = bf.estimated_count();
476 let lower = BigRational::from_usize(75);
477 let upper = BigRational::from_usize(125);
478 assert!(estimated > lower && estimated < upper);
479
480 assert!(bf.estimated_false_positive_rate() > BigRational::zero());
482 assert!(bf.estimated_false_positive_rate() < BigRational::one());
483 }
484
485 #[cfg(feature = "std")]
486 #[test]
487 fn test_with_rate() {
488 let fp_rate = BigRational::from_frac_u64(1, 100);
490 let mut bf = BloomFilter::<Sha256>::with_rate(NZUsize!(1000), fp_rate.clone());
491
492 let expected_bits = BloomFilter::<Sha256>::optimal_bits(1000, &fp_rate);
494 let expected_hashers = BloomFilter::<Sha256>::optimal_hashers(1000, expected_bits);
495 assert_eq!(bf.bits().get(), expected_bits);
496 assert_eq!(bf.hashers(), expected_hashers);
497
498 for i in 0..1000usize {
500 bf.insert(&i.to_be_bytes());
501 }
502
503 for i in 0..1000usize {
505 assert!(bf.contains(&i.to_be_bytes()));
506 }
507
508 let mut false_positives = 0;
510 for i in 1000..2000usize {
511 if bf.contains(&i.to_be_bytes()) {
512 false_positives += 1;
513 }
514 }
515
516 assert!(false_positives < 20);
519 }
520
521 #[cfg(feature = "std")]
522 #[test]
523 fn test_optimal_hashers() {
524 let k = BloomFilter::<Sha256>::optimal_hashers(1000, 10000);
527 assert_eq!(k.get(), 6);
528
529 let k = BloomFilter::<Sha256>::optimal_hashers(100, 1000);
532 assert_eq!(k.get(), 6);
533
534 let k = BloomFilter::<Sha256>::optimal_hashers(1000, 100);
536 assert_eq!(k.get(), 1);
537
538 let k = BloomFilter::<Sha256>::optimal_hashers(100, 100000);
540 assert_eq!(k.get(), 16);
541
542 let k = BloomFilter::<Sha256>::optimal_hashers(0, 1000);
544 assert_eq!(k.get(), 1);
545
546 let k = BloomFilter::<Sha256>::optimal_hashers(1 << 48, 1000);
549 assert_eq!(k.get(), 1);
550 let k = BloomFilter::<Sha256>::optimal_hashers(usize::MAX, usize::MAX);
551 assert!((1..=16).contains(&k.get()));
552 }
553
554 #[cfg(feature = "std")]
555 #[test]
556 fn test_optimal_bits() {
557 let fp_1pct = BigRational::from_frac_u64(1, 100);
561 let bits = BloomFilter::<Sha256>::optimal_bits(1000, &fp_1pct);
562 assert_eq!(bits, 16384);
563 assert!(bits.is_power_of_two());
564
565 let fp_001pct = BigRational::from_frac_u64(1, 100_000);
569 let bits_lower_fp = BloomFilter::<Sha256>::optimal_bits(10000, &fp_001pct);
570 assert_eq!(bits_lower_fp, 262144);
571 assert!(bits_lower_fp.is_power_of_two());
572 }
573
574 #[cfg(feature = "std")]
575 #[test]
576 fn test_bits_extreme_values() {
577 let fp_001pct = BigRational::from_frac_u64(1, 10_000);
578 let fp_1pct = BigRational::from_frac_u64(1, 100);
579
580 let bits = BloomFilter::<Sha256>::optimal_bits(usize::MAX / 2, &fp_001pct);
582 assert!(bits.is_power_of_two());
583 assert!(bits > 0);
584
585 let bits = BloomFilter::<Sha256>::optimal_bits(1_000_000_000, &fp_001pct);
587 assert!(bits.is_power_of_two());
588
589 let bits = BloomFilter::<Sha256>::optimal_bits(0, &fp_1pct);
591 assert!(bits.is_power_of_two());
592 assert_eq!(bits, 1); }
594
595 #[cfg(feature = "std")]
596 #[test]
597 fn test_with_rate_deterministic() {
598 let fp_rate = BigRational::from_frac_u64(1, 100);
599 let bf1 = BloomFilter::<Sha256>::with_rate(NZUsize!(1000), fp_rate.clone());
600 let bf2 = BloomFilter::<Sha256>::with_rate(NZUsize!(1000), fp_rate);
601 assert_eq!(bf1.bits(), bf2.bits());
602 assert_eq!(bf1.hashers(), bf2.hashers());
603 }
604
605 #[cfg(feature = "std")]
606 #[test]
607 fn test_optimal_bits_matches_formula() {
608 let fp_rate = BigRational::from_frac_u64(1, 100);
612 let bits = BloomFilter::<Sha256>::optimal_bits(1000, &fp_rate);
613 assert_eq!(bits, 16384);
614 }
615}