Skip to main content

rns_core/transport/
dedup.rs

1use alloc::collections::BTreeMap;
2use alloc::vec;
3use alloc::vec::Vec;
4
5/// Bounded FIFO packet-hash deduplication.
6///
7/// Retains at most `max_size` unique packet hashes. New unique hashes are
8/// appended in insertion order; when full, the oldest retained hash is evicted.
9/// Re-inserting a retained hash is a no-op and does not refresh its recency.
10pub struct PacketHashlist {
11    queue: PacketHashQueue,
12    set: PacketHashSet,
13}
14
15impl PacketHashlist {
16    pub fn new(max_size: usize) -> Self {
17        Self {
18            queue: PacketHashQueue::new(max_size),
19            set: PacketHashSet::new(max_size),
20        }
21    }
22
23    /// Check if a hash is currently retained.
24    pub fn is_duplicate(&self, hash: &[u8; 32]) -> bool {
25        self.set.contains(hash)
26    }
27
28    /// Retain a hash. If the dedup table is full, evict the oldest unique hash.
29    pub fn add(&mut self, hash: [u8; 32]) {
30        if self.queue.capacity() == 0 || self.set.contains(&hash) {
31            return;
32        }
33
34        if self.queue.len() == self.queue.capacity() {
35            let evicted = self
36                .queue
37                .pop_front()
38                .expect("full dedup queue must have an oldest entry to evict");
39            let removed = self.set.remove(&evicted);
40            debug_assert!(removed, "evicted hash must exist in dedup set");
41        }
42
43        let inserted = self.set.insert(hash);
44        debug_assert!(inserted, "new hash must insert into dedup set");
45        self.queue.push_back(hash);
46    }
47
48    /// Total number of retained packet hashes.
49    pub fn len(&self) -> usize {
50        debug_assert_eq!(self.queue.len(), self.set.len());
51        self.queue.len()
52    }
53
54    pub fn is_empty(&self) -> bool {
55        self.len() == 0
56    }
57}
58
59/// Bounded TTL cache for announce signature verification results.
60///
61/// Stores hashes of recently verified (destination_hash, signature) pairs so
62/// that duplicate announces from multiple peers skip redundant Ed25519
63/// verification. Entries expire after `ttl_secs` and are culled periodically.
64/// When `max_entries` is 0 the cache is disabled and all methods are no-ops.
65pub struct AnnounceSignatureCache {
66    entries: BTreeMap<[u8; 32], f64>,
67    insertion_order: Vec<[u8; 32]>,
68    max_entries: usize,
69    ttl_secs: f64,
70}
71
72impl AnnounceSignatureCache {
73    pub fn new(max_entries: usize, ttl_secs: f64) -> Self {
74        Self {
75            entries: BTreeMap::new(),
76            insertion_order: Vec::new(),
77            max_entries,
78            ttl_secs,
79        }
80    }
81
82    /// Check if a cache key is present (i.e., already verified).
83    pub fn contains(&self, key: &[u8; 32]) -> bool {
84        if self.max_entries == 0 {
85            return false;
86        }
87        self.entries.contains_key(key)
88    }
89
90    /// Insert a verified cache key with the current timestamp.
91    pub fn insert(&mut self, key: [u8; 32], now: f64) {
92        if self.max_entries == 0 {
93            return;
94        }
95        if self.entries.contains_key(&key) {
96            return;
97        }
98        // FIFO eviction if at capacity
99        while self.entries.len() >= self.max_entries {
100            if let Some(oldest) = self.insertion_order.first().copied() {
101                self.entries.remove(&oldest);
102                self.insertion_order.remove(0);
103            } else {
104                break;
105            }
106        }
107        self.entries.insert(key, now);
108        self.insertion_order.push(key);
109    }
110
111    /// Remove entries older than TTL. Returns the number of entries removed.
112    pub fn cull(&mut self, now: f64) -> usize {
113        if self.max_entries == 0 {
114            return 0;
115        }
116        let cutoff = now - self.ttl_secs;
117        let before = self.entries.len();
118        self.entries.retain(|_, ts| *ts > cutoff);
119        self.insertion_order
120            .retain(|key| self.entries.contains_key(key));
121        before - self.entries.len()
122    }
123
124    pub fn len(&self) -> usize {
125        self.entries.len()
126    }
127
128    pub fn is_empty(&self) -> bool {
129        self.entries.is_empty()
130    }
131}
132
133struct PacketHashQueue {
134    entries: Vec<[u8; 32]>,
135    head: usize,
136    len: usize,
137}
138
139impl PacketHashQueue {
140    fn new(capacity: usize) -> Self {
141        Self {
142            entries: vec![[0u8; 32]; capacity],
143            head: 0,
144            len: 0,
145        }
146    }
147
148    fn capacity(&self) -> usize {
149        self.entries.len()
150    }
151
152    fn len(&self) -> usize {
153        self.len
154    }
155
156    fn push_back(&mut self, hash: [u8; 32]) {
157        debug_assert!(self.len < self.capacity());
158        if self.capacity() == 0 {
159            return;
160        }
161        let tail = (self.head + self.len) % self.capacity();
162        self.entries[tail] = hash;
163        self.len += 1;
164    }
165
166    fn pop_front(&mut self) -> Option<[u8; 32]> {
167        if self.len == 0 || self.capacity() == 0 {
168            return None;
169        }
170        let hash = self.entries[self.head];
171        self.head = (self.head + 1) % self.capacity();
172        self.len -= 1;
173        if self.len == 0 {
174            self.head = 0;
175        }
176        Some(hash)
177    }
178}
179
180struct PacketHashSet {
181    buckets: Vec<Option<[u8; 32]>>,
182    len: usize,
183}
184
185impl PacketHashSet {
186    fn new(max_entries: usize) -> Self {
187        Self {
188            buckets: vec![None; bucket_capacity(max_entries)],
189            len: 0,
190        }
191    }
192
193    fn len(&self) -> usize {
194        self.len
195    }
196
197    fn contains(&self, hash: &[u8; 32]) -> bool {
198        if self.buckets.is_empty() {
199            return false;
200        }
201
202        let mut idx = self.bucket_index(hash);
203        loop {
204            match self.buckets[idx] {
205                Some(entry) if &entry == hash => return true,
206                Some(_) => idx = (idx + 1) & (self.buckets.len() - 1),
207                None => return false,
208            }
209        }
210    }
211
212    fn insert(&mut self, hash: [u8; 32]) -> bool {
213        if self.buckets.is_empty() {
214            return false;
215        }
216
217        let mut idx = self.bucket_index(&hash);
218        loop {
219            match self.buckets[idx] {
220                Some(entry) if entry == hash => return false,
221                Some(_) => idx = (idx + 1) & (self.buckets.len() - 1),
222                None => {
223                    self.buckets[idx] = Some(hash);
224                    self.len += 1;
225                    return true;
226                }
227            }
228        }
229    }
230
231    fn remove(&mut self, hash: &[u8; 32]) -> bool {
232        if self.buckets.is_empty() {
233            return false;
234        }
235
236        let mut idx = self.bucket_index(hash);
237        loop {
238            match self.buckets[idx] {
239                Some(entry) if &entry == hash => break,
240                Some(_) => idx = (idx + 1) & (self.buckets.len() - 1),
241                None => return false,
242            }
243        }
244
245        self.buckets[idx] = None;
246        self.len -= 1;
247
248        let mut next = (idx + 1) & (self.buckets.len() - 1);
249        while let Some(entry) = self.buckets[next].take() {
250            self.len -= 1;
251            let inserted = self.insert(entry);
252            debug_assert!(inserted, "cluster reinsert after removal must succeed");
253            next = (next + 1) & (self.buckets.len() - 1);
254        }
255
256        true
257    }
258
259    fn bucket_index(&self, hash: &[u8; 32]) -> usize {
260        debug_assert!(!self.buckets.is_empty());
261        (hash_bytes(hash) as usize) & (self.buckets.len() - 1)
262    }
263}
264
265fn bucket_capacity(max_entries: usize) -> usize {
266    if max_entries == 0 {
267        return 0;
268    }
269
270    let min_capacity = max_entries.saturating_mul(2).max(1);
271    min_capacity.next_power_of_two()
272}
273
274fn hash_bytes(hash: &[u8; 32]) -> u64 {
275    let mut state = 0xcbf29ce484222325u64;
276    for byte in hash {
277        state ^= u64::from(*byte);
278        state = state.wrapping_mul(0x100000001b3);
279    }
280    state
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    fn make_hash(seed: u8) -> [u8; 32] {
288        let mut h = [0u8; 32];
289        h[0] = seed;
290        h
291    }
292
293    #[test]
294    fn test_new_hash_not_duplicate() {
295        let hl = PacketHashlist::new(100);
296        assert!(!hl.is_duplicate(&make_hash(1)));
297    }
298
299    #[test]
300    fn test_added_hash_is_duplicate() {
301        let mut hl = PacketHashlist::new(100);
302        let h = make_hash(1);
303        hl.add(h);
304        assert!(hl.is_duplicate(&h));
305    }
306
307    #[test]
308    fn test_duplicate_insert_does_not_increase_len() {
309        let mut hl = PacketHashlist::new(2);
310        let h = make_hash(1);
311
312        hl.add(h);
313        hl.add(h);
314
315        assert_eq!(hl.len(), 1);
316        assert!(hl.is_duplicate(&h));
317    }
318
319    #[test]
320    fn test_full_hashlist_evicts_oldest_unique_hash() {
321        let mut hl = PacketHashlist::new(3);
322        let h1 = make_hash(1);
323        let h2 = make_hash(2);
324        let h3 = make_hash(3);
325        let h4 = make_hash(4);
326
327        hl.add(h1);
328        hl.add(h2);
329        hl.add(h3);
330        hl.add(h4);
331
332        assert!(!hl.is_duplicate(&h1));
333        assert!(hl.is_duplicate(&h2));
334        assert!(hl.is_duplicate(&h3));
335        assert!(hl.is_duplicate(&h4));
336        assert_eq!(hl.len(), 3);
337    }
338
339    #[test]
340    fn test_duplicate_does_not_refresh_recency() {
341        let mut hl = PacketHashlist::new(2);
342        let h1 = make_hash(1);
343        let h2 = make_hash(2);
344        let h3 = make_hash(3);
345
346        hl.add(h1);
347        hl.add(h2);
348        hl.add(h2);
349        hl.add(h3);
350
351        assert!(!hl.is_duplicate(&h1));
352        assert!(hl.is_duplicate(&h2));
353        assert!(hl.is_duplicate(&h3));
354        assert_eq!(hl.len(), 2);
355    }
356
357    #[test]
358    fn test_fifo_eviction_order_is_exact_across_multiple_inserts() {
359        let mut hl = PacketHashlist::new(3);
360        let h1 = make_hash(1);
361        let h2 = make_hash(2);
362        let h3 = make_hash(3);
363        let h4 = make_hash(4);
364        let h5 = make_hash(5);
365
366        hl.add(h1);
367        hl.add(h2);
368        hl.add(h3);
369        hl.add(h4);
370        hl.add(h5);
371
372        assert!(!hl.is_duplicate(&h1));
373        assert!(!hl.is_duplicate(&h2));
374        assert!(hl.is_duplicate(&h3));
375        assert!(hl.is_duplicate(&h4));
376        assert!(hl.is_duplicate(&h5));
377        assert_eq!(hl.len(), 3);
378    }
379
380    #[test]
381    fn test_zero_capacity_hashlist_is_noop() {
382        let mut hl = PacketHashlist::new(0);
383        let h = make_hash(1);
384
385        hl.add(h);
386
387        assert_eq!(hl.len(), 0);
388        assert!(!hl.is_duplicate(&h));
389    }
390
391    // --- AnnounceSignatureCache tests ---
392
393    #[test]
394    fn test_sig_cache_insert_and_contains() {
395        let mut cache = AnnounceSignatureCache::new(100, 60.0);
396        let k = make_hash(1);
397        assert!(!cache.contains(&k));
398        cache.insert(k, 100.0);
399        assert!(cache.contains(&k));
400        assert_eq!(cache.len(), 1);
401    }
402
403    #[test]
404    fn test_sig_cache_duplicate_insert_is_noop() {
405        let mut cache = AnnounceSignatureCache::new(100, 60.0);
406        let k = make_hash(1);
407        cache.insert(k, 100.0);
408        cache.insert(k, 200.0);
409        assert_eq!(cache.len(), 1);
410    }
411
412    #[test]
413    fn test_sig_cache_ttl_expiry() {
414        let mut cache = AnnounceSignatureCache::new(100, 60.0);
415        cache.insert(make_hash(1), 100.0);
416        cache.insert(make_hash(2), 150.0);
417
418        // At t=155, entry 1 (age=55) is still within TTL, entry 2 (age=5) too
419        assert_eq!(cache.cull(155.0), 0);
420        assert_eq!(cache.len(), 2);
421
422        // At t=161, entry 1 (age=61) expired, entry 2 (age=11) still valid
423        assert_eq!(cache.cull(161.0), 1);
424        assert_eq!(cache.len(), 1);
425        assert!(!cache.contains(&make_hash(1)));
426        assert!(cache.contains(&make_hash(2)));
427    }
428
429    #[test]
430    fn test_sig_cache_capacity_eviction() {
431        let mut cache = AnnounceSignatureCache::new(2, 600.0);
432        cache.insert(make_hash(1), 100.0);
433        cache.insert(make_hash(2), 101.0);
434        cache.insert(make_hash(3), 102.0); // should evict hash(1)
435
436        assert_eq!(cache.len(), 2);
437        assert!(!cache.contains(&make_hash(1)));
438        assert!(cache.contains(&make_hash(2)));
439        assert!(cache.contains(&make_hash(3)));
440    }
441
442    #[test]
443    fn test_sig_cache_disabled_when_zero_capacity() {
444        let mut cache = AnnounceSignatureCache::new(0, 60.0);
445        let k = make_hash(1);
446        cache.insert(k, 100.0);
447        assert!(!cache.contains(&k));
448        assert_eq!(cache.len(), 0);
449        assert_eq!(cache.cull(200.0), 0);
450    }
451}