1use ipfrs_core::{Cid, Error, Result};
19use parking_lot::RwLock;
20use serde::{Deserialize, Serialize};
21use std::path::Path;
22
23const DEFAULT_FALSE_POSITIVE_RATE: f64 = 0.01;
25
26pub struct BloomFilter {
31 inner: RwLock<BloomFilterInner>,
33 config: BloomConfig,
35}
36
37#[derive(Serialize, Deserialize)]
39struct BloomFilterInner {
40 bits: Vec<u64>,
42 count: usize,
44}
45
46#[derive(Debug, Clone)]
48pub struct BloomConfig {
49 pub expected_items: usize,
51 pub false_positive_rate: f64,
53 pub num_hashes: usize,
55 pub num_bits: usize,
57}
58
59impl BloomConfig {
60 pub fn new(expected_items: usize, false_positive_rate: f64) -> Self {
62 let ln2_squared = std::f64::consts::LN_2 * std::f64::consts::LN_2;
65 let num_bits =
66 (-((expected_items as f64) * false_positive_rate.ln()) / ln2_squared).ceil() as usize;
67
68 let num_hashes =
70 ((num_bits as f64 / expected_items as f64) * std::f64::consts::LN_2).ceil() as usize;
71
72 let num_bits = num_bits.max(64);
74 let num_hashes = num_hashes.clamp(1, 16); Self {
77 expected_items,
78 false_positive_rate,
79 num_hashes,
80 num_bits,
81 }
82 }
83
84 pub fn low_memory(expected_items: usize) -> Self {
86 Self::new(expected_items, 0.05) }
88
89 pub fn high_accuracy(expected_items: usize) -> Self {
91 Self::new(expected_items, 0.001) }
93
94 #[inline]
96 pub fn memory_bytes(&self) -> usize {
97 self.num_bits.div_ceil(64) * 8
99 }
100}
101
102impl Default for BloomConfig {
103 fn default() -> Self {
104 Self::new(100_000, DEFAULT_FALSE_POSITIVE_RATE)
105 }
106}
107
108impl BloomFilter {
109 pub fn new(expected_items: usize, false_positive_rate: f64) -> Self {
115 let config = BloomConfig::new(expected_items, false_positive_rate);
116 Self::with_config(config)
117 }
118
119 pub fn with_config(config: BloomConfig) -> Self {
121 let num_u64s = config.num_bits.div_ceil(64);
122 let inner = BloomFilterInner {
123 bits: vec![0u64; num_u64s],
124 count: 0,
125 };
126 Self {
127 inner: RwLock::new(inner),
128 config,
129 }
130 }
131
132 #[inline]
134 pub fn insert_cid(&self, cid: &Cid) {
135 self.insert(&cid.to_bytes());
136 }
137
138 #[inline]
143 pub fn contains_cid(&self, cid: &Cid) -> bool {
144 self.contains(&cid.to_bytes())
145 }
146
147 pub fn insert(&self, data: &[u8]) {
149 let mut inner = self.inner.write();
150 let hashes = self.compute_hashes(data);
151
152 for hash in hashes {
153 let bit_index = hash % self.config.num_bits;
154 let word_index = bit_index / 64;
155 let bit_offset = bit_index % 64;
156 inner.bits[word_index] |= 1u64 << bit_offset;
157 }
158 inner.count += 1;
159 }
160
161 pub fn contains(&self, data: &[u8]) -> bool {
163 let inner = self.inner.read();
164 let hashes = self.compute_hashes(data);
165
166 for hash in hashes {
167 let bit_index = hash % self.config.num_bits;
168 let word_index = bit_index / 64;
169 let bit_offset = bit_index % 64;
170 if inner.bits[word_index] & (1u64 << bit_offset) == 0 {
171 return false;
172 }
173 }
174 true
175 }
176
177 fn compute_hashes(&self, data: &[u8]) -> Vec<usize> {
179 let h1 = fnv1a_hash(data);
181 let h2 = fnv1a_hash_with_seed(data, 0x811c_9dc5);
182
183 let mut hashes = Vec::with_capacity(self.config.num_hashes);
184 for i in 0..self.config.num_hashes {
185 let hash = h1.wrapping_add((i as u64).wrapping_mul(h2));
187 hashes.push(hash as usize);
188 }
189 hashes
190 }
191
192 #[inline]
194 pub fn count(&self) -> usize {
195 self.inner.read().count
196 }
197
198 pub fn fill_ratio(&self) -> f64 {
200 let inner = self.inner.read();
201 let set_bits: usize = inner.bits.iter().map(|w| w.count_ones() as usize).sum();
202 set_bits as f64 / self.config.num_bits as f64
203 }
204
205 pub fn estimated_fpr(&self) -> f64 {
207 let fill = self.fill_ratio();
208 fill.powi(self.config.num_hashes as i32)
209 }
210
211 #[inline]
213 pub fn memory_bytes(&self) -> usize {
214 self.config.memory_bytes()
215 }
216
217 pub fn clear(&self) {
219 let mut inner = self.inner.write();
220 for word in inner.bits.iter_mut() {
221 *word = 0;
222 }
223 inner.count = 0;
224 }
225
226 pub fn save_to_file(&self, path: &Path) -> Result<()> {
228 let inner = self.inner.read();
229 let data = oxicode::serde::encode_to_vec(&*inner, oxicode::config::standard())
230 .map_err(|e| Error::Serialization(format!("Failed to serialize bloom filter: {e}")))?;
231 std::fs::write(path, data)
232 .map_err(|e| Error::Storage(format!("Failed to write bloom filter: {e}")))?;
233 Ok(())
234 }
235
236 pub fn load_from_file(path: &Path, config: BloomConfig) -> Result<Self> {
238 let data = std::fs::read(path)
239 .map_err(|e| Error::Storage(format!("Failed to read bloom filter: {e}")))?;
240 let inner: BloomFilterInner =
241 oxicode::serde::decode_owned_from_slice(&data, oxicode::config::standard())
242 .map(|(v, _)| v)
243 .map_err(|e| {
244 Error::Deserialization(format!("Failed to deserialize bloom filter: {e}"))
245 })?;
246
247 let expected_words = config.num_bits.div_ceil(64);
249 if inner.bits.len() != expected_words {
250 return Err(Error::InvalidData(format!(
251 "Bloom filter size mismatch: expected {} words, got {}",
252 expected_words,
253 inner.bits.len()
254 )));
255 }
256
257 Ok(Self {
258 inner: RwLock::new(inner),
259 config,
260 })
261 }
262
263 pub fn stats(&self) -> BloomStats {
265 BloomStats {
266 count: self.count(),
267 memory_bytes: self.memory_bytes(),
268 fill_ratio: self.fill_ratio(),
269 estimated_fpr: self.estimated_fpr(),
270 num_bits: self.config.num_bits,
271 num_hashes: self.config.num_hashes,
272 }
273 }
274}
275
276#[derive(Debug, Clone)]
278pub struct BloomStats {
279 pub count: usize,
281 pub memory_bytes: usize,
283 pub fill_ratio: f64,
285 pub estimated_fpr: f64,
287 pub num_bits: usize,
289 pub num_hashes: usize,
291}
292
293#[inline]
295fn fnv1a_hash(data: &[u8]) -> u64 {
296 const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
297 const FNV_PRIME: u64 = 0x0100_0000_01b3;
298
299 let mut hash = FNV_OFFSET;
300 for &byte in data {
301 hash ^= byte as u64;
302 hash = hash.wrapping_mul(FNV_PRIME);
303 }
304 hash
305}
306
307#[inline]
309fn fnv1a_hash_with_seed(data: &[u8], seed: u64) -> u64 {
310 const FNV_PRIME: u64 = 0x0100_0000_01b3;
311
312 let mut hash = seed;
313 for &byte in data {
314 hash ^= byte as u64;
315 hash = hash.wrapping_mul(FNV_PRIME);
316 }
317 hash
318}
319
320impl BloomFilter {
326 pub fn new_with_bits(bits: usize) -> Self {
330 let rounded = bits.div_ceil(64) * 64;
332 let config = BloomConfig {
333 expected_items: 100_000,
334 false_positive_rate: 0.01,
335 num_hashes: 7,
336 num_bits: rounded,
337 };
338 Self::with_config(config)
339 }
340
341 #[inline]
343 pub fn len(&self) -> usize {
344 self.count()
345 }
346
347 #[inline]
349 pub fn is_empty(&self) -> bool {
350 self.count() == 0
351 }
352
353 #[inline]
355 pub fn is_bloom_empty(&self) -> bool {
356 self.is_empty()
357 }
358
359 #[inline]
361 pub fn bit_count(&self) -> usize {
362 self.config.num_bits
363 }
364
365 #[inline]
367 pub fn may_contain(&self, key: &[u8]) -> bool {
368 self.contains(key)
369 }
370
371 #[inline]
373 pub fn estimated_fill_ratio(&self) -> f64 {
374 self.fill_ratio()
375 }
376}
377
378#[derive(Debug, Clone)]
382pub struct BloomFilterConfig {
383 pub bits: usize,
385 pub expected_elements: usize,
387}
388
389impl Default for BloomFilterConfig {
390 fn default() -> Self {
391 Self {
392 bits: 1_048_576,
393 expected_elements: 100_000,
394 }
395 }
396}
397
398#[derive(Debug, Clone)]
402pub struct BloomSnapshot {
403 pub fill_ratio: f64,
405 pub estimated_elements: usize,
407 pub bit_count: usize,
409}
410
411pub struct CidBloomFilter {
418 inner: BloomFilter,
419 config: BloomFilterConfig,
420}
421
422impl CidBloomFilter {
423 pub fn new(config: BloomFilterConfig) -> Self {
425 let filter = BloomFilter::new_with_bits(config.bits);
426 Self {
427 inner: filter,
428 config,
429 }
430 }
431
432 pub fn default_config() -> Self {
434 Self::new(BloomFilterConfig::default())
435 }
436
437 #[inline]
439 pub fn insert_cid(&self, cid: &str) {
440 self.inner.insert(cid.as_bytes());
441 }
442
443 #[inline]
445 pub fn may_contain_cid(&self, cid: &str) -> bool {
446 self.inner.may_contain(cid.as_bytes())
447 }
448
449 pub fn snapshot(&self) -> BloomSnapshot {
451 let fill = self.inner.estimated_fill_ratio();
452 let bit_count = self.inner.bit_count();
453
454 let k = self.inner.config.num_hashes as f64;
458 let m = bit_count as f64;
459 let estimated_elements = if fill >= 1.0 {
460 usize::MAX
461 } else {
462 let est = -(m / k) * (1.0 - fill).ln();
463 est.round() as usize
464 };
465
466 BloomSnapshot {
467 fill_ratio: fill,
468 estimated_elements,
469 bit_count,
470 }
471 }
472
473 #[inline]
475 pub fn reset(&self) {
476 self.inner.clear();
477 }
478
479 #[inline]
481 pub fn inner(&self) -> &BloomFilter {
482 &self.inner
483 }
484
485 #[inline]
487 pub fn config(&self) -> &BloomFilterConfig {
488 &self.config
489 }
490}
491
492impl Default for CidBloomFilter {
493 fn default() -> Self {
494 Self::default_config()
495 }
496}
497
498use crate::traits::BlockStore;
500use async_trait::async_trait;
501use ipfrs_core::Block;
502
503pub struct BloomBlockStore<S: BlockStore> {
504 store: S,
505 filter: BloomFilter,
506}
507
508impl<S: BlockStore> BloomBlockStore<S> {
509 pub fn new(store: S, expected_items: usize, false_positive_rate: f64) -> Self {
511 Self {
512 store,
513 filter: BloomFilter::new(expected_items, false_positive_rate),
514 }
515 }
516
517 pub fn with_config(store: S, config: BloomConfig) -> Self {
519 Self {
520 store,
521 filter: BloomFilter::with_config(config),
522 }
523 }
524
525 pub fn rebuild_filter(&self) -> Result<()> {
527 self.filter.clear();
528 for cid in self.store.list_cids()? {
529 self.filter.insert_cid(&cid);
530 }
531 Ok(())
532 }
533
534 pub fn bloom_stats(&self) -> BloomStats {
536 self.filter.stats()
537 }
538
539 #[inline]
541 pub fn store(&self) -> &S {
542 &self.store
543 }
544}
545
546#[async_trait]
547impl<S: BlockStore> BlockStore for BloomBlockStore<S> {
548 async fn put(&self, block: &Block) -> Result<()> {
549 self.filter.insert_cid(block.cid());
550 self.store.put(block).await
551 }
552
553 async fn put_many(&self, blocks: &[Block]) -> Result<()> {
554 for block in blocks {
555 self.filter.insert_cid(block.cid());
556 }
557 self.store.put_many(blocks).await
558 }
559
560 async fn get(&self, cid: &Cid) -> Result<Option<Block>> {
561 if !self.filter.contains_cid(cid) {
563 return Ok(None);
564 }
565 self.store.get(cid).await
567 }
568
569 async fn has(&self, cid: &Cid) -> Result<bool> {
570 if !self.filter.contains_cid(cid) {
572 return Ok(false);
573 }
574 self.store.has(cid).await
576 }
577
578 async fn has_many(&self, cids: &[Cid]) -> Result<Vec<bool>> {
579 let mut results = Vec::with_capacity(cids.len());
581 let mut to_check = Vec::new();
582 let mut indices = Vec::new();
583
584 for (i, cid) in cids.iter().enumerate() {
585 if self.filter.contains_cid(cid) {
586 to_check.push(*cid);
587 indices.push(i);
588 }
589 results.push(false); }
591
592 if !to_check.is_empty() {
594 let store_results = self.store.has_many(&to_check).await?;
595 for (idx, exists) in indices.into_iter().zip(store_results) {
596 results[idx] = exists;
597 }
598 }
599
600 Ok(results)
601 }
602
603 async fn delete(&self, cid: &Cid) -> Result<()> {
604 self.store.delete(cid).await
607 }
608
609 async fn delete_many(&self, cids: &[Cid]) -> Result<()> {
610 self.store.delete_many(cids).await
611 }
612
613 fn list_cids(&self) -> Result<Vec<Cid>> {
614 self.store.list_cids()
615 }
616
617 fn len(&self) -> usize {
618 self.store.len()
619 }
620
621 fn is_empty(&self) -> bool {
622 self.store.is_empty()
623 }
624
625 async fn flush(&self) -> Result<()> {
626 self.store.flush().await
627 }
628
629 async fn close(&self) -> Result<()> {
630 self.store.close().await
631 }
632}
633
634#[cfg(test)]
635mod tests {
636 use super::*;
637
638 #[test]
639 fn test_bloom_filter_basic() {
640 let filter = BloomFilter::new(1000, 0.01);
641
642 filter.insert(b"hello");
643 filter.insert(b"world");
644
645 assert!(filter.contains(b"hello"));
646 assert!(filter.contains(b"world"));
647 assert!(!filter.contains(b"foo")); }
649
650 #[test]
651 fn test_bloom_filter_false_positive_rate() {
652 let filter = BloomFilter::new(10000, 0.01);
653
654 for i in 0i32..10000 {
656 filter.insert(&i.to_le_bytes());
657 }
658
659 let mut false_positives = 0;
661 for i in 10000i32..20000 {
662 if filter.contains(&i.to_le_bytes()) {
663 false_positives += 1;
664 }
665 }
666
667 let fpr = false_positives as f64 / 10000.0;
669 assert!(fpr < 0.03, "False positive rate {} too high", fpr);
670 }
671
672 #[test]
673 fn test_bloom_config_memory() {
674 let config = BloomConfig::new(1_000_000, 0.01);
675 let memory_mb = config.memory_bytes() as f64 / (1024.0 * 1024.0);
676 assert!(
678 memory_mb < 10.0,
679 "Memory {} MB exceeds 10MB target",
680 memory_mb
681 );
682 }
683
684 #[test]
685 fn test_bloom_filter_stats() {
686 let filter = BloomFilter::new(1000, 0.01);
687
688 for i in 0i32..100 {
689 filter.insert(&i.to_le_bytes());
690 }
691
692 let stats = filter.stats();
693 assert_eq!(stats.count, 100);
694 assert!(stats.fill_ratio > 0.0);
695 assert!(stats.fill_ratio < 1.0);
696 }
697
698 #[test]
702 fn test_new_with_bits_rounding() {
703 let f = BloomFilter::new_with_bits(1);
704 assert_eq!(f.bit_count(), 64, "1 bit should round up to 64");
705
706 let f2 = BloomFilter::new_with_bits(65);
707 assert_eq!(f2.bit_count(), 128, "65 bits should round up to 128");
708
709 let f3 = BloomFilter::new_with_bits(1_048_576);
710 assert_eq!(
711 f3.bit_count(),
712 1_048_576,
713 "exact multiple must stay unchanged"
714 );
715 }
716
717 #[test]
719 fn test_zero_false_negatives() {
720 let filter = BloomFilter::new_with_bits(1_048_576);
721 let items: Vec<String> = (0..500).map(|i| format!("item-{}", i)).collect();
722
723 for item in &items {
724 filter.insert(item.as_bytes());
725 }
726 for item in &items {
727 assert!(
728 filter.may_contain(item.as_bytes()),
729 "False negative detected for '{}'",
730 item
731 );
732 }
733 }
734
735 #[test]
738 fn test_absent_keys_not_found() {
739 let filter = BloomFilter::new_with_bits(1_048_576);
740 assert!(!filter.may_contain(b"never-inserted-key-abc"));
742 assert!(!filter.may_contain(b"another-absent-key-xyz"));
743 }
744
745 #[test]
747 fn test_false_positive_rate_under_one_percent() {
748 let filter = BloomFilter::new_with_bits(1_048_576);
749
750 for i in 0u32..1_000 {
752 filter.insert(format!("inserted-{}", i).as_bytes());
753 }
754
755 let mut false_positives = 0usize;
757 let total = 5_000usize;
758 for i in 0u32..total as u32 {
759 if filter.may_contain(format!("probe-{}", i).as_bytes()) {
760 false_positives += 1;
761 }
762 }
763 let fpr = false_positives as f64 / total as f64;
764 assert!(
765 fpr < 0.01,
766 "FPR {:.4} ≥ 1 % for 1 000 elements in 1 M-bit filter",
767 fpr
768 );
769 }
770
771 #[test]
773 fn test_clear_resets_filter() {
774 let filter = BloomFilter::new_with_bits(1_048_576);
775 filter.insert(b"key-a");
776 filter.insert(b"key-b");
777 assert!(filter.may_contain(b"key-a"));
778 assert_eq!(filter.len(), 2);
779
780 filter.clear();
781
782 assert_eq!(filter.len(), 0);
783 assert_eq!(filter.estimated_fill_ratio(), 0.0);
784 assert!(
785 !filter.may_contain(b"key-a"),
786 "key-a should be absent after clear"
787 );
788 assert!(
789 !filter.may_contain(b"key-b"),
790 "key-b should be absent after clear"
791 );
792 }
793
794 #[test]
796 fn test_fill_ratio_grows_with_insertions() {
797 let filter = BloomFilter::new_with_bits(1_048_576);
798 let mut prev = filter.estimated_fill_ratio();
799
800 for i in 0u32..200 {
801 filter.insert(format!("grow-{}", i).as_bytes());
802 let current = filter.estimated_fill_ratio();
803 assert!(
804 current >= prev,
805 "fill_ratio decreased after insertion {} ({} < {})",
806 i,
807 current,
808 prev
809 );
810 prev = current;
811 }
812 assert!(prev > 0.0, "fill_ratio must be positive after insertions");
813 }
814
815 #[test]
817 fn test_accessors_consistency() {
818 let filter = BloomFilter::new_with_bits(1_048_576);
819 assert_eq!(filter.bit_count(), 1_048_576);
820 assert_eq!(filter.len(), 0);
821
822 filter.insert(b"x");
823 assert_eq!(filter.len(), 1);
824 }
825
826 #[test]
828 fn test_cid_bloom_zero_false_negatives() {
829 let cbf = CidBloomFilter::default_config();
830 let cids: Vec<String> = (0..300).map(|i| format!("Qm{:044}", i)).collect();
831
832 for cid in &cids {
833 cbf.insert_cid(cid);
834 }
835 for cid in &cids {
836 assert!(
837 cbf.may_contain_cid(cid),
838 "CidBloomFilter false negative for '{}'",
839 cid
840 );
841 }
842 }
843
844 #[test]
846 fn test_cid_bloom_absent_cids() {
847 let cbf = CidBloomFilter::default_config();
848 assert!(!cbf.may_contain_cid("QmNeverInserted000000000000000000000000000000000"));
849 }
850
851 #[test]
853 fn test_cid_bloom_reset() {
854 let cbf = CidBloomFilter::default_config();
855 cbf.insert_cid("QmSomeTestCid0000000000000000000000000000000000");
856 assert!(cbf.may_contain_cid("QmSomeTestCid0000000000000000000000000000000000"));
857
858 cbf.reset();
859
860 assert!(
861 !cbf.may_contain_cid("QmSomeTestCid0000000000000000000000000000000000"),
862 "CID should be absent after reset"
863 );
864 let snap = cbf.snapshot();
865 assert_eq!(snap.fill_ratio, 0.0, "fill_ratio must be 0 after reset");
866 }
867
868 #[test]
870 fn test_bloom_snapshot_fields() {
871 let cbf = CidBloomFilter::new(BloomFilterConfig {
872 bits: 1_048_576,
873 expected_elements: 100_000,
874 });
875
876 let snap_before = cbf.snapshot();
877 assert_eq!(snap_before.bit_count, 1_048_576);
878 assert_eq!(snap_before.fill_ratio, 0.0);
879
880 for i in 0u32..100 {
881 cbf.insert_cid(&format!("Qm{:044}", i));
882 }
883
884 let snap_after = cbf.snapshot();
885 assert!(
886 snap_after.fill_ratio > 0.0,
887 "fill_ratio must increase after insertions"
888 );
889 assert_eq!(snap_after.bit_count, 1_048_576);
890 assert!(
891 snap_after.estimated_elements > 0,
892 "estimated_elements must be positive after insertions"
893 );
894 }
895
896 #[test]
898 fn test_bloom_filter_config_defaults() {
899 let cfg = BloomFilterConfig::default();
900 assert_eq!(cfg.bits, 1_048_576, "default bits should be 1 048 576");
901 assert_eq!(
902 cfg.expected_elements, 100_000,
903 "default expected_elements should be 100 000"
904 );
905 }
906
907 #[test]
909 fn test_snapshot_estimated_elements_grows() {
910 let cbf = CidBloomFilter::default_config();
911 let snap0 = cbf.snapshot();
912 assert_eq!(snap0.estimated_elements, 0);
913
914 for i in 0u32..500 {
915 cbf.insert_cid(&format!("Qm{:044}", i));
916 }
917 let snap1 = cbf.snapshot();
918 assert!(
919 snap1.estimated_elements > 0,
920 "estimated_elements should be > 0 after 500 insertions"
921 );
922 }
923
924 #[test]
926 fn test_is_bloom_empty() {
927 let f = BloomFilter::new_with_bits(1_048_576);
928 assert!(f.is_bloom_empty(), "freshly created filter must be empty");
929 f.insert(b"one");
930 assert!(
931 !f.is_bloom_empty(),
932 "filter must not be empty after one insertion"
933 );
934 f.clear();
935 assert!(f.is_bloom_empty(), "filter must be empty after clear");
936 }
937}