1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
// Certificate Deduplicator using Bloom Filter
//
// Memory-efficient deduplication for certificate streaming
use bloomfilter::Bloom;
use sha2::{Digest, Sha256};
/// Deduplicator using Bloom filter for memory-efficient duplicate detection
pub struct Deduplicator {
bloom: Bloom<[u8; 32]>,
total_seen: u64,
duplicates_filtered: u64,
}
impl Deduplicator {
/// Create a new deduplicator
///
/// # Arguments
/// * `expected_items` - Expected number of unique items (used for sizing)
/// * `false_positive_rate` - Acceptable false positive rate (e.g., 0.0001 for 0.01%)
pub fn new(expected_items: usize, false_positive_rate: f64) -> Self {
let bloom = Bloom::new_for_fp_rate(expected_items, false_positive_rate);
Self {
bloom,
total_seen: 0,
duplicates_filtered: 0,
}
}
/// Check if certificate has been seen before and mark it as seen
///
/// Returns true if this is a new (unique) certificate
pub fn check_and_insert(&mut self, cert_der: &[u8]) -> bool {
self.total_seen += 1;
// Hash the certificate DER to get a consistent key
let hash = self.hash_certificate(cert_der);
// Check if we've seen this hash before
if self.bloom.check(&hash) {
// Likely a duplicate (subject to false positive rate)
self.duplicates_filtered += 1;
false
} else {
// New certificate - insert into bloom filter
self.bloom.set(&hash);
true
}
}
/// Hash a certificate to create a bloom filter key
fn hash_certificate(&self, cert_der: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(cert_der);
hasher.finalize().into()
}
/// Get total certificates seen
pub fn total_seen(&self) -> u64 {
self.total_seen
}
/// Get total duplicates filtered
pub fn duplicates_filtered(&self) -> u64 {
self.duplicates_filtered
}
/// Get unique certificates count
pub fn unique_count(&self) -> u64 {
self.total_seen - self.duplicates_filtered
}
/// Get current false positive rate estimate
pub fn false_positive_rate(&self) -> f64 {
// Bloom filter false positive rate increases with number of items
// This is an approximation based on the expected formula:
// (1 - e^(-kn/m))^k
// where k = number of hash functions, n = number of items, m = bit array size
// For simplicity, we'll return the configured rate
// In practice, the actual rate will be close to this for expected_items
let bitmap_bits = self.bloom.number_of_bits() as f64;
let num_hashes = self.bloom.number_of_hash_functions() as f64;
let items_inserted = self.unique_count() as f64;
if items_inserted == 0.0 {
return 0.0;
}
let exponent = -(num_hashes * items_inserted) / bitmap_bits;
let base = 1.0 - exponent.exp();
base.powf(num_hashes)
}
/// Get memory usage estimate in bytes
pub fn memory_usage_bytes(&self) -> usize {
// Bloom filter bitmap size + struct overhead
((self.bloom.number_of_bits() / 8) as usize) + std::mem::size_of::<Self>()
}
}
impl Default for Deduplicator {
fn default() -> Self {
// Default: 1 million expected items with 0.01% false positive rate
Self::new(1_000_000, 0.0001)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deduplicator_new_certificates() {
let mut dedup = Deduplicator::new(1000, 0.01);
let cert1 = b"certificate data 1";
let cert2 = b"certificate data 2";
// First time seeing cert1 - should be unique
assert!(dedup.check_and_insert(cert1));
assert_eq!(dedup.total_seen(), 1);
assert_eq!(dedup.unique_count(), 1);
assert_eq!(dedup.duplicates_filtered(), 0);
// First time seeing cert2 - should be unique
assert!(dedup.check_and_insert(cert2));
assert_eq!(dedup.total_seen(), 2);
assert_eq!(dedup.unique_count(), 2);
assert_eq!(dedup.duplicates_filtered(), 0);
}
#[test]
fn test_deduplicator_duplicate_certificates() {
let mut dedup = Deduplicator::new(1000, 0.01);
let cert1 = b"certificate data 1";
// First time - unique
assert!(dedup.check_and_insert(cert1));
assert_eq!(dedup.unique_count(), 1);
// Second time - duplicate
assert!(!dedup.check_and_insert(cert1));
assert_eq!(dedup.total_seen(), 2);
assert_eq!(dedup.unique_count(), 1);
assert_eq!(dedup.duplicates_filtered(), 1);
// Third time - still duplicate
assert!(!dedup.check_and_insert(cert1));
assert_eq!(dedup.total_seen(), 3);
assert_eq!(dedup.unique_count(), 1);
assert_eq!(dedup.duplicates_filtered(), 2);
}
#[test]
fn test_deduplicator_memory_usage() {
let dedup = Deduplicator::new(1000, 0.01);
let memory = dedup.memory_usage_bytes();
// Should use some memory (at least a few KB for 1000 items)
assert!(memory > 1000);
// Should use reasonable memory (less than 1 MB for 1000 items)
assert!(memory < 1_000_000);
}
#[test]
fn test_deduplicator_false_positive_rate() {
let dedup = Deduplicator::new(1000, 0.01);
let fp_rate = dedup.false_positive_rate();
// Initial FP rate should be very low (no items inserted)
assert!(fp_rate >= 0.0);
assert!(fp_rate <= 1.0);
}
#[test]
fn test_deduplicator_different_certificates() {
let mut dedup = Deduplicator::new(100, 0.01);
// Generate 50 different certificates
let mut unique_inserted = 0u64;
for i in 0..50 {
let cert = format!("certificate data {}", i);
if dedup.check_and_insert(cert.as_bytes()) {
unique_inserted += 1;
}
}
assert_eq!(dedup.total_seen(), 50);
// Bloom filter can yield false positives; allow small tolerance.
assert!(unique_inserted >= 45);
assert!(dedup.unique_count() <= 50);
}
#[test]
fn test_deduplicator_hash_consistency() {
let dedup = Deduplicator::new(100, 0.01);
let cert = b"test certificate";
let hash1 = dedup.hash_certificate(cert);
let hash2 = dedup.hash_certificate(cert);
// Same certificate should produce same hash
assert_eq!(hash1, hash2);
assert_eq!(hash1.len(), 32); // SHA256 produces 32 bytes
}
}