light_bloom_filter/
lib.rs1use std::f64::consts::LN_2;
2
3use thiserror::Error;
4
5#[derive(Debug, Error, PartialEq)]
6pub enum BloomFilterError {
7 #[error("Bloom filter is full")]
8 Full,
9 #[error("Invalid store capacity")]
10 InvalidStoreCapacity,
11}
12
13impl From<BloomFilterError> for u32 {
14 fn from(e: BloomFilterError) -> u32 {
15 match e {
16 BloomFilterError::Full => 14201,
17 BloomFilterError::InvalidStoreCapacity => 14202,
18 }
19 }
20}
21
22#[cfg(all(feature = "solana", not(feature = "pinocchio")))]
23impl From<BloomFilterError> for solana_program_error::ProgramError {
24 fn from(e: BloomFilterError) -> Self {
25 solana_program_error::ProgramError::Custom(e.into())
26 }
27}
28
29#[cfg(all(feature = "pinocchio", not(feature = "solana")))]
30impl From<BloomFilterError> for pinocchio::program_error::ProgramError {
31 fn from(e: BloomFilterError) -> Self {
32 pinocchio::program_error::ProgramError::Custom(e.into())
33 }
34}
35
36#[derive(Debug)]
37pub struct BloomFilter<'a> {
38 pub num_iters: usize,
39 pub capacity: u64,
40 pub store: &'a mut [u8],
41}
42
43impl<'a> BloomFilter<'a> {
44 pub fn calculate_bloom_filter_size(n: usize, p: f64) -> usize {
46 let m = -((n as f64) * p.ln()) / (LN_2 * LN_2);
47 m.ceil() as usize
48 }
49
50 pub fn calculate_optimal_hash_functions(n: usize, m: usize) -> usize {
51 let k = (m as f64 / n as f64) * LN_2;
52 k.ceil() as usize
53 }
54
55 pub fn new(
56 num_iters: usize,
57 capacity: u64,
58 store: &'a mut [u8],
59 ) -> Result<Self, BloomFilterError> {
60 if store.len() * 8 != capacity as usize {
62 return Err(BloomFilterError::InvalidStoreCapacity);
63 }
64 Ok(Self {
65 num_iters,
66 capacity,
67 store,
68 })
69 }
70
71 pub fn probe_index_fast_murmur(value_bytes: &[u8], iteration: usize, capacity: &u64) -> usize {
72 let iter_bytes = iteration.to_le_bytes();
73 let base_hash = fastmurmur3::hash(value_bytes);
74 let mut combined_bytes = [0u8; 24];
75 combined_bytes[..16].copy_from_slice(&base_hash.to_le_bytes());
76 combined_bytes[16..].copy_from_slice(&iter_bytes);
77
78 let combined_hash = fastmurmur3::hash(&combined_bytes);
79 (combined_hash % (*capacity as u128)) as usize
80 }
81
82 pub fn insert(&mut self, value: &[u8; 32]) -> Result<(), BloomFilterError> {
83 if self._insert(value, true) {
84 Ok(())
85 } else {
86 Err(BloomFilterError::Full)
87 }
88 }
89
90 pub fn contains(&mut self, value: &[u8; 32]) -> bool {
92 !self._insert(value, false)
93 }
94
95 fn _insert(&mut self, value: &[u8; 32], insert: bool) -> bool {
96 let mut all_bits_set = true;
97 use bitvec::prelude::*;
98
99 let bits = BitSlice::<u8, Msb0>::from_slice_mut(self.store);
100 for i in 0..self.num_iters {
101 let probe_index = Self::probe_index_fast_murmur(value, i, &(self.capacity));
102 if bits[probe_index] {
103 continue;
104 } else if insert {
105 all_bits_set = false;
106 bits.set(probe_index, true);
107 } else if !bits[probe_index] && !insert {
108 return true;
109 }
110 }
111 !all_bits_set
112 }
113}
114
115#[cfg(test)]
116mod test {
117 use light_hasher::bigint::bigint_to_be_bytes_array;
118 use num_bigint::{RandBigInt, ToBigUint};
119 use rand::thread_rng;
120
121 use super::*;
122
123 #[test]
124 fn test_insert_and_contains() -> Result<(), BloomFilterError> {
125 let capacity = 128_000 * 8;
126 let mut store = [0u8; 128_000];
127 let mut bf = BloomFilter {
128 num_iters: 3,
129 capacity,
130 store: &mut store,
131 };
132
133 let value1 = [1u8; 32];
134 let value2 = [2u8; 32];
135
136 bf.insert(&value1)?;
137 assert!(bf.contains(&value1));
138 assert!(!bf.contains(&value2));
139
140 Ok(())
141 }
142
143 #[test]
144 fn short_rnd_test() {
145 let capacity = 500;
146 let bloom_filter_capacity = 20_000 * 8;
147 let optimal_hash_functions = 3;
148 rnd_test(
149 1000,
150 capacity,
151 bloom_filter_capacity,
152 optimal_hash_functions,
153 false,
154 );
155 }
156
157 #[ignore = "bench"]
162 #[test]
163 fn bench_bloom_filter() {
164 let capacity = 5000;
165 let bloom_filter_capacity =
166 BloomFilter::calculate_bloom_filter_size(capacity, 0.000_000_000_1);
167 let optimal_hash_functions = 15;
168 let iterations = 1_000_000;
169 rnd_test(
170 iterations,
171 capacity,
172 bloom_filter_capacity,
173 optimal_hash_functions,
174 true,
175 );
176 }
177
178 fn rnd_test(
179 num_iters: usize,
180 capacity: usize,
181 bloom_filter_capacity: usize,
182 optimal_hash_functions: usize,
183 bench: bool,
184 ) {
185 println!("Optimal hash functions: {}", optimal_hash_functions);
186 println!(
187 "Bloom filter capacity (kb): {}",
188 bloom_filter_capacity / 8 / 1_000
189 );
190 let mut num_total_txs = 0;
191 let mut rng = thread_rng();
192 let mut failed_vec = Vec::new();
193 for j in 0..num_iters {
194 let mut inserted_values = Vec::new();
195 let mut store = vec![0; bloom_filter_capacity];
196 let mut bf = BloomFilter {
197 num_iters: optimal_hash_functions,
198 capacity: bloom_filter_capacity as u64,
199 store: &mut store,
200 };
201 if j == 0 {
202 println!("Bloom filter capacity: {}", bf.capacity);
203 println!("Bloom filter size: {}", bf.store.len());
204 println!("Bloom filter size (kb): {}", bf.store.len() / 8 / 1_000);
205 println!("num iters: {}", bf.num_iters);
206 }
207 for i in 0..capacity {
208 num_total_txs += 1;
209 let value = {
210 let mut _value = 0u64.to_biguint().unwrap();
211 while inserted_values.contains(&_value.clone()) {
212 _value = rng.gen_biguint(254);
213 }
214 inserted_values.push(_value.clone());
215
216 _value
217 };
218 let value: [u8; 32] = bigint_to_be_bytes_array(&value).unwrap();
219 match bf.insert(&value) {
220 Ok(_) => {
221 assert!(bf.contains(&value));
222 }
223 Err(_) => {
224 println!("Failed to insert iter: {}", i);
225 println!("total iter {}", j);
226 println!("num_total_txs {}", num_total_txs);
227 failed_vec.push(i);
228 }
229 };
230 assert!(bf.contains(&value));
231 assert!(bf.insert(&value).is_err());
232 }
233 }
234 if bench {
235 println!("total num tx {}", num_total_txs);
236 let average = failed_vec.iter().sum::<usize>() as f64 / failed_vec.len() as f64;
237 println!("average failed insertions: {}", average);
238 println!(
239 "max failed insertions: {}",
240 failed_vec.iter().max().unwrap()
241 );
242 println!(
243 "min failed insertions: {}",
244 failed_vec.iter().min().unwrap()
245 );
246
247 let num_chunks = 10;
248 let chunk_size = num_iters / num_chunks;
249 failed_vec.sort();
250 for (i, chunk) in failed_vec.chunks(chunk_size).enumerate() {
251 let average = chunk.iter().sum::<usize>() as f64 / chunk.len() as f64;
252 println!("chunk: {} average failed insertions: {}", i, average);
253 println!(
254 "chunk: {} max failed insertions: {}",
255 i,
256 chunk.iter().max().unwrap()
257 );
258 println!(
259 "chunk: {} min failed insertions: {}",
260 i,
261 chunk.iter().min().unwrap()
262 );
263 }
264 }
265 }
266}