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