Skip to main content

commonware_cryptography/bloomfilter/
mod.rs

1//! An implementation of a [Bloom Filter](https://en.wikipedia.org/wiki/Bloom_filter).
2
3#[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/// Rational approximation of ln(2) with 6 digits of precision: 14397/20769.
26#[cfg(feature = "std")]
27const LN2: (u64, u64) = (14397, 20769);
28
29/// Rational approximation of 1/ln(2) with 6 digits of precision: 29145/20201.
30#[cfg(feature = "std")]
31const LN2_INV: (u64, u64) = (29145, 20201);
32
33/// A [Bloom Filter](https://en.wikipedia.org/wiki/Bloom_filter).
34///
35/// This implementation uses the Kirsch-Mitzenmacher optimization to derive `k` hash functions
36/// from two hash values, which are in turn derived from a single hash digest. This provides
37/// efficient hashing for [BloomFilter::insert] and [BloomFilter::contains] operations.
38///
39/// # Hasher Selection
40///
41/// The `H` type parameter specifies the hash function to use. It defaults to [Sha256].
42/// The hasher's digest must be at least 16 bytes (128 bits) long, this is enforced at
43/// compile time.
44///
45/// When choosing a hasher, consider:
46///
47/// - **Security**: If the bloom filter accepts untrusted input, use a cryptographically
48///   secure hash function to prevent attackers from crafting inputs that cause excessive
49///   collisions (degrading the filter to always return `true`).
50///
51/// - **Determinism**: If the bloom filter must produce consistent results across runs
52///   or machines (e.g. for serialization or consensus-critical applications), avoid keyed
53///   or randomized hash functions. Both [Sha256] and [Blake3](crate::blake3::Blake3)
54///   are deterministic.
55///
56/// - **Performance**: Hash function performance varies with the size of items inserted
57///   and queried. [Sha256] is faster for smaller items (up to ~2KB), while
58///   [Blake3](crate::blake3::Blake3) is faster for larger items (4KB+).
59#[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    /// Compile-time assertion that the digest is at least 16 bytes.
76    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    /// Creates a new [BloomFilter] with `hashers` hash functions and `bits` bits.
82    ///
83    /// The number of bits will be rounded up to the next power of 2. If that would
84    /// overflow, the maximum power of 2 for the platform (2^63 on 64-bit) is used.
85    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    /// Creates a new [BloomFilter] with optimal parameters for the expected number
98    /// of items and desired false positive rate.
99    ///
100    /// Uses exact rational arithmetic for full determinism across all platforms.
101    ///
102    /// # Arguments
103    ///
104    /// * `expected_items` - Number of items expected to be inserted
105    /// * `fp_rate` - False positive rate as a rational (e.g., `BigRational::from_frac_u64(1, 100)` for 1%)
106    ///
107    /// # Panics
108    ///
109    /// Panics if `fp_rate` is not in (0, 1).
110    #[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    /// Returns the number of hashers used by the filter.
122    pub const fn hashers(&self) -> NonZeroU8 {
123        self.hashers
124    }
125
126    /// Returns the number of bits used by the filter.
127    pub const fn bits(&self) -> NonZeroUsize {
128        NonZeroUsize::new(self.bits.len() as usize).expect("bits is never zero")
129    }
130
131    /// Generate `num_hashers` bit indices for a given item.
132    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        // Extract two 64-bit hash values from the digest of the item
137        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        // Ensure h2 is odd (non-zero). If h2 were 0, all k hash functions would
142        // produce the same index (h1), defeating the purpose of multiple hashers.
143        h2 |= 1;
144
145        // Generate `hashers` hashes using the Kirsch-Mitzenmacher optimization:
146        //
147        // `h_i(x) = (h1(x) + i * h2(x)) mod m`
148        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    /// Inserts an item into the [BloomFilter].
154    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    /// Checks if an item is possibly in the [BloomFilter].
162    ///
163    /// Returns `true` if the item is probably in the set, and `false` if it is definitely not.
164    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    /// Estimates the current false positive probability.
175    ///
176    /// This approximates the false positive rate as `f^k` where `f` is the fill ratio
177    /// (proportion of bits set to 1) and `k` is the number of hash functions.
178    ///
179    /// Returns a [`BigRational`] for exact representation and cross-platform determinism.
180    #[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    /// Estimates the number of items that have been inserted.
189    ///
190    /// Uses the formula `n = -(m/k) * ln(1 - x/m)` where `m` is the number of bits,
191    /// `k` is the number of hash functions, and `x` is the number of bits set to 1.
192    ///
193    /// Returns a [`BigRational`] using `log2_floor` for the logarithm computation.
194    #[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        // ln(1 - x/m) = log2(1 - x/m) * ln(2)
204        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        // n = -(m/k) * ln(1 - x/m)
210        let m_over_k = BigRational::new(m.into(), k.into());
211        -m_over_k * ln_result
212    }
213
214    /// Calculates the optimal number of hash functions for a given capacity and bit count.
215    ///
216    /// Uses [`BigRational`] for determinism. The result is clamped to [1, 16] since
217    /// beyond ~10-12 hashes provides negligible improvement while increasing CPU cost.
218    #[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        // k = (m/n) * ln(2)
225        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    /// Calculates the optimal number of bits for a given capacity and false positive rate.
232    ///
233    /// Uses exact rational arithmetic for full determinism across all platforms.
234    /// The result is rounded up to the next power of 2. If that would overflow, the maximum
235    /// power of 2 for the platform (2^63 on 64-bit) is used.
236    ///
237    /// Formula: m = -n * log2(p) / ln(2)
238    ///
239    /// # Panics
240    ///
241    /// Panics if `fp_rate` is not in (0, 1).
242    #[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        // log2(p) is negative for p < 1. Use floor to get a more negative value,
250        // which results in more bits (conservative choice to not exceed target FP rate).
251        let log2_p = fp_rate.log2_floor(16);
252
253        // m = -n * log2(p) / ln(2) = -n * log2(p) * (1/ln(2))
254        // Since log2(p) < 0 for p < 1, -log2(p) > 0
255        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    // The number of hashers and the number of bits that the bitmap must have.
275    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        // Ensure at least 1 hasher
319        let hashers = NonZeroU8::arbitrary(u).unwrap_or(NonZeroU8::MIN);
320        // Generate u64 in u16 range to avoid OOM, then round to power of two
321        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        // Check for inserted items
369        for i in 0..10usize {
370            assert!(bf.contains(&i.to_be_bytes()));
371        }
372
373        // Check for non-inserted items and count false positives
374        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        // A small bloom filter with many items will have some false positives.
382        // The exact number is probabilistic, but it should not be zero and not all should be FPs.
383        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        // Too large
417        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        // Too small
428        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        // Wrong bit count
446        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        // Non-power-of-2 bits
461        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        // Empty filter should have 0 estimated count and FP rate
477        assert_eq!(bf.estimated_count(), BigRational::zero());
478        assert_eq!(bf.estimated_false_positive_rate(), BigRational::zero());
479
480        // Insert some items
481        for i in 0..100usize {
482            bf.insert(&i.to_be_bytes());
483        }
484
485        // Estimated count should be reasonably close to 100
486        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        // FP rate should be non-zero after insertions
492        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        // Create a filter for 1000 items with 1% false positive rate
499        let fp_rate = BigRational::from_frac_u64(1, 100);
500        let mut bf = BloomFilter::<Sha256>::with_rate(NZUsize!(1000), fp_rate.clone());
501
502        // Verify getters return expected values
503        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        // Insert 1000 items
509        for i in 0..1000usize {
510            bf.insert(&i.to_be_bytes());
511        }
512
513        // All inserted items should be found
514        for i in 0..1000usize {
515            assert!(bf.contains(&i.to_be_bytes()));
516        }
517
518        // Count false positives on non-inserted items
519        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        // With 1% target FP rate, we expect around 10 false positives out of 1000
527        // Allow some variance (should be well under 2%)
528        assert!(false_positives < 20);
529    }
530
531    #[test]
532    fn test_optimal_hashers() {
533        // For 1000 items in 10000 bits, optimal k = (10000/1000) * ln(2) = 6.93
534        // Integer math truncates to 6
535        let k = BloomFilter::<Sha256>::optimal_hashers(1000, 10000);
536        assert_eq!(k.get(), 6);
537
538        // For 100 items in 1000 bits, optimal k = (1000/100) * ln(2) = 6.93
539        // Integer math truncates to 6
540        let k = BloomFilter::<Sha256>::optimal_hashers(100, 1000);
541        assert_eq!(k.get(), 6);
542
543        // Edge case: very few bits per item, clamped to 1
544        let k = BloomFilter::<Sha256>::optimal_hashers(1000, 100);
545        assert_eq!(k.get(), 1);
546
547        // Edge case: many bits per item, clamped to 16
548        let k = BloomFilter::<Sha256>::optimal_hashers(100, 100000);
549        assert_eq!(k.get(), 16);
550
551        // Edge case: zero items returns 1
552        let k = BloomFilter::<Sha256>::optimal_hashers(0, 1000);
553        assert_eq!(k.get(), 1);
554
555        // Edge case: extreme values that would overflow (n << 16 wraps to 0 for n >= 2^48)
556        // Should not panic, should return clamped value
557        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        // For 1000 items with 1% FP rate
566        // Formula: m = -n * ln(p) / (ln(2))^2 = -1000 * ln(0.01) / 0.4804 = 9585
567        // Rounded to next power of 2 = 16384
568        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        // For 10000 items with 0.001% FP rate (need significantly more bits)
574        // Formula: m = -10000 * ln(0.00001) / 0.4804 = 239627
575        // Rounded to next power of 2 = 262144
576        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        // Very large expected_items
588        let bits = BloomFilter::<Sha256>::optimal_bits(usize::MAX / 2, &fp_001pct);
589        assert!(bits.is_power_of_two());
590        assert!(bits > 0);
591
592        // Large but reasonable values
593        let bits = BloomFilter::<Sha256>::optimal_bits(1_000_000_000, &fp_001pct);
594        assert!(bits.is_power_of_two());
595
596        // Zero items
597        let bits = BloomFilter::<Sha256>::optimal_bits(0, &fp_1pct);
598        assert!(bits.is_power_of_two());
599        assert_eq!(bits, 1); // 0 * bpe rounds up to 1
600    }
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        // For 1000 items at 1% FP rate
614        // m = -1000 * log2(0.01) / ln(2) = 9585
615        // Rounded to power of 2 = 16384
616        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}