Skip to main content

rns_core/transport/
pathfinder.rs

1use super::tables::PathEntry;
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) -> PathDecision {
54    // Hop limit
55    if announce_hops >= constants::PATHFINDER_M + 1 {
56        return PathDecision::Reject;
57    }
58
59    let existing = match existing {
60        None => return PathDecision::Add,
61        Some(e) => e,
62    };
63
64    let path_timebase = timebase_from_random_blobs(&existing.random_blobs);
65    let blob_is_new = !existing.random_blobs.contains(random_blob);
66
67    if announce_hops <= existing.hops {
68        // Equal or fewer hops: accept if new blob AND newer emission
69        if blob_is_new && announce_emitted_ts > path_timebase {
70            return PathDecision::Add;
71        }
72        // Same emission + unresponsive path: accept for path recovery
73        if announce_emitted_ts == path_timebase && path_is_unresponsive {
74            return PathDecision::Add;
75        }
76        PathDecision::Reject
77    } else {
78        // More hops than existing path
79        let path_expired = now >= existing.expires;
80
81        if path_expired && blob_is_new {
82            return PathDecision::Add;
83        }
84
85        if announce_emitted_ts > path_timebase && blob_is_new {
86            return PathDecision::Add;
87        }
88
89        if announce_emitted_ts == path_timebase && path_is_unresponsive {
90            return PathDecision::Add;
91        }
92
93        PathDecision::Reject
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use super::super::types::InterfaceId;
101
102    fn make_blob(timebase: u64) -> [u8; 10] {
103        let mut blob = [0u8; 10];
104        let bytes = timebase.to_be_bytes();
105        // timebase is stored in blob[5..10] = last 5 bytes of u64
106        blob[5..10].copy_from_slice(&bytes[3..8]);
107        blob
108    }
109
110    fn make_path_entry(hops: u8, blobs: &[[u8; 10]], expires: f64) -> PathEntry {
111        PathEntry {
112            timestamp: 1000.0,
113            next_hop: [0xAA; 16],
114            hops,
115            expires,
116            random_blobs: blobs.to_vec(),
117            receiving_interface: InterfaceId(1),
118            packet_hash: [0xBB; 32],
119            announce_raw: None,
120        }
121    }
122
123    #[test]
124    fn test_timebase_extraction() {
125        let blob = make_blob(12345);
126        assert_eq!(timebase_from_random_blob(&blob), 12345);
127    }
128
129    #[test]
130    fn test_timebase_from_multiple_blobs() {
131        let b1 = make_blob(100);
132        let b2 = make_blob(200);
133        let b3 = make_blob(50);
134        assert_eq!(timebase_from_random_blobs(&[b1, b2, b3]), 200);
135    }
136
137    #[test]
138    fn test_timebase_empty_blobs() {
139        assert_eq!(timebase_from_random_blobs(&[]), 0);
140    }
141
142    #[test]
143    fn test_extract_random_blob() {
144        // Need at least 74 + 10 = 84 bytes
145        let mut data = [0u8; 100];
146        // Put a known blob at offset 74
147        data[74..84].copy_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
148        let blob = extract_random_blob(&data).unwrap();
149        assert_eq!(blob, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
150    }
151
152    #[test]
153    fn test_extract_random_blob_too_short() {
154        let data = [0u8; 80]; // too short
155        assert!(extract_random_blob(&data).is_none());
156    }
157
158    // --- Decision tree tests ---
159
160    #[test]
161    fn test_no_existing_path_always_add() {
162        let blob = make_blob(100);
163        assert_eq!(
164            should_update_path(None, 3, 100, &blob, false, 1000.0),
165            PathDecision::Add
166        );
167    }
168
169    #[test]
170    fn test_hop_limit_reject() {
171        let blob = make_blob(100);
172        assert_eq!(
173            should_update_path(None, 129, 100, &blob, false, 1000.0),
174            PathDecision::Reject
175        );
176    }
177
178    #[test]
179    fn test_fewer_hops_new_blob_newer_emission_add() {
180        let old_blob = make_blob(100);
181        let new_blob = make_blob(200);
182        let entry = make_path_entry(5, &[old_blob], 9999.0);
183        assert_eq!(
184            should_update_path(Some(&entry), 3, 200, &new_blob, false, 1000.0),
185            PathDecision::Add
186        );
187    }
188
189    #[test]
190    fn test_fewer_hops_duplicate_blob_reject() {
191        let blob = make_blob(100);
192        let entry = make_path_entry(5, &[blob], 9999.0);
193        assert_eq!(
194            should_update_path(Some(&entry), 3, 200, &blob, false, 1000.0),
195            PathDecision::Reject
196        );
197    }
198
199    #[test]
200    fn test_fewer_hops_same_emission_unresponsive_add() {
201        let old_blob = make_blob(100);
202        let mut different_blob = [0u8; 10];
203        different_blob[0] = 0xFF;
204        different_blob[5..10].copy_from_slice(&100u64.to_be_bytes()[3..8]);
205
206        let entry = make_path_entry(5, &[old_blob], 9999.0);
207        assert_eq!(
208            should_update_path(Some(&entry), 3, 100, &different_blob, true, 1000.0),
209            PathDecision::Add
210        );
211    }
212
213    #[test]
214    fn test_fewer_hops_same_emission_responsive_reject() {
215        let old_blob = make_blob(100);
216        let mut different_blob = [0u8; 10];
217        different_blob[0] = 0xFF;
218        different_blob[5..10].copy_from_slice(&100u64.to_be_bytes()[3..8]);
219
220        let entry = make_path_entry(5, &[old_blob], 9999.0);
221        assert_eq!(
222            should_update_path(Some(&entry), 3, 100, &different_blob, false, 1000.0),
223            PathDecision::Reject
224        );
225    }
226
227    #[test]
228    fn test_more_hops_expired_path_new_blob_add() {
229        let old_blob = make_blob(100);
230        let new_blob = make_blob(50); // older emission but path expired
231        let entry = make_path_entry(2, &[old_blob], 500.0); // expires at 500
232
233        assert_eq!(
234            should_update_path(Some(&entry), 5, 50, &new_blob, false, 600.0), // now > expires
235            PathDecision::Add
236        );
237    }
238
239    #[test]
240    fn test_more_hops_not_expired_older_emission_reject() {
241        let old_blob = make_blob(200);
242        let new_blob = make_blob(100);
243        let entry = make_path_entry(2, &[old_blob], 9999.0);
244
245        assert_eq!(
246            should_update_path(Some(&entry), 5, 100, &new_blob, false, 1000.0),
247            PathDecision::Reject
248        );
249    }
250
251    #[test]
252    fn test_more_hops_newer_emission_new_blob_add() {
253        let old_blob = make_blob(100);
254        let new_blob = make_blob(200);
255        let entry = make_path_entry(2, &[old_blob], 9999.0);
256
257        assert_eq!(
258            should_update_path(Some(&entry), 5, 200, &new_blob, false, 1000.0),
259            PathDecision::Add
260        );
261    }
262
263    #[test]
264    fn test_more_hops_same_emission_unresponsive_add() {
265        let old_blob = make_blob(100);
266        let mut different_blob = [0u8; 10];
267        different_blob[0] = 0xFF;
268        different_blob[5..10].copy_from_slice(&100u64.to_be_bytes()[3..8]);
269
270        let entry = make_path_entry(2, &[old_blob], 9999.0);
271
272        assert_eq!(
273            should_update_path(Some(&entry), 5, 100, &different_blob, true, 1000.0),
274            PathDecision::Add
275        );
276    }
277
278    #[test]
279    fn test_more_hops_same_emission_responsive_reject() {
280        let old_blob = make_blob(100);
281        let mut different_blob = [0u8; 10];
282        different_blob[0] = 0xFF;
283        different_blob[5..10].copy_from_slice(&100u64.to_be_bytes()[3..8]);
284
285        let entry = make_path_entry(2, &[old_blob], 9999.0);
286
287        assert_eq!(
288            should_update_path(Some(&entry), 5, 100, &different_blob, false, 1000.0),
289            PathDecision::Reject
290        );
291    }
292
293    #[test]
294    fn test_more_hops_duplicate_blob_reject() {
295        let blob = make_blob(200);
296        let entry = make_path_entry(2, &[blob], 9999.0);
297
298        assert_eq!(
299            should_update_path(Some(&entry), 5, 200, &blob, false, 1000.0),
300            PathDecision::Reject
301        );
302    }
303
304    #[test]
305    fn test_equal_hops_new_blob_newer_emission_add() {
306        let old_blob = make_blob(100);
307        let new_blob = make_blob(200);
308        let entry = make_path_entry(3, &[old_blob], 9999.0);
309
310        assert_eq!(
311            should_update_path(Some(&entry), 3, 200, &new_blob, false, 1000.0),
312            PathDecision::Add
313        );
314    }
315}