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