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