Skip to main content

rns_core/transport/
dedup.rs

1use alloc::boxed::Box;
2use alloc::collections::BTreeMap;
3use alloc::vec;
4use alloc::vec::Vec;
5use core::mem::MaybeUninit;
6
7use super::types::PacketHashlistAllocation;
8
9/// Bounded FIFO packet-hash deduplication.
10///
11/// Retains at most `max_size` unique packet hashes. New unique hashes are
12/// appended in insertion order; when full, the oldest retained hash is evicted.
13/// Re-inserting a retained hash is a no-op and does not refresh its recency.
14pub struct PacketHashlist {
15    queue: PacketHashQueue,
16    set: PacketHashSet,
17}
18
19impl PacketHashlist {
20    pub fn new(max_size: usize) -> Self {
21        Self::with_allocation(max_size, PacketHashlistAllocation::Eager)
22    }
23
24    pub fn with_allocation(max_size: usize, allocation: PacketHashlistAllocation) -> Self {
25        Self {
26            queue: PacketHashQueue::new(max_size, allocation),
27            set: PacketHashSet::new(max_size, allocation),
28        }
29    }
30
31    /// Check if a hash is currently retained.
32    pub fn is_duplicate(&self, hash: &[u8; 32]) -> bool {
33        self.set.contains(hash)
34    }
35
36    /// Retain a hash. If the dedup table is full, evict the oldest unique hash.
37    pub fn add(&mut self, hash: [u8; 32]) {
38        if self.queue.capacity() == 0 || self.set.contains(&hash) {
39            return;
40        }
41
42        if self.queue.len() == self.queue.capacity() {
43            let Some(evicted) = self.queue.pop_front() else {
44                return;
45            };
46            let removed = self.set.remove(&evicted);
47            debug_assert!(removed, "evicted hash must exist in dedup set");
48        }
49
50        let inserted = self.set.insert(hash);
51        debug_assert!(inserted, "new hash must insert into dedup set");
52        self.queue.push_back(hash);
53    }
54
55    /// Stop retaining a hash, preserving the FIFO order of all other entries.
56    pub fn remove(&mut self, hash: &[u8; 32]) -> bool {
57        if !self.set.remove(hash) {
58            return false;
59        }
60
61        let removed = self.queue.remove(hash);
62        debug_assert!(removed, "dedup set entry must exist in FIFO queue");
63        true
64    }
65
66    /// Total number of retained packet hashes.
67    pub fn len(&self) -> usize {
68        debug_assert_eq!(self.queue.len(), self.set.len());
69        self.queue.len()
70    }
71
72    pub fn is_empty(&self) -> bool {
73        self.len() == 0
74    }
75
76    /// Iterate retained hashes from oldest to newest.
77    pub fn iter(&self) -> impl Iterator<Item = &[u8; 32]> {
78        (0..self.queue.len).map(|offset| {
79            let index = (self.queue.head + offset) % self.queue.capacity();
80            self.queue.entries.get(index)
81        })
82    }
83}
84
85/// Fixed-capacity hash payload slots whose initialization is tracked by their owner.
86///
87/// Queue owners may read only slots in their logical FIFO range. Set owners may
88/// read only slots whose corresponding control byte is occupied. Keeping reads
89/// here confines the unsafe code required for lazy payload initialization.
90struct RawHashSlots {
91    slots: Box<[MaybeUninit<[u8; 32]>]>,
92}
93
94impl RawHashSlots {
95    fn new(capacity: usize, allocation: PacketHashlistAllocation) -> Self {
96        let mut slots = Box::<[[u8; 32]]>::new_uninit_slice(capacity);
97        if allocation == PacketHashlistAllocation::Eager {
98            for slot in &mut slots {
99                // Volatile writes make eager page prefaulting an observable side
100                // effect that release-mode optimization cannot remove.
101                unsafe { core::ptr::write_volatile(slot.as_mut_ptr(), [0; 32]) };
102            }
103        }
104        Self { slots }
105    }
106
107    fn len(&self) -> usize {
108        self.slots.len()
109    }
110
111    fn write(&mut self, index: usize, hash: [u8; 32]) {
112        self.slots[index].write(hash);
113    }
114
115    fn read(&self, index: usize) -> [u8; 32] {
116        // SAFETY: callers establish initialization through the queue's logical
117        // range or the set's occupied control byte before calling this method.
118        unsafe { self.slots[index].assume_init_read() }
119    }
120
121    fn get(&self, index: usize) -> &[u8; 32] {
122        // SAFETY: callers establish initialization through the queue's logical
123        // range or the set's occupied control byte before calling this method.
124        unsafe { self.slots[index].assume_init_ref() }
125    }
126}
127
128/// Bounded TTL cache for announce signature verification results.
129///
130/// Stores hashes of recently verified (destination_hash, signature) pairs so
131/// that duplicate announces from multiple peers skip redundant Ed25519
132/// verification. Entries expire after `ttl_secs` and are culled periodically.
133/// When `max_entries` is 0 the cache is disabled and all methods are no-ops.
134pub struct AnnounceSignatureCache {
135    entries: BTreeMap<[u8; 32], f64>,
136    insertion_order: Vec<[u8; 32]>,
137    max_entries: usize,
138    ttl_secs: f64,
139}
140
141impl AnnounceSignatureCache {
142    pub fn new(max_entries: usize, ttl_secs: f64) -> Self {
143        Self {
144            entries: BTreeMap::new(),
145            insertion_order: Vec::new(),
146            max_entries,
147            ttl_secs,
148        }
149    }
150
151    /// Check if a cache key is present (i.e., already verified).
152    pub fn contains(&self, key: &[u8; 32]) -> bool {
153        if self.max_entries == 0 {
154            return false;
155        }
156        self.entries.contains_key(key)
157    }
158
159    /// Insert a verified cache key with the current timestamp.
160    pub fn insert(&mut self, key: [u8; 32], now: f64) {
161        if self.max_entries == 0 {
162            return;
163        }
164        if self.entries.contains_key(&key) {
165            return;
166        }
167        // FIFO eviction if at capacity
168        while self.entries.len() >= self.max_entries {
169            if let Some(oldest) = self.insertion_order.first().copied() {
170                self.entries.remove(&oldest);
171                self.insertion_order.remove(0);
172            } else {
173                break;
174            }
175        }
176        self.entries.insert(key, now);
177        self.insertion_order.push(key);
178    }
179
180    /// Remove entries older than TTL. Returns the number of entries removed.
181    pub fn cull(&mut self, now: f64) -> usize {
182        if self.max_entries == 0 {
183            return 0;
184        }
185        let cutoff = now - self.ttl_secs;
186        let before = self.entries.len();
187        self.entries.retain(|_, ts| *ts > cutoff);
188        self.insertion_order
189            .retain(|key| self.entries.contains_key(key));
190        before - self.entries.len()
191    }
192
193    pub fn len(&self) -> usize {
194        self.entries.len()
195    }
196
197    pub fn is_empty(&self) -> bool {
198        self.entries.is_empty()
199    }
200}
201
202struct PacketHashQueue {
203    entries: RawHashSlots,
204    head: usize,
205    len: usize,
206}
207
208impl PacketHashQueue {
209    fn new(capacity: usize, allocation: PacketHashlistAllocation) -> Self {
210        Self {
211            entries: RawHashSlots::new(capacity, allocation),
212            head: 0,
213            len: 0,
214        }
215    }
216
217    fn capacity(&self) -> usize {
218        self.entries.len()
219    }
220
221    fn len(&self) -> usize {
222        self.len
223    }
224
225    fn push_back(&mut self, hash: [u8; 32]) {
226        debug_assert!(self.len < self.capacity());
227        if self.capacity() == 0 {
228            return;
229        }
230        let tail = (self.head + self.len) % self.capacity();
231        self.entries.write(tail, hash);
232        self.len += 1;
233    }
234
235    fn pop_front(&mut self) -> Option<[u8; 32]> {
236        if self.len == 0 || self.capacity() == 0 {
237            return None;
238        }
239        let hash = self.entries.read(self.head);
240        self.head = (self.head + 1) % self.capacity();
241        self.len -= 1;
242        if self.len == 0 {
243            self.head = 0;
244        }
245        Some(hash)
246    }
247
248    fn remove(&mut self, hash: &[u8; 32]) -> bool {
249        let Some(offset) = (0..self.len).find(|offset| {
250            let index = (self.head + offset) % self.capacity();
251            self.entries.get(index) == hash
252        }) else {
253            return false;
254        };
255
256        for current in offset..self.len - 1 {
257            let next_index = (self.head + current + 1) % self.capacity();
258            let current_index = (self.head + current) % self.capacity();
259            let next = self.entries.read(next_index);
260            self.entries.write(current_index, next);
261        }
262        self.len -= 1;
263        if self.len == 0 {
264            self.head = 0;
265        }
266        true
267    }
268}
269
270struct PacketHashSet {
271    entries: RawHashSlots,
272    controls: Box<[u8]>,
273    len: usize,
274}
275
276impl PacketHashSet {
277    fn new(max_entries: usize, allocation: PacketHashlistAllocation) -> Self {
278        let capacity = bucket_capacity(max_entries);
279        Self {
280            entries: RawHashSlots::new(capacity, allocation),
281            controls: vec![0; capacity].into_boxed_slice(),
282            len: 0,
283        }
284    }
285
286    fn len(&self) -> usize {
287        self.len
288    }
289
290    fn contains(&self, hash: &[u8; 32]) -> bool {
291        if self.controls.is_empty() {
292            return false;
293        }
294
295        let mut idx = self.bucket_index(hash);
296        loop {
297            if self.controls[idx] == 0 {
298                return false;
299            }
300            if self.entries.get(idx) == hash {
301                return true;
302            }
303            idx = (idx + 1) & (self.controls.len() - 1);
304        }
305    }
306
307    fn insert(&mut self, hash: [u8; 32]) -> bool {
308        if self.controls.is_empty() {
309            return false;
310        }
311
312        let mut idx = self.bucket_index(&hash);
313        loop {
314            if self.controls[idx] == 0 {
315                // Publish occupancy only after the payload is initialized.
316                self.entries.write(idx, hash);
317                self.controls[idx] = 1;
318                self.len += 1;
319                return true;
320            }
321            if self.entries.get(idx) == &hash {
322                return false;
323            }
324            idx = (idx + 1) & (self.controls.len() - 1);
325        }
326    }
327
328    fn remove(&mut self, hash: &[u8; 32]) -> bool {
329        if self.controls.is_empty() {
330            return false;
331        }
332
333        let mut idx = self.bucket_index(hash);
334        loop {
335            if self.controls[idx] == 0 {
336                return false;
337            }
338            if self.entries.get(idx) == hash {
339                break;
340            }
341            idx = (idx + 1) & (self.controls.len() - 1);
342        }
343
344        self.controls[idx] = 0;
345        self.len -= 1;
346
347        let mut next = (idx + 1) & (self.controls.len() - 1);
348        while self.controls[next] != 0 {
349            let entry = self.entries.read(next);
350            self.controls[next] = 0;
351            self.len -= 1;
352            let inserted = self.insert(entry);
353            debug_assert!(inserted, "cluster reinsert after removal must succeed");
354            next = (next + 1) & (self.controls.len() - 1);
355        }
356
357        true
358    }
359
360    fn bucket_index(&self, hash: &[u8; 32]) -> usize {
361        debug_assert!(!self.controls.is_empty());
362        (hash_bytes(hash) as usize) & (self.controls.len() - 1)
363    }
364}
365
366fn bucket_capacity(max_entries: usize) -> usize {
367    if max_entries == 0 {
368        return 0;
369    }
370
371    let min_capacity = max_entries.saturating_mul(2).max(1);
372    min_capacity.next_power_of_two()
373}
374
375fn hash_bytes(hash: &[u8; 32]) -> u64 {
376    let mut state = 0xcbf29ce484222325u64;
377    for byte in hash {
378        state ^= u64::from(*byte);
379        state = state.wrapping_mul(0x100000001b3);
380    }
381    state
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387
388    fn make_hash(seed: u8) -> [u8; 32] {
389        let mut h = [0u8; 32];
390        h[0] = seed;
391        h
392    }
393
394    fn policies() -> [PacketHashlistAllocation; 2] {
395        [
396            PacketHashlistAllocation::Eager,
397            PacketHashlistAllocation::Lazy,
398        ]
399    }
400
401    #[test]
402    fn test_new_hash_not_duplicate() {
403        for policy in policies() {
404            let hl = PacketHashlist::with_allocation(100, policy);
405            assert!(!hl.is_duplicate(&make_hash(1)));
406        }
407    }
408
409    #[test]
410    fn test_added_hash_is_duplicate() {
411        for policy in policies() {
412            let mut hl = PacketHashlist::with_allocation(100, policy);
413            let h = make_hash(1);
414            hl.add(h);
415            assert!(hl.is_duplicate(&h));
416        }
417    }
418
419    #[test]
420    fn test_duplicate_insert_does_not_increase_len() {
421        for policy in policies() {
422            let mut hl = PacketHashlist::with_allocation(2, policy);
423            let h = make_hash(1);
424            hl.add(h);
425            hl.add(h);
426            assert_eq!(hl.len(), 1);
427            assert!(hl.is_duplicate(&h));
428        }
429    }
430
431    #[test]
432    fn test_full_hashlist_evicts_oldest_unique_hash() {
433        for policy in policies() {
434            let mut hl = PacketHashlist::with_allocation(3, policy);
435            let hashes = [make_hash(1), make_hash(2), make_hash(3), make_hash(4)];
436            for hash in hashes {
437                hl.add(hash);
438            }
439            assert!(!hl.is_duplicate(&hashes[0]));
440            assert!(hashes[1..].iter().all(|hash| hl.is_duplicate(hash)));
441            assert_eq!(hl.len(), 3);
442        }
443    }
444
445    #[test]
446    fn test_duplicate_does_not_refresh_recency() {
447        for policy in policies() {
448            let mut hl = PacketHashlist::with_allocation(2, policy);
449            let h1 = make_hash(1);
450            let h2 = make_hash(2);
451            let h3 = make_hash(3);
452            hl.add(h1);
453            hl.add(h2);
454            hl.add(h2);
455            hl.add(h3);
456            assert!(!hl.is_duplicate(&h1));
457            assert!(hl.is_duplicate(&h2));
458            assert!(hl.is_duplicate(&h3));
459        }
460    }
461
462    #[test]
463    fn removal_preserves_fifo_order_after_queue_wraps() {
464        for policy in policies() {
465            let mut hl = PacketHashlist::with_allocation(3, policy);
466            for seed in 1..=4 {
467                hl.add(make_hash(seed));
468            }
469
470            assert!(hl.remove(&make_hash(3)));
471            assert!(!hl.remove(&make_hash(1)));
472            assert_eq!(
473                hl.iter().copied().collect::<Vec<_>>(),
474                vec![make_hash(2), make_hash(4)]
475            );
476
477            hl.add(make_hash(5));
478            assert_eq!(
479                hl.iter().copied().collect::<Vec<_>>(),
480                vec![make_hash(2), make_hash(4), make_hash(5)]
481            );
482        }
483    }
484
485    #[test]
486    fn test_fifo_eviction_order_is_exact_across_multiple_inserts() {
487        for policy in policies() {
488            let mut hl = PacketHashlist::with_allocation(3, policy);
489            for seed in 1..=9 {
490                hl.add(make_hash(seed));
491            }
492            assert_eq!(
493                hl.iter().copied().collect::<Vec<_>>(),
494                vec![make_hash(7), make_hash(8), make_hash(9)]
495            );
496        }
497    }
498
499    #[test]
500    fn test_zero_capacity_hashlist_is_noop() {
501        for policy in policies() {
502            let mut hl = PacketHashlist::with_allocation(0, policy);
503            let h = make_hash(1);
504            hl.add(h);
505            assert_eq!(hl.len(), 0);
506            assert!(!hl.is_duplicate(&h));
507            assert_eq!(hl.iter().count(), 0);
508        }
509    }
510
511    #[test]
512    fn collision_cluster_removal_preserves_remaining_entries() {
513        for policy in policies() {
514            let mut set = PacketHashSet::new(3, policy);
515            let mut colliding = Vec::new();
516            for seed in 0..=u8::MAX {
517                let hash = make_hash(seed);
518                if hash_bytes(&hash) & 7 == 0 {
519                    colliding.push(hash);
520                    if colliding.len() == 3 {
521                        break;
522                    }
523                }
524            }
525            assert_eq!(colliding.len(), 3);
526            for hash in &colliding {
527                assert!(set.insert(*hash));
528            }
529            assert!(set.remove(&colliding[0]));
530            assert!(set.contains(&colliding[1]));
531            assert!(set.contains(&colliding[2]));
532        }
533    }
534
535    #[test]
536    fn raw_slots_read_only_after_write() {
537        for policy in policies() {
538            let mut slots = RawHashSlots::new(2, policy);
539            slots.write(1, make_hash(42));
540            assert_eq!(slots.read(1), make_hash(42));
541        }
542    }
543
544    // --- AnnounceSignatureCache tests ---
545
546    #[test]
547    fn test_sig_cache_insert_and_contains() {
548        let mut cache = AnnounceSignatureCache::new(100, 60.0);
549        let k = make_hash(1);
550        assert!(!cache.contains(&k));
551        cache.insert(k, 100.0);
552        assert!(cache.contains(&k));
553        assert_eq!(cache.len(), 1);
554    }
555
556    #[test]
557    fn test_sig_cache_duplicate_insert_is_noop() {
558        let mut cache = AnnounceSignatureCache::new(100, 60.0);
559        let k = make_hash(1);
560        cache.insert(k, 100.0);
561        cache.insert(k, 200.0);
562        assert_eq!(cache.len(), 1);
563    }
564
565    #[test]
566    fn test_sig_cache_ttl_expiry() {
567        let mut cache = AnnounceSignatureCache::new(100, 60.0);
568        cache.insert(make_hash(1), 100.0);
569        cache.insert(make_hash(2), 150.0);
570
571        // At t=155, entry 1 (age=55) is still within TTL, entry 2 (age=5) too
572        assert_eq!(cache.cull(155.0), 0);
573        assert_eq!(cache.len(), 2);
574
575        // At t=161, entry 1 (age=61) expired, entry 2 (age=11) still valid
576        assert_eq!(cache.cull(161.0), 1);
577        assert_eq!(cache.len(), 1);
578        assert!(!cache.contains(&make_hash(1)));
579        assert!(cache.contains(&make_hash(2)));
580    }
581
582    #[test]
583    fn test_sig_cache_capacity_eviction() {
584        let mut cache = AnnounceSignatureCache::new(2, 600.0);
585        cache.insert(make_hash(1), 100.0);
586        cache.insert(make_hash(2), 101.0);
587        cache.insert(make_hash(3), 102.0); // should evict hash(1)
588
589        assert_eq!(cache.len(), 2);
590        assert!(!cache.contains(&make_hash(1)));
591        assert!(cache.contains(&make_hash(2)));
592        assert!(cache.contains(&make_hash(3)));
593    }
594
595    #[test]
596    fn test_sig_cache_disabled_when_zero_capacity() {
597        let mut cache = AnnounceSignatureCache::new(0, 60.0);
598        let k = make_hash(1);
599        cache.insert(k, 100.0);
600        assert!(!cache.contains(&k));
601        assert_eq!(cache.len(), 0);
602        assert_eq!(cache.cull(200.0), 0);
603    }
604}