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
129struct PacketHashQueue {
130    entries: Vec<[u8; 32]>,
131    head: usize,
132    len: usize,
133}
134
135impl PacketHashQueue {
136    fn new(capacity: usize) -> Self {
137        Self {
138            entries: vec![[0u8; 32]; capacity],
139            head: 0,
140            len: 0,
141        }
142    }
143
144    fn capacity(&self) -> usize {
145        self.entries.len()
146    }
147
148    fn len(&self) -> usize {
149        self.len
150    }
151
152    fn push_back(&mut self, hash: [u8; 32]) {
153        debug_assert!(self.len < self.capacity());
154        if self.capacity() == 0 {
155            return;
156        }
157        let tail = (self.head + self.len) % self.capacity();
158        self.entries[tail] = hash;
159        self.len += 1;
160    }
161
162    fn pop_front(&mut self) -> Option<[u8; 32]> {
163        if self.len == 0 || self.capacity() == 0 {
164            return None;
165        }
166        let hash = self.entries[self.head];
167        self.head = (self.head + 1) % self.capacity();
168        self.len -= 1;
169        if self.len == 0 {
170            self.head = 0;
171        }
172        Some(hash)
173    }
174}
175
176struct PacketHashSet {
177    buckets: Vec<Option<[u8; 32]>>,
178    len: usize,
179}
180
181impl PacketHashSet {
182    fn new(max_entries: usize) -> Self {
183        Self {
184            buckets: vec![None; bucket_capacity(max_entries)],
185            len: 0,
186        }
187    }
188
189    fn len(&self) -> usize {
190        self.len
191    }
192
193    fn contains(&self, hash: &[u8; 32]) -> bool {
194        if self.buckets.is_empty() {
195            return false;
196        }
197
198        let mut idx = self.bucket_index(hash);
199        loop {
200            match self.buckets[idx] {
201                Some(entry) if &entry == hash => return true,
202                Some(_) => idx = (idx + 1) & (self.buckets.len() - 1),
203                None => return false,
204            }
205        }
206    }
207
208    fn insert(&mut self, hash: [u8; 32]) -> bool {
209        if self.buckets.is_empty() {
210            return false;
211        }
212
213        let mut idx = self.bucket_index(&hash);
214        loop {
215            match self.buckets[idx] {
216                Some(entry) if entry == hash => return false,
217                Some(_) => idx = (idx + 1) & (self.buckets.len() - 1),
218                None => {
219                    self.buckets[idx] = Some(hash);
220                    self.len += 1;
221                    return true;
222                }
223            }
224        }
225    }
226
227    fn remove(&mut self, hash: &[u8; 32]) -> bool {
228        if self.buckets.is_empty() {
229            return false;
230        }
231
232        let mut idx = self.bucket_index(hash);
233        loop {
234            match self.buckets[idx] {
235                Some(entry) if &entry == hash => break,
236                Some(_) => idx = (idx + 1) & (self.buckets.len() - 1),
237                None => return false,
238            }
239        }
240
241        self.buckets[idx] = None;
242        self.len -= 1;
243
244        let mut next = (idx + 1) & (self.buckets.len() - 1);
245        while let Some(entry) = self.buckets[next].take() {
246            self.len -= 1;
247            let inserted = self.insert(entry);
248            debug_assert!(inserted, "cluster reinsert after removal must succeed");
249            next = (next + 1) & (self.buckets.len() - 1);
250        }
251
252        true
253    }
254
255    fn bucket_index(&self, hash: &[u8; 32]) -> usize {
256        debug_assert!(!self.buckets.is_empty());
257        (hash_bytes(hash) as usize) & (self.buckets.len() - 1)
258    }
259}
260
261fn bucket_capacity(max_entries: usize) -> usize {
262    if max_entries == 0 {
263        return 0;
264    }
265
266    let min_capacity = max_entries.saturating_mul(2).max(1);
267    min_capacity.next_power_of_two()
268}
269
270fn hash_bytes(hash: &[u8; 32]) -> u64 {
271    let mut state = 0xcbf29ce484222325u64;
272    for byte in hash {
273        state ^= u64::from(*byte);
274        state = state.wrapping_mul(0x100000001b3);
275    }
276    state
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    fn make_hash(seed: u8) -> [u8; 32] {
284        let mut h = [0u8; 32];
285        h[0] = seed;
286        h
287    }
288
289    #[test]
290    fn test_new_hash_not_duplicate() {
291        let hl = PacketHashlist::new(100);
292        assert!(!hl.is_duplicate(&make_hash(1)));
293    }
294
295    #[test]
296    fn test_added_hash_is_duplicate() {
297        let mut hl = PacketHashlist::new(100);
298        let h = make_hash(1);
299        hl.add(h);
300        assert!(hl.is_duplicate(&h));
301    }
302
303    #[test]
304    fn test_duplicate_insert_does_not_increase_len() {
305        let mut hl = PacketHashlist::new(2);
306        let h = make_hash(1);
307
308        hl.add(h);
309        hl.add(h);
310
311        assert_eq!(hl.len(), 1);
312        assert!(hl.is_duplicate(&h));
313    }
314
315    #[test]
316    fn test_full_hashlist_evicts_oldest_unique_hash() {
317        let mut hl = PacketHashlist::new(3);
318        let h1 = make_hash(1);
319        let h2 = make_hash(2);
320        let h3 = make_hash(3);
321        let h4 = make_hash(4);
322
323        hl.add(h1);
324        hl.add(h2);
325        hl.add(h3);
326        hl.add(h4);
327
328        assert!(!hl.is_duplicate(&h1));
329        assert!(hl.is_duplicate(&h2));
330        assert!(hl.is_duplicate(&h3));
331        assert!(hl.is_duplicate(&h4));
332        assert_eq!(hl.len(), 3);
333    }
334
335    #[test]
336    fn test_duplicate_does_not_refresh_recency() {
337        let mut hl = PacketHashlist::new(2);
338        let h1 = make_hash(1);
339        let h2 = make_hash(2);
340        let h3 = make_hash(3);
341
342        hl.add(h1);
343        hl.add(h2);
344        hl.add(h2);
345        hl.add(h3);
346
347        assert!(!hl.is_duplicate(&h1));
348        assert!(hl.is_duplicate(&h2));
349        assert!(hl.is_duplicate(&h3));
350        assert_eq!(hl.len(), 2);
351    }
352
353    #[test]
354    fn test_fifo_eviction_order_is_exact_across_multiple_inserts() {
355        let mut hl = PacketHashlist::new(3);
356        let h1 = make_hash(1);
357        let h2 = make_hash(2);
358        let h3 = make_hash(3);
359        let h4 = make_hash(4);
360        let h5 = make_hash(5);
361
362        hl.add(h1);
363        hl.add(h2);
364        hl.add(h3);
365        hl.add(h4);
366        hl.add(h5);
367
368        assert!(!hl.is_duplicate(&h1));
369        assert!(!hl.is_duplicate(&h2));
370        assert!(hl.is_duplicate(&h3));
371        assert!(hl.is_duplicate(&h4));
372        assert!(hl.is_duplicate(&h5));
373        assert_eq!(hl.len(), 3);
374    }
375
376    #[test]
377    fn test_zero_capacity_hashlist_is_noop() {
378        let mut hl = PacketHashlist::new(0);
379        let h = make_hash(1);
380
381        hl.add(h);
382
383        assert_eq!(hl.len(), 0);
384        assert!(!hl.is_duplicate(&h));
385    }
386
387    // --- AnnounceSignatureCache tests ---
388
389    #[test]
390    fn test_sig_cache_insert_and_contains() {
391        let mut cache = AnnounceSignatureCache::new(100, 60.0);
392        let k = make_hash(1);
393        assert!(!cache.contains(&k));
394        cache.insert(k, 100.0);
395        assert!(cache.contains(&k));
396        assert_eq!(cache.len(), 1);
397    }
398
399    #[test]
400    fn test_sig_cache_duplicate_insert_is_noop() {
401        let mut cache = AnnounceSignatureCache::new(100, 60.0);
402        let k = make_hash(1);
403        cache.insert(k, 100.0);
404        cache.insert(k, 200.0);
405        assert_eq!(cache.len(), 1);
406    }
407
408    #[test]
409    fn test_sig_cache_ttl_expiry() {
410        let mut cache = AnnounceSignatureCache::new(100, 60.0);
411        cache.insert(make_hash(1), 100.0);
412        cache.insert(make_hash(2), 150.0);
413
414        // At t=155, entry 1 (age=55) is still within TTL, entry 2 (age=5) too
415        assert_eq!(cache.cull(155.0), 0);
416        assert_eq!(cache.len(), 2);
417
418        // At t=161, entry 1 (age=61) expired, entry 2 (age=11) still valid
419        assert_eq!(cache.cull(161.0), 1);
420        assert_eq!(cache.len(), 1);
421        assert!(!cache.contains(&make_hash(1)));
422        assert!(cache.contains(&make_hash(2)));
423    }
424
425    #[test]
426    fn test_sig_cache_capacity_eviction() {
427        let mut cache = AnnounceSignatureCache::new(2, 600.0);
428        cache.insert(make_hash(1), 100.0);
429        cache.insert(make_hash(2), 101.0);
430        cache.insert(make_hash(3), 102.0); // should evict hash(1)
431
432        assert_eq!(cache.len(), 2);
433        assert!(!cache.contains(&make_hash(1)));
434        assert!(cache.contains(&make_hash(2)));
435        assert!(cache.contains(&make_hash(3)));
436    }
437
438    #[test]
439    fn test_sig_cache_disabled_when_zero_capacity() {
440        let mut cache = AnnounceSignatureCache::new(0, 60.0);
441        let k = make_hash(1);
442        cache.insert(k, 100.0);
443        assert!(!cache.contains(&k));
444        assert_eq!(cache.len(), 0);
445        assert_eq!(cache.cull(200.0), 0);
446    }
447}