Skip to main content

rns_core/transport/
tables.rs

1use alloc::vec::Vec;
2
3use super::types::InterfaceId;
4
5/// Entry in the path table, keyed by destination_hash.
6#[derive(Debug, Clone)]
7pub struct PathEntry {
8    pub timestamp: f64,
9    pub next_hop: [u8; 16],
10    pub hops: u8,
11    pub expires: f64,
12    pub random_blobs: Vec<[u8; 10]>,
13    pub receiving_interface: InterfaceId,
14    pub packet_hash: [u8; 32],
15    /// Last time this path was used for outbound traffic; 0 means never used.
16    pub used: f64,
17    /// Original announce raw bytes (pre-hop-increment) for cache/retransmission.
18    pub announce_raw: Option<Vec<u8>>,
19}
20
21/// Entry in the announce table, keyed by destination_hash.
22#[derive(Debug, Clone)]
23pub struct AnnounceEntry {
24    pub timestamp: f64,
25    pub retransmit_timeout: f64,
26    pub retries: u8,
27    pub received_from: [u8; 16],
28    pub hops: u8,
29    pub packet_raw: Vec<u8>,
30    pub packet_data: Vec<u8>,
31    pub destination_hash: [u8; 16],
32    pub context_flag: u8,
33    pub local_rebroadcasts: u8,
34    pub block_rebroadcasts: bool,
35    pub attached_interface: Option<InterfaceId>,
36}
37
38/// Reverse-path entries keyed by truncated packet hash.
39///
40/// Senders can grind packets for chosen hashes, so the table's hasher must
41/// be keyed with a secret seed; see [`TransportEngine::seed_table_hashing`].
42///
43/// [`TransportEngine::seed_table_hashing`]: super::TransportEngine::seed_table_hashing
44pub type ReverseTable = hashbrown::HashMap<[u8; 16], ReverseEntry, foldhash::fast::FixedState>;
45
46/// Entry in the reverse table, keyed by truncated packet hash.
47#[derive(Debug, Clone)]
48pub struct ReverseEntry {
49    pub receiving_interface: InterfaceId,
50    pub outbound_interface: InterfaceId,
51    pub timestamp: f64,
52}
53
54/// Entry in the link table, keyed by link_id.
55#[derive(Debug, Clone)]
56pub struct LinkEntry {
57    pub timestamp: f64,
58    pub next_hop_transport_id: [u8; 16],
59    pub next_hop_interface: InterfaceId,
60    pub remaining_hops: u8,
61    pub received_interface: InterfaceId,
62    pub taken_hops: u8,
63    pub destination_hash: [u8; 16],
64    pub validated: bool,
65    pub proof_timeout: f64,
66}
67
68/// A pending discovery path request — stored when a path request arrives
69/// on a DISCOVER_PATHS_FOR interface for an unknown destination.
70#[derive(Debug, Clone)]
71pub struct DiscoveryPathRequest {
72    pub timestamp: f64,
73    pub requesting_interfaces: Vec<InterfaceId>,
74    pub engaged: bool,
75}
76
77/// Entry in the announce rate table, keyed by destination_hash.
78#[derive(Debug, Clone)]
79pub struct RateEntry {
80    pub last: f64,
81    pub rate_violations: u32,
82    pub blocked_until: f64,
83    pub timestamps: Vec<f64>,
84}
85
86/// A bounded set of alternative paths for a single destination.
87///
88/// `paths[0]` is always the *primary* (best) path.  Ranking: lowest hops
89/// first, then most-recent timestamp.
90#[derive(Debug, Clone)]
91pub struct PathSet {
92    paths: Vec<PathEntry>,
93    capacity: usize,
94}
95
96impl PathSet {
97    /// Create a new PathSet containing a single path.
98    pub fn from_single(entry: PathEntry, capacity: usize) -> Self {
99        PathSet {
100            paths: alloc::vec![entry],
101            capacity: capacity.max(1),
102        }
103    }
104
105    /// Access the primary (best) path, if any.
106    pub fn primary(&self) -> Option<&PathEntry> {
107        self.paths.first()
108    }
109
110    /// Mutable access to the primary path.
111    pub fn primary_mut(&mut self) -> Option<&mut PathEntry> {
112        self.paths.first_mut()
113    }
114
115    /// Update the current primary path's hop metric and restore path ranking.
116    pub fn update_primary_hops(&mut self, hops: u8) -> bool {
117        let Some(primary) = self.paths.first_mut() else {
118            return false;
119        };
120        primary.hops = hops;
121        self.sort();
122        true
123    }
124
125    /// Whether the set contains any paths.
126    pub fn is_empty(&self) -> bool {
127        self.paths.is_empty()
128    }
129
130    /// Number of stored paths.
131    pub fn len(&self) -> usize {
132        self.paths.len()
133    }
134
135    /// Iterator over all paths (primary first).
136    pub fn iter(&self) -> impl Iterator<Item = &PathEntry> {
137        self.paths.iter()
138    }
139
140    /// Insert or update a path entry.
141    ///
142    /// - If a path with the same `next_hop` already exists, it is replaced in-place.
143    /// - Otherwise the entry is added as an alternative.  If at capacity the
144    ///   worst path (highest hops, then oldest) is evicted.
145    ///
146    /// After mutation the vector is re-sorted so `paths[0]` remains the best.
147    pub fn upsert(&mut self, entry: PathEntry) {
148        // Check for existing same-next_hop path
149        if let Some(pos) = self.paths.iter().position(|p| p.next_hop == entry.next_hop) {
150            self.paths[pos] = entry;
151        } else if self.paths.len() < self.capacity {
152            self.paths.push(entry);
153        } else {
154            // At capacity — evict worst (last after sort, but we haven't sorted
155            // the new entry yet).  Replace worst if new entry is better.
156            // We push then sort then truncate, which is simple and correct.
157            self.paths.push(entry);
158        }
159        self.sort();
160        self.paths.truncate(self.capacity);
161    }
162
163    /// Insert or update a path and make it primary among otherwise equal paths.
164    pub fn upsert_primary(&mut self, entry: PathEntry) {
165        if let Some(pos) = self
166            .paths
167            .iter()
168            .position(|path| path.next_hop == entry.next_hop)
169        {
170            self.paths.remove(pos);
171        }
172        self.paths.insert(0, entry);
173        self.sort();
174        self.paths.truncate(self.capacity);
175    }
176
177    /// Promote the next-best path after the current primary becomes
178    /// unresponsive.
179    ///
180    /// If `remove` is true the old primary is discarded; otherwise it is
181    /// moved to the back of the list (it may recover later).
182    pub fn failover(&mut self, remove: bool) {
183        if self.paths.len() <= 1 {
184            return;
185        }
186        if remove {
187            self.paths.remove(0);
188        } else {
189            let old_primary = self.paths.remove(0);
190            self.paths.push(old_primary);
191        }
192    }
193
194    /// Remove expired or orphaned paths.
195    ///
196    /// `interface_exists` is a predicate that checks whether an interface is
197    /// still registered.
198    pub fn cull(&mut self, now: f64, interface_exists: impl Fn(&InterfaceId) -> bool) {
199        self.paths
200            .retain(|p| now <= p.expires && interface_exists(&p.receiving_interface));
201    }
202
203    /// Filter paths by a predicate, keeping only those that match.
204    pub fn retain(&mut self, predicate: impl Fn(&PathEntry) -> bool) {
205        self.paths.retain(predicate);
206    }
207
208    /// Expire all paths in this set (set timestamp/expires to 0).
209    pub fn expire_all(&mut self) {
210        for p in &mut self.paths {
211            p.timestamp = 0.0;
212            p.expires = 0.0;
213        }
214    }
215
216    /// Collect all random_blobs across every path in this set.
217    pub fn all_random_blobs(&self) -> Vec<[u8; 10]> {
218        let mut blobs = Vec::new();
219        for p in &self.paths {
220            blobs.extend_from_slice(&p.random_blobs);
221        }
222        blobs
223    }
224
225    /// Find the path entry that matches a given `next_hop`, if any.
226    pub fn find_by_next_hop(&self, next_hop: &[u8; 16]) -> Option<&PathEntry> {
227        self.paths.iter().find(|p| &p.next_hop == next_hop)
228    }
229
230    /// Sort: lowest hops first, then most-recent timestamp first.
231    fn sort(&mut self) {
232        self.paths.sort_by(|a, b| {
233            a.hops.cmp(&b.hops).then_with(|| {
234                b.timestamp
235                    .partial_cmp(&a.timestamp)
236                    .unwrap_or(core::cmp::Ordering::Equal)
237            })
238        });
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn test_path_entry_creation() {
248        let entry = PathEntry {
249            timestamp: 1000.0,
250            next_hop: [0xAA; 16],
251            hops: 3,
252            expires: 2000.0,
253            random_blobs: Vec::new(),
254            receiving_interface: InterfaceId(1),
255            packet_hash: [0xBB; 32],
256            announce_raw: None,
257            used: 0.0,
258        };
259        assert_eq!(entry.hops, 3);
260        assert_eq!(entry.receiving_interface, InterfaceId(1));
261    }
262
263    #[test]
264    fn upsert_primary_preserves_alternative_and_wins_equal_rank_tie() {
265        let first = PathEntry {
266            timestamp: 1000.0,
267            next_hop: [0xAA; 16],
268            hops: 3,
269            expires: 2000.0,
270            random_blobs: vec![[1; 10]],
271            receiving_interface: InterfaceId(1),
272            packet_hash: [0x11; 32],
273            announce_raw: None,
274            used: 0.0,
275        };
276        let preferred = PathEntry {
277            next_hop: [0xBB; 16],
278            receiving_interface: InterfaceId(2),
279            packet_hash: [0x22; 32],
280            ..first.clone()
281        };
282        let mut paths = PathSet::from_single(first, 3);
283
284        paths.upsert_primary(preferred);
285
286        assert_eq!(paths.len(), 2);
287        assert_eq!(paths.primary().unwrap().receiving_interface, InterfaceId(2));
288    }
289
290    #[test]
291    fn test_link_entry_creation() {
292        let entry = LinkEntry {
293            timestamp: 100.0,
294            next_hop_transport_id: [0x11; 16],
295            next_hop_interface: InterfaceId(2),
296            remaining_hops: 5,
297            received_interface: InterfaceId(3),
298            taken_hops: 2,
299            destination_hash: [0x22; 16],
300            validated: false,
301            proof_timeout: 200.0,
302        };
303        assert!(!entry.validated);
304        assert_eq!(entry.remaining_hops, 5);
305    }
306
307    #[test]
308    fn test_rate_entry_creation() {
309        let entry = RateEntry {
310            last: 50.0,
311            rate_violations: 0,
312            blocked_until: 0.0,
313            timestamps: Vec::new(),
314        };
315        assert_eq!(entry.rate_violations, 0);
316    }
317
318    // =========================================================================
319    // PathSet tests
320    // =========================================================================
321
322    fn make_path(next_hop: [u8; 16], hops: u8, timestamp: f64, expires: f64) -> PathEntry {
323        PathEntry {
324            timestamp,
325            next_hop,
326            hops,
327            expires,
328            random_blobs: Vec::new(),
329            receiving_interface: InterfaceId(1),
330            packet_hash: [0; 32],
331            used: 0.0,
332            announce_raw: None,
333        }
334    }
335
336    #[test]
337    fn test_pathset_from_single() {
338        let ps = PathSet::from_single(make_path([1; 16], 3, 100.0, 9999.0), 3);
339        assert_eq!(ps.len(), 1);
340        assert_eq!(ps.primary().unwrap().hops, 3);
341    }
342
343    #[test]
344    fn test_pathset_upsert_same_nexthop_replaces() {
345        let mut ps = PathSet::from_single(make_path([1; 16], 3, 100.0, 9999.0), 3);
346        ps.upsert(make_path([1; 16], 2, 200.0, 9999.0));
347        assert_eq!(ps.len(), 1);
348        assert_eq!(ps.primary().unwrap().hops, 2);
349        assert_eq!(ps.primary().unwrap().timestamp, 200.0);
350    }
351
352    #[test]
353    fn test_pathset_upsert_new_nexthop_adds_alternative() {
354        let mut ps = PathSet::from_single(make_path([1; 16], 3, 100.0, 9999.0), 3);
355        ps.upsert(make_path([2; 16], 2, 200.0, 9999.0));
356        assert_eq!(ps.len(), 2);
357        // Best path (fewer hops) should be primary
358        assert_eq!(ps.primary().unwrap().next_hop, [2; 16]);
359    }
360
361    #[test]
362    fn test_pathset_capacity_eviction() {
363        let mut ps = PathSet::from_single(make_path([1; 16], 1, 100.0, 9999.0), 2);
364        ps.upsert(make_path([2; 16], 2, 200.0, 9999.0));
365        ps.upsert(make_path([3; 16], 3, 300.0, 9999.0));
366        // Capacity is 2, worst (3 hops) should be evicted
367        assert_eq!(ps.len(), 2);
368        assert_eq!(ps.primary().unwrap().next_hop, [1; 16]);
369        assert!(ps.find_by_next_hop(&[3; 16]).is_none());
370    }
371
372    #[test]
373    fn test_pathset_failover_promotes_second() {
374        let mut ps = PathSet::from_single(make_path([1; 16], 1, 100.0, 9999.0), 3);
375        ps.upsert(make_path([2; 16], 2, 200.0, 9999.0));
376
377        ps.failover(false); // demote, don't remove
378        assert_eq!(ps.primary().unwrap().next_hop, [2; 16]);
379        assert_eq!(ps.len(), 2); // old primary moved to back
380    }
381
382    #[test]
383    fn test_pathset_failover_with_remove() {
384        let mut ps = PathSet::from_single(make_path([1; 16], 1, 100.0, 9999.0), 3);
385        ps.upsert(make_path([2; 16], 2, 200.0, 9999.0));
386
387        ps.failover(true); // remove old primary
388        assert_eq!(ps.primary().unwrap().next_hop, [2; 16]);
389        assert_eq!(ps.len(), 1);
390    }
391
392    #[test]
393    fn test_pathset_sort_ordering() {
394        let mut ps = PathSet::from_single(make_path([1; 16], 5, 300.0, 9999.0), 4);
395        ps.upsert(make_path([2; 16], 2, 100.0, 9999.0));
396        ps.upsert(make_path([3; 16], 2, 200.0, 9999.0));
397        ps.upsert(make_path([4; 16], 3, 400.0, 9999.0));
398
399        let hops: Vec<u8> = ps.iter().map(|p| p.hops).collect();
400        // Sorted by hops asc, then timestamp desc within same hops
401        assert_eq!(hops, vec![2, 2, 3, 5]);
402        // Among the 2-hop paths, newer timestamp first
403        assert_eq!(ps.paths[0].next_hop, [3; 16]); // timestamp 200
404        assert_eq!(ps.paths[1].next_hop, [2; 16]); // timestamp 100
405    }
406
407    #[test]
408    fn test_pathset_cull_removes_expired() {
409        let mut ps = PathSet::from_single(make_path([1; 16], 1, 100.0, 500.0), 3);
410        ps.upsert(make_path([2; 16], 2, 200.0, 9999.0));
411
412        ps.cull(600.0, |_| true); // now > 500 for first path
413        assert_eq!(ps.len(), 1);
414        assert_eq!(ps.primary().unwrap().next_hop, [2; 16]);
415    }
416
417    #[test]
418    fn test_pathset_cull_removes_orphaned_interface() {
419        let mut ps = PathSet::from_single(make_path([1; 16], 1, 100.0, 9999.0), 3);
420        ps.cull(200.0, |id| id.0 != 1); // interface 1 doesn't exist
421        assert!(ps.is_empty());
422    }
423
424    #[test]
425    fn test_pathset_retain_filters() {
426        let mut ps = PathSet::from_single(make_path([1; 16], 1, 100.0, 9999.0), 3);
427        ps.upsert(make_path([2; 16], 2, 200.0, 9999.0));
428
429        ps.retain(|p| p.next_hop != [1; 16]);
430        assert_eq!(ps.len(), 1);
431        assert_eq!(ps.primary().unwrap().next_hop, [2; 16]);
432    }
433
434    #[test]
435    fn test_pathset_expire_all() {
436        let mut ps = PathSet::from_single(make_path([1; 16], 1, 100.0, 9999.0), 3);
437        ps.upsert(make_path([2; 16], 2, 200.0, 9999.0));
438
439        ps.expire_all();
440        for p in ps.iter() {
441            assert_eq!(p.timestamp, 0.0);
442            assert_eq!(p.expires, 0.0);
443        }
444    }
445
446    #[test]
447    fn test_pathset_all_random_blobs() {
448        let mut e1 = make_path([1; 16], 1, 100.0, 9999.0);
449        e1.random_blobs = alloc::vec![[0xAA; 10]];
450        let mut e2 = make_path([2; 16], 2, 200.0, 9999.0);
451        e2.random_blobs = alloc::vec![[0xBB; 10], [0xCC; 10]];
452
453        let mut ps = PathSet::from_single(e1, 3);
454        ps.upsert(e2);
455
456        let blobs = ps.all_random_blobs();
457        assert_eq!(blobs.len(), 3);
458    }
459
460    #[test]
461    fn test_pathset_failover_single_path_noop() {
462        let mut ps = PathSet::from_single(make_path([1; 16], 1, 100.0, 9999.0), 3);
463        ps.failover(false);
464        assert_eq!(ps.len(), 1);
465        assert_eq!(ps.primary().unwrap().next_hop, [1; 16]);
466    }
467}