light_bloom_filter/
lib.rs

1use 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    // TODO: find source for this
45    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        // Capacity is in bits while store is in bytes.
61        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    // TODO: reconsider &mut self
91    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    /// Bench results:
158    /// - 15310 CU for 10 insertions with 3 hash functions
159    /// - capacity 5000 0.000_000_000_1 with 15 hash functions seems to not
160    ///   produce any collisions
161    #[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}