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::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/// 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, &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        // Ensure at least 1 hasher
313        let hashers = NonZeroU8::arbitrary(u).unwrap_or(NonZeroU8::MIN);
314        // Generate u64 in u16 range to avoid OOM, then round to power of two
315        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        // Check for inserted items
363        for i in 0..10usize {
364            assert!(bf.contains(&i.to_be_bytes()));
365        }
366
367        // Check for non-inserted items and count false positives
368        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        // A small bloom filter with many items will have some false positives.
376        // The exact number is probabilistic, but it should not be zero and not all should be FPs.
377        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        // Too large
411        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        // Too small
422        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        // Wrong bit count
440        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        // Non-power-of-2 bits
449        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        // Empty filter should have 0 estimated count and FP rate
466        assert_eq!(bf.estimated_count(), BigRational::zero());
467        assert_eq!(bf.estimated_false_positive_rate(), BigRational::zero());
468
469        // Insert some items
470        for i in 0..100usize {
471            bf.insert(&i.to_be_bytes());
472        }
473
474        // Estimated count should be reasonably close to 100
475        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        // FP rate should be non-zero after insertions
481        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        // Create a filter for 1000 items with 1% false positive rate
489        let fp_rate = BigRational::from_frac_u64(1, 100);
490        let mut bf = BloomFilter::<Sha256>::with_rate(NZUsize!(1000), fp_rate.clone());
491
492        // Verify getters return expected values
493        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        // Insert 1000 items
499        for i in 0..1000usize {
500            bf.insert(&i.to_be_bytes());
501        }
502
503        // All inserted items should be found
504        for i in 0..1000usize {
505            assert!(bf.contains(&i.to_be_bytes()));
506        }
507
508        // Count false positives on non-inserted items
509        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        // With 1% target FP rate, we expect around 10 false positives out of 1000
517        // Allow some variance (should be well under 2%)
518        assert!(false_positives < 20);
519    }
520
521    #[cfg(feature = "std")]
522    #[test]
523    fn test_optimal_hashers() {
524        // For 1000 items in 10000 bits, optimal k = (10000/1000) * ln(2) = 6.93
525        // Integer math truncates to 6
526        let k = BloomFilter::<Sha256>::optimal_hashers(1000, 10000);
527        assert_eq!(k.get(), 6);
528
529        // For 100 items in 1000 bits, optimal k = (1000/100) * ln(2) = 6.93
530        // Integer math truncates to 6
531        let k = BloomFilter::<Sha256>::optimal_hashers(100, 1000);
532        assert_eq!(k.get(), 6);
533
534        // Edge case: very few bits per item, clamped to 1
535        let k = BloomFilter::<Sha256>::optimal_hashers(1000, 100);
536        assert_eq!(k.get(), 1);
537
538        // Edge case: many bits per item, clamped to 16
539        let k = BloomFilter::<Sha256>::optimal_hashers(100, 100000);
540        assert_eq!(k.get(), 16);
541
542        // Edge case: zero items returns 1
543        let k = BloomFilter::<Sha256>::optimal_hashers(0, 1000);
544        assert_eq!(k.get(), 1);
545
546        // Edge case: extreme values that would overflow (n << 16 wraps to 0 for n >= 2^48)
547        // Should not panic, should return clamped value
548        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        // For 1000 items with 1% FP rate
558        // Formula: m = -n * ln(p) / (ln(2))^2 = -1000 * ln(0.01) / 0.4804 = 9585
559        // Rounded to next power of 2 = 16384
560        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        // For 10000 items with 0.001% FP rate (need significantly more bits)
566        // Formula: m = -10000 * ln(0.00001) / 0.4804 = 239627
567        // Rounded to next power of 2 = 262144
568        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        // Very large expected_items
581        let bits = BloomFilter::<Sha256>::optimal_bits(usize::MAX / 2, &fp_001pct);
582        assert!(bits.is_power_of_two());
583        assert!(bits > 0);
584
585        // Large but reasonable values
586        let bits = BloomFilter::<Sha256>::optimal_bits(1_000_000_000, &fp_001pct);
587        assert!(bits.is_power_of_two());
588
589        // Zero items
590        let bits = BloomFilter::<Sha256>::optimal_bits(0, &fp_1pct);
591        assert!(bits.is_power_of_two());
592        assert_eq!(bits, 1); // 0 * bpe rounds up to 1
593    }
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        // For 1000 items at 1% FP rate
609        // m = -1000 * log2(0.01) / ln(2) = 9585
610        // Rounded to power of 2 = 16384
611        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}