Skip to main content

rns_core/transport/
dedup.rs

1use alloc::vec;
2use alloc::vec::Vec;
3
4/// Bounded FIFO packet-hash deduplication.
5///
6/// Retains at most `max_size` unique packet hashes. New unique hashes are
7/// appended in insertion order; when full, the oldest retained hash is evicted.
8/// Re-inserting a retained hash is a no-op and does not refresh its recency.
9pub struct PacketHashlist {
10    queue: PacketHashQueue,
11    set: PacketHashSet,
12}
13
14impl PacketHashlist {
15    pub fn new(max_size: usize) -> Self {
16        Self {
17            queue: PacketHashQueue::new(max_size),
18            set: PacketHashSet::new(max_size),
19        }
20    }
21
22    /// Check if a hash is currently retained.
23    pub fn is_duplicate(&self, hash: &[u8; 32]) -> bool {
24        self.set.contains(hash)
25    }
26
27    /// Retain a hash. If the dedup table is full, evict the oldest unique hash.
28    pub fn add(&mut self, hash: [u8; 32]) {
29        if self.queue.capacity() == 0 || self.set.contains(&hash) {
30            return;
31        }
32
33        if self.queue.len() == self.queue.capacity() {
34            let evicted = self
35                .queue
36                .pop_front()
37                .expect("full dedup queue must have an oldest entry to evict");
38            let removed = self.set.remove(&evicted);
39            debug_assert!(removed, "evicted hash must exist in dedup set");
40        }
41
42        let inserted = self.set.insert(hash);
43        debug_assert!(inserted, "new hash must insert into dedup set");
44        self.queue.push_back(hash);
45    }
46
47    /// Total number of retained packet hashes.
48    pub fn len(&self) -> usize {
49        debug_assert_eq!(self.queue.len(), self.set.len());
50        self.queue.len()
51    }
52
53    pub fn is_empty(&self) -> bool {
54        self.len() == 0
55    }
56}
57
58struct PacketHashQueue {
59    entries: Vec<[u8; 32]>,
60    head: usize,
61    len: usize,
62}
63
64impl PacketHashQueue {
65    fn new(capacity: usize) -> Self {
66        Self {
67            entries: vec![[0u8; 32]; capacity],
68            head: 0,
69            len: 0,
70        }
71    }
72
73    fn capacity(&self) -> usize {
74        self.entries.len()
75    }
76
77    fn len(&self) -> usize {
78        self.len
79    }
80
81    fn push_back(&mut self, hash: [u8; 32]) {
82        debug_assert!(self.len < self.capacity());
83        if self.capacity() == 0 {
84            return;
85        }
86        let tail = (self.head + self.len) % self.capacity();
87        self.entries[tail] = hash;
88        self.len += 1;
89    }
90
91    fn pop_front(&mut self) -> Option<[u8; 32]> {
92        if self.len == 0 || self.capacity() == 0 {
93            return None;
94        }
95        let hash = self.entries[self.head];
96        self.head = (self.head + 1) % self.capacity();
97        self.len -= 1;
98        if self.len == 0 {
99            self.head = 0;
100        }
101        Some(hash)
102    }
103}
104
105struct PacketHashSet {
106    buckets: Vec<Option<[u8; 32]>>,
107    len: usize,
108}
109
110impl PacketHashSet {
111    fn new(max_entries: usize) -> Self {
112        Self {
113            buckets: vec![None; bucket_capacity(max_entries)],
114            len: 0,
115        }
116    }
117
118    fn len(&self) -> usize {
119        self.len
120    }
121
122    fn contains(&self, hash: &[u8; 32]) -> bool {
123        if self.buckets.is_empty() {
124            return false;
125        }
126
127        let mut idx = self.bucket_index(hash);
128        loop {
129            match self.buckets[idx] {
130                Some(entry) if &entry == hash => return true,
131                Some(_) => idx = (idx + 1) & (self.buckets.len() - 1),
132                None => return false,
133            }
134        }
135    }
136
137    fn insert(&mut self, hash: [u8; 32]) -> bool {
138        if self.buckets.is_empty() {
139            return false;
140        }
141
142        let mut idx = self.bucket_index(&hash);
143        loop {
144            match self.buckets[idx] {
145                Some(entry) if entry == hash => return false,
146                Some(_) => idx = (idx + 1) & (self.buckets.len() - 1),
147                None => {
148                    self.buckets[idx] = Some(hash);
149                    self.len += 1;
150                    return true;
151                }
152            }
153        }
154    }
155
156    fn remove(&mut self, hash: &[u8; 32]) -> bool {
157        if self.buckets.is_empty() {
158            return false;
159        }
160
161        let mut idx = self.bucket_index(hash);
162        loop {
163            match self.buckets[idx] {
164                Some(entry) if &entry == hash => break,
165                Some(_) => idx = (idx + 1) & (self.buckets.len() - 1),
166                None => return false,
167            }
168        }
169
170        self.buckets[idx] = None;
171        self.len -= 1;
172
173        let mut next = (idx + 1) & (self.buckets.len() - 1);
174        while let Some(entry) = self.buckets[next].take() {
175            self.len -= 1;
176            let inserted = self.insert(entry);
177            debug_assert!(inserted, "cluster reinsert after removal must succeed");
178            next = (next + 1) & (self.buckets.len() - 1);
179        }
180
181        true
182    }
183
184    fn bucket_index(&self, hash: &[u8; 32]) -> usize {
185        debug_assert!(!self.buckets.is_empty());
186        (hash_bytes(hash) as usize) & (self.buckets.len() - 1)
187    }
188}
189
190fn bucket_capacity(max_entries: usize) -> usize {
191    if max_entries == 0 {
192        return 0;
193    }
194
195    let min_capacity = max_entries.saturating_mul(2).max(1);
196    min_capacity.next_power_of_two()
197}
198
199fn hash_bytes(hash: &[u8; 32]) -> u64 {
200    let mut state = 0xcbf29ce484222325u64;
201    for byte in hash {
202        state ^= u64::from(*byte);
203        state = state.wrapping_mul(0x100000001b3);
204    }
205    state
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    fn make_hash(seed: u8) -> [u8; 32] {
213        let mut h = [0u8; 32];
214        h[0] = seed;
215        h
216    }
217
218    #[test]
219    fn test_new_hash_not_duplicate() {
220        let hl = PacketHashlist::new(100);
221        assert!(!hl.is_duplicate(&make_hash(1)));
222    }
223
224    #[test]
225    fn test_added_hash_is_duplicate() {
226        let mut hl = PacketHashlist::new(100);
227        let h = make_hash(1);
228        hl.add(h);
229        assert!(hl.is_duplicate(&h));
230    }
231
232    #[test]
233    fn test_duplicate_insert_does_not_increase_len() {
234        let mut hl = PacketHashlist::new(2);
235        let h = make_hash(1);
236
237        hl.add(h);
238        hl.add(h);
239
240        assert_eq!(hl.len(), 1);
241        assert!(hl.is_duplicate(&h));
242    }
243
244    #[test]
245    fn test_full_hashlist_evicts_oldest_unique_hash() {
246        let mut hl = PacketHashlist::new(3);
247        let h1 = make_hash(1);
248        let h2 = make_hash(2);
249        let h3 = make_hash(3);
250        let h4 = make_hash(4);
251
252        hl.add(h1);
253        hl.add(h2);
254        hl.add(h3);
255        hl.add(h4);
256
257        assert!(!hl.is_duplicate(&h1));
258        assert!(hl.is_duplicate(&h2));
259        assert!(hl.is_duplicate(&h3));
260        assert!(hl.is_duplicate(&h4));
261        assert_eq!(hl.len(), 3);
262    }
263
264    #[test]
265    fn test_duplicate_does_not_refresh_recency() {
266        let mut hl = PacketHashlist::new(2);
267        let h1 = make_hash(1);
268        let h2 = make_hash(2);
269        let h3 = make_hash(3);
270
271        hl.add(h1);
272        hl.add(h2);
273        hl.add(h2);
274        hl.add(h3);
275
276        assert!(!hl.is_duplicate(&h1));
277        assert!(hl.is_duplicate(&h2));
278        assert!(hl.is_duplicate(&h3));
279        assert_eq!(hl.len(), 2);
280    }
281
282    #[test]
283    fn test_fifo_eviction_order_is_exact_across_multiple_inserts() {
284        let mut hl = PacketHashlist::new(3);
285        let h1 = make_hash(1);
286        let h2 = make_hash(2);
287        let h3 = make_hash(3);
288        let h4 = make_hash(4);
289        let h5 = make_hash(5);
290
291        hl.add(h1);
292        hl.add(h2);
293        hl.add(h3);
294        hl.add(h4);
295        hl.add(h5);
296
297        assert!(!hl.is_duplicate(&h1));
298        assert!(!hl.is_duplicate(&h2));
299        assert!(hl.is_duplicate(&h3));
300        assert!(hl.is_duplicate(&h4));
301        assert!(hl.is_duplicate(&h5));
302        assert_eq!(hl.len(), 3);
303    }
304
305    #[test]
306    fn test_zero_capacity_hashlist_is_noop() {
307        let mut hl = PacketHashlist::new(0);
308        let h = make_hash(1);
309
310        hl.add(h);
311
312        assert_eq!(hl.len(), 0);
313        assert!(!hl.is_duplicate(&h));
314    }
315}