Skip to main content

rns_core/transport/
pathfinder.rs

1use super::tables::{PathEntry, PathSet};
2use crate::constants;
3
4/// Extract emission timestamp from bytes [5:10] of a random_blob (big-endian u64).
5pub fn timebase_from_random_blob(blob: &[u8; 10]) -> u64 {
6    let mut bytes = [0u8; 8];
7    bytes[3..8].copy_from_slice(&blob[5..10]);
8    u64::from_be_bytes(bytes)
9}
10
11/// Maximum emission timestamp across all random blobs.
12pub fn timebase_from_random_blobs(blobs: &[[u8; 10]]) -> u64 {
13    let mut timebase: u64 = 0;
14    for blob in blobs {
15        let emitted = timebase_from_random_blob(blob);
16        if emitted > timebase {
17            timebase = emitted;
18        }
19    }
20    timebase
21}
22
23/// Extract the random_blob from announce packet data.
24///
25/// Located at offset `KEYSIZE/8 + NAME_HASH_LENGTH/8` = 64 + 10 = 74,
26/// length 10 bytes.
27pub fn extract_random_blob(packet_data: &[u8]) -> Option<[u8; 10]> {
28    let offset = constants::KEYSIZE / 8 + constants::NAME_HASH_LENGTH / 8;
29    if packet_data.len() < offset + 10 {
30        return None;
31    }
32    let mut blob = [0u8; 10];
33    blob.copy_from_slice(&packet_data[offset..offset + 10]);
34    Some(blob)
35}
36
37#[derive(Debug, PartialEq, Eq)]
38pub enum PathDecision {
39    Add,
40    Reject,
41}
42
43/// Full path update decision tree from Transport.py:1604-1686.
44///
45/// Determines whether an incoming announce should update the path table.
46pub fn should_update_path(
47    existing: Option<&PathEntry>,
48    announce_hops: u8,
49    announce_emitted_ts: u64,
50    random_blob: &[u8; 10],
51    path_is_unresponsive: bool,
52    now: f64,
53    prefer_shorter_path: bool,
54) -> PathDecision {
55    // Hop limit
56    if announce_hops > constants::PATHFINDER_M {
57        return PathDecision::Reject;
58    }
59
60    let existing = match existing {
61        None => return PathDecision::Add,
62        Some(e) => e,
63    };
64
65    let path_timebase = timebase_from_random_blobs(&existing.random_blobs);
66    let blob_is_new = !existing.random_blobs.contains(random_blob);
67
68    if announce_hops <= existing.hops {
69        // Accept strictly shorter path even with duplicate blob
70        if prefer_shorter_path && announce_hops < existing.hops {
71            return PathDecision::Add;
72        }
73        // Equal or fewer hops: accept if new blob AND newer emission
74        if blob_is_new && announce_emitted_ts > path_timebase {
75            return PathDecision::Add;
76        }
77        // Same emission + unresponsive path: accept for path recovery
78        if announce_emitted_ts == path_timebase && path_is_unresponsive {
79            return PathDecision::Add;
80        }
81        PathDecision::Reject
82    } else {
83        // More hops than existing path
84        let path_expired = now >= existing.expires;
85
86        if path_expired && blob_is_new {
87            return PathDecision::Add;
88        }
89
90        if announce_emitted_ts > path_timebase && blob_is_new {
91            return PathDecision::Add;
92        }
93
94        if announce_emitted_ts == path_timebase && path_is_unresponsive {
95            return PathDecision::Add;
96        }
97
98        PathDecision::Reject
99    }
100}
101
102/// Decision for multi-path announce processing.
103#[derive(Debug, PartialEq, Eq)]
104pub enum MultiPathDecision {
105    /// Replace/update the primary path (or the path with the same next_hop).
106    ReplacePrimary,
107    /// Accept as an alternative path via a new next_hop.
108    AddAlternative,
109    /// Reject this announce.
110    Reject,
111}
112
113/// Multi-path aware announce decision.
114///
115/// Determines whether an incoming announce should update the primary path,
116/// be stored as an alternative, or be rejected.
117///
118/// - No existing `PathSet` → `ReplacePrimary` (first path for this dest)
119/// - Same `next_hop` exists in the set → delegate to `should_update_path`
120///   against **that** specific path entry
121/// - New `next_hop` → accept as `AddAlternative` if the blob is genuinely
122///   new (not in any stored path's blobs) and emission timestamp is valid
123#[allow(clippy::too_many_arguments)]
124pub fn decide_announce_multipath(
125    existing_set: Option<&PathSet>,
126    announce_hops: u8,
127    announce_emitted_ts: u64,
128    random_blob: &[u8; 10],
129    next_hop: &[u8; 16],
130    path_is_unresponsive: bool,
131    now: f64,
132    prefer_shorter_path: bool,
133) -> MultiPathDecision {
134    // Hop limit
135    if announce_hops > constants::PATHFINDER_M {
136        return MultiPathDecision::Reject;
137    }
138
139    let path_set = match existing_set {
140        None => return MultiPathDecision::ReplacePrimary,
141        Some(ps) if ps.is_empty() => return MultiPathDecision::ReplacePrimary,
142        Some(ps) => ps,
143    };
144
145    // Check if there's already a path with the same next_hop
146    if let Some(existing_path) = path_set.find_by_next_hop(next_hop) {
147        let decision = should_update_path(
148            Some(existing_path),
149            announce_hops,
150            announce_emitted_ts,
151            random_blob,
152            path_is_unresponsive,
153            now,
154            prefer_shorter_path,
155        );
156        match decision {
157            PathDecision::Add => MultiPathDecision::ReplacePrimary,
158            PathDecision::Reject => MultiPathDecision::Reject,
159        }
160    } else {
161        // New next_hop — check if blob is genuinely new across all paths
162        let all_blobs = path_set.all_random_blobs();
163        let blob_is_new = !all_blobs.contains(random_blob);
164
165        if !blob_is_new {
166            return MultiPathDecision::Reject;
167        }
168
169        let max_timebase = timebase_from_random_blobs(&all_blobs);
170        if announce_emitted_ts >= max_timebase {
171            MultiPathDecision::AddAlternative
172        } else {
173            MultiPathDecision::Reject
174        }
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::super::types::InterfaceId;
181    use super::*;
182
183    fn make_blob(timebase: u64) -> [u8; 10] {
184        let mut blob = [0u8; 10];
185        let bytes = timebase.to_be_bytes();
186        // timebase is stored in blob[5..10] = last 5 bytes of u64
187        blob[5..10].copy_from_slice(&bytes[3..8]);
188        blob
189    }
190
191    fn make_path_entry(hops: u8, blobs: &[[u8; 10]], expires: f64) -> PathEntry {
192        PathEntry {
193            timestamp: 1000.0,
194            next_hop: [0xAA; 16],
195            hops,
196            expires,
197            random_blobs: blobs.to_vec(),
198            receiving_interface: InterfaceId(1),
199            packet_hash: [0xBB; 32],
200            announce_raw: None,
201        }
202    }
203
204    #[test]
205    fn test_timebase_extraction() {
206        let blob = make_blob(12345);
207        assert_eq!(timebase_from_random_blob(&blob), 12345);
208    }
209
210    #[test]
211    fn test_timebase_from_multiple_blobs() {
212        let b1 = make_blob(100);
213        let b2 = make_blob(200);
214        let b3 = make_blob(50);
215        assert_eq!(timebase_from_random_blobs(&[b1, b2, b3]), 200);
216    }
217
218    #[test]
219    fn test_timebase_empty_blobs() {
220        assert_eq!(timebase_from_random_blobs(&[]), 0);
221    }
222
223    #[test]
224    fn test_extract_random_blob() {
225        // Need at least 74 + 10 = 84 bytes
226        let mut data = [0u8; 100];
227        // Put a known blob at offset 74
228        data[74..84].copy_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
229        let blob = extract_random_blob(&data).unwrap();
230        assert_eq!(blob, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
231    }
232
233    #[test]
234    fn test_extract_random_blob_too_short() {
235        let data = [0u8; 80]; // too short
236        assert!(extract_random_blob(&data).is_none());
237    }
238
239    // --- Decision tree tests ---
240
241    #[test]
242    fn test_no_existing_path_always_add() {
243        let blob = make_blob(100);
244        assert_eq!(
245            should_update_path(None, 3, 100, &blob, false, 1000.0, false),
246            PathDecision::Add
247        );
248    }
249
250    #[test]
251    fn test_hop_limit_reject() {
252        let blob = make_blob(100);
253        assert_eq!(
254            should_update_path(None, 129, 100, &blob, false, 1000.0, false),
255            PathDecision::Reject
256        );
257    }
258
259    #[test]
260    fn test_fewer_hops_new_blob_newer_emission_add() {
261        let old_blob = make_blob(100);
262        let new_blob = make_blob(200);
263        let entry = make_path_entry(5, &[old_blob], 9999.0);
264        assert_eq!(
265            should_update_path(Some(&entry), 3, 200, &new_blob, false, 1000.0, false),
266            PathDecision::Add
267        );
268    }
269
270    #[test]
271    fn test_fewer_hops_duplicate_blob_reject() {
272        let blob = make_blob(100);
273        let entry = make_path_entry(5, &[blob], 9999.0);
274        assert_eq!(
275            should_update_path(Some(&entry), 3, 200, &blob, false, 1000.0, false),
276            PathDecision::Reject
277        );
278    }
279
280    #[test]
281    fn test_fewer_hops_same_emission_unresponsive_add() {
282        let old_blob = make_blob(100);
283        let mut different_blob = [0u8; 10];
284        different_blob[0] = 0xFF;
285        different_blob[5..10].copy_from_slice(&100u64.to_be_bytes()[3..8]);
286
287        let entry = make_path_entry(5, &[old_blob], 9999.0);
288        assert_eq!(
289            should_update_path(Some(&entry), 3, 100, &different_blob, true, 1000.0, false),
290            PathDecision::Add
291        );
292    }
293
294    #[test]
295    fn test_fewer_hops_same_emission_responsive_reject() {
296        let old_blob = make_blob(100);
297        let mut different_blob = [0u8; 10];
298        different_blob[0] = 0xFF;
299        different_blob[5..10].copy_from_slice(&100u64.to_be_bytes()[3..8]);
300
301        let entry = make_path_entry(5, &[old_blob], 9999.0);
302        assert_eq!(
303            should_update_path(Some(&entry), 3, 100, &different_blob, false, 1000.0, false),
304            PathDecision::Reject
305        );
306    }
307
308    #[test]
309    fn test_more_hops_expired_path_new_blob_add() {
310        let old_blob = make_blob(100);
311        let new_blob = make_blob(50); // older emission but path expired
312        let entry = make_path_entry(2, &[old_blob], 500.0); // expires at 500
313
314        assert_eq!(
315            should_update_path(Some(&entry), 5, 50, &new_blob, false, 600.0, false), // now > expires
316            PathDecision::Add
317        );
318    }
319
320    #[test]
321    fn test_more_hops_not_expired_older_emission_reject() {
322        let old_blob = make_blob(200);
323        let new_blob = make_blob(100);
324        let entry = make_path_entry(2, &[old_blob], 9999.0);
325
326        assert_eq!(
327            should_update_path(Some(&entry), 5, 100, &new_blob, false, 1000.0, false),
328            PathDecision::Reject
329        );
330    }
331
332    #[test]
333    fn test_more_hops_newer_emission_new_blob_add() {
334        let old_blob = make_blob(100);
335        let new_blob = make_blob(200);
336        let entry = make_path_entry(2, &[old_blob], 9999.0);
337
338        assert_eq!(
339            should_update_path(Some(&entry), 5, 200, &new_blob, false, 1000.0, false),
340            PathDecision::Add
341        );
342    }
343
344    #[test]
345    fn test_more_hops_same_emission_unresponsive_add() {
346        let old_blob = make_blob(100);
347        let mut different_blob = [0u8; 10];
348        different_blob[0] = 0xFF;
349        different_blob[5..10].copy_from_slice(&100u64.to_be_bytes()[3..8]);
350
351        let entry = make_path_entry(2, &[old_blob], 9999.0);
352
353        assert_eq!(
354            should_update_path(Some(&entry), 5, 100, &different_blob, true, 1000.0, false),
355            PathDecision::Add
356        );
357    }
358
359    #[test]
360    fn test_more_hops_same_emission_responsive_reject() {
361        let old_blob = make_blob(100);
362        let mut different_blob = [0u8; 10];
363        different_blob[0] = 0xFF;
364        different_blob[5..10].copy_from_slice(&100u64.to_be_bytes()[3..8]);
365
366        let entry = make_path_entry(2, &[old_blob], 9999.0);
367
368        assert_eq!(
369            should_update_path(Some(&entry), 5, 100, &different_blob, false, 1000.0, false),
370            PathDecision::Reject
371        );
372    }
373
374    #[test]
375    fn test_more_hops_duplicate_blob_reject() {
376        let blob = make_blob(200);
377        let entry = make_path_entry(2, &[blob], 9999.0);
378
379        assert_eq!(
380            should_update_path(Some(&entry), 5, 200, &blob, false, 1000.0, false),
381            PathDecision::Reject
382        );
383    }
384
385    #[test]
386    fn test_equal_hops_new_blob_newer_emission_add() {
387        let old_blob = make_blob(100);
388        let new_blob = make_blob(200);
389        let entry = make_path_entry(3, &[old_blob], 9999.0);
390
391        assert_eq!(
392            should_update_path(Some(&entry), 3, 200, &new_blob, false, 1000.0, false),
393            PathDecision::Add
394        );
395    }
396
397    // --- prefer_shorter_path tests ---
398
399    #[test]
400    fn test_prefer_shorter_path_strictly_fewer_hops_duplicate_blob_add() {
401        let blob = make_blob(100);
402        let entry = make_path_entry(5, &[blob], 9999.0);
403        assert_eq!(
404            should_update_path(Some(&entry), 3, 100, &blob, false, 1000.0, true),
405            PathDecision::Add
406        );
407    }
408
409    #[test]
410    fn test_prefer_shorter_path_equal_hops_duplicate_blob_reject() {
411        // Equal hops with same blob: no benefit, still rejected
412        let blob = make_blob(100);
413        let entry = make_path_entry(3, &[blob], 9999.0);
414        assert_eq!(
415            should_update_path(Some(&entry), 3, 100, &blob, false, 1000.0, true),
416            PathDecision::Reject
417        );
418    }
419
420    #[test]
421    fn test_prefer_shorter_path_more_hops_duplicate_blob_reject() {
422        // More hops: prefer_shorter_path does not help
423        let blob = make_blob(100);
424        let entry = make_path_entry(2, &[blob], 9999.0);
425        assert_eq!(
426            should_update_path(Some(&entry), 5, 100, &blob, false, 1000.0, true),
427            PathDecision::Reject
428        );
429    }
430
431    // --- MultiPathDecision tests ---
432
433    #[test]
434    fn test_multipath_no_existing_set_replace_primary() {
435        let blob = make_blob(100);
436        assert_eq!(
437            decide_announce_multipath(None, 3, 100, &blob, &[0xBB; 16], false, 1000.0, false),
438            MultiPathDecision::ReplacePrimary
439        );
440    }
441
442    #[test]
443    fn test_multipath_same_nexthop_update() {
444        let blob_old = make_blob(100);
445        let blob_new = make_blob(200);
446        let entry = make_path_entry(3, &[blob_old], 9999.0);
447        let ps = PathSet::from_single(entry, 3);
448
449        // Same next_hop, newer blob → ReplacePrimary
450        assert_eq!(
451            decide_announce_multipath(
452                Some(&ps),
453                2,
454                200,
455                &blob_new,
456                &[0xAA; 16],
457                false,
458                1000.0,
459                false
460            ),
461            MultiPathDecision::ReplacePrimary
462        );
463    }
464
465    #[test]
466    fn test_multipath_same_nexthop_reject() {
467        let blob = make_blob(100);
468        let entry = make_path_entry(3, &[blob], 9999.0);
469        let ps = PathSet::from_single(entry, 3);
470
471        // Same next_hop, same blob → Reject
472        assert_eq!(
473            decide_announce_multipath(Some(&ps), 3, 100, &blob, &[0xAA; 16], false, 1000.0, false),
474            MultiPathDecision::Reject
475        );
476    }
477
478    #[test]
479    fn test_multipath_new_nexthop_novel_blob_add_alternative() {
480        let blob_existing = make_blob(100);
481        let blob_new = make_blob(200);
482        let entry = make_path_entry(3, &[blob_existing], 9999.0);
483        let ps = PathSet::from_single(entry, 3);
484
485        // Different next_hop, novel blob, newer emission → AddAlternative
486        assert_eq!(
487            decide_announce_multipath(
488                Some(&ps),
489                4,
490                200,
491                &blob_new,
492                &[0xCC; 16],
493                false,
494                1000.0,
495                false
496            ),
497            MultiPathDecision::AddAlternative
498        );
499    }
500
501    #[test]
502    fn test_multipath_new_nexthop_known_blob_reject() {
503        let blob = make_blob(100);
504        let entry = make_path_entry(3, &[blob], 9999.0);
505        let ps = PathSet::from_single(entry, 3);
506
507        // Different next_hop but blob already known → Reject
508        assert_eq!(
509            decide_announce_multipath(Some(&ps), 4, 100, &blob, &[0xCC; 16], false, 1000.0, false),
510            MultiPathDecision::Reject
511        );
512    }
513
514    #[test]
515    fn test_multipath_new_nexthop_older_emission_reject() {
516        let blob_existing = make_blob(200);
517        let blob_new = make_blob(100); // older emission
518        let entry = make_path_entry(3, &[blob_existing], 9999.0);
519        let ps = PathSet::from_single(entry, 3);
520
521        // Novel blob but older emission timestamp → Reject
522        assert_eq!(
523            decide_announce_multipath(
524                Some(&ps),
525                4,
526                100,
527                &blob_new,
528                &[0xCC; 16],
529                false,
530                1000.0,
531                false
532            ),
533            MultiPathDecision::Reject
534        );
535    }
536
537    #[test]
538    fn test_multipath_hop_limit_reject() {
539        let blob = make_blob(100);
540        assert_eq!(
541            decide_announce_multipath(None, 129, 100, &blob, &[0xBB; 16], false, 1000.0, false),
542            MultiPathDecision::Reject
543        );
544    }
545}