1use hopr_api::graph::{CostFn, NetworkGraphTraverse, NetworkGraphView, costs::EdgeCostFn, traits::EdgeObservableRead};
2
3const DEFAULT_EDGE_PENALTY: f64 = 0.5;
5
6const DEFAULT_MIN_ACK_RATE: f64 = 0.5;
8use hopr_types::{crypto::types::OffchainPublicKey, internal::errors::PathError};
9
10use crate::{
11 errors::{PathPlannerError, Result},
12 traits::{PathSelector, PathWithCost},
13};
14
15fn compute_paths<G, C>(
22 graph: &G,
23 src: &OffchainPublicKey,
24 dest: &OffchainPublicKey,
25 length: std::num::NonZeroUsize,
26 take: usize,
27 cost_fn: C,
28) -> Vec<PathWithCost>
29where
30 G: NetworkGraphTraverse<NodeId = OffchainPublicKey> + NetworkGraphView<NodeId = OffchainPublicKey>,
31 <G as NetworkGraphTraverse>::Observed: EdgeObservableRead + Send + 'static,
32 C: CostFn<Weight = <G as NetworkGraphTraverse>::Observed, Cost = f64>,
33{
34 let raw = graph.simple_paths(src, dest, length.get(), Some(take), cost_fn);
35
36 raw.into_iter()
37 .filter_map(|(path, _, cost)| {
38 tracing::trace!(?path, ?cost, "evaluating candidate path");
39 if cost > 0.0 {
40 Some(PathWithCost {
42 path: path.into_iter().skip(1).collect::<Vec<_>>(),
43 cost,
44 })
45 } else {
46 None
47 }
48 })
49 .collect::<Vec<_>>()
50}
51
52fn compute_extended_forward_paths<G>(
62 graph: &G,
63 src: &OffchainPublicKey,
64 dest: &OffchainPublicKey,
65 shorter_length: std::num::NonZeroUsize,
66 take: usize,
67 existing: &[PathWithCost],
68) -> Vec<PathWithCost>
69where
70 G: NetworkGraphTraverse<NodeId = OffchainPublicKey> + NetworkGraphView<NodeId = OffchainPublicKey>,
71 <G as NetworkGraphTraverse>::Observed: EdgeObservableRead + Send + 'static,
72{
73 let raw = graph.simple_paths_from(
74 src,
75 shorter_length.get(),
76 Some(take),
77 EdgeCostFn::forward_without_self_loopback(DEFAULT_EDGE_PENALTY, DEFAULT_MIN_ACK_RATE),
78 );
79
80 raw.into_iter()
81 .filter_map(|(path, _, cost)| {
82 if cost <= 0.0 {
83 return None;
84 }
85
86 let mut candidate: Vec<_> = path.into_iter().skip(1).collect();
88
89 if candidate.contains(dest) {
91 return None;
92 }
93
94 candidate.push(*dest);
95
96 if existing.iter().any(|pwc| pwc.path == candidate) {
98 return None;
99 }
100
101 tracing::trace!(?candidate, ?cost, "extended forward path candidate");
102 Some(PathWithCost { path: candidate, cost })
103 })
104 .take(take)
105 .collect()
106}
107
108#[derive(Clone)]
120pub struct HoprGraphPathSelector<G> {
121 me: OffchainPublicKey,
122 graph: G,
123 max_paths: usize,
124}
125
126impl<G> HoprGraphPathSelector<G>
127where
128 G: NetworkGraphTraverse<NodeId = OffchainPublicKey>
129 + NetworkGraphView<NodeId = OffchainPublicKey>
130 + Clone
131 + Send
132 + Sync
133 + 'static,
134 <G as NetworkGraphTraverse>::Observed: EdgeObservableRead + Send + 'static,
135{
136 pub fn new(me: OffchainPublicKey, graph: G, max_paths: usize) -> Self {
142 Self { me, graph, max_paths }
143 }
144}
145
146impl<G> PathSelector for HoprGraphPathSelector<G>
147where
148 G: NetworkGraphTraverse<NodeId = OffchainPublicKey>
149 + NetworkGraphView<NodeId = OffchainPublicKey>
150 + Clone
151 + Send
152 + Sync
153 + 'static,
154 <G as NetworkGraphTraverse>::Observed: EdgeObservableRead + Send + 'static,
155{
156 #[tracing::instrument(level = "trace", skip(self), fields(src = %src, dest = %dest, hops), ret, err)]
168 fn select_path(&self, src: OffchainPublicKey, dest: OffchainPublicKey, hops: usize) -> Result<Vec<PathWithCost>> {
169 tracing::trace!(%src, %dest, hops, "computing paths from graph");
170 let length = std::num::NonZeroUsize::new(hops + 1)
171 .expect("can never fail, it is physically at least 1 after the addition");
172
173 let paths = if src == self.me {
174 let mut found = compute_paths(
176 &self.graph,
177 &src,
178 &dest,
179 length,
180 self.max_paths,
181 EdgeCostFn::forward(length, DEFAULT_EDGE_PENALTY, DEFAULT_MIN_ACK_RATE),
182 );
183
184 if found.len() < self.max_paths
187 && let Some(shorter) = std::num::NonZeroUsize::new(length.get() - 1)
188 {
189 let remaining = self.max_paths - found.len();
190 let extended = compute_extended_forward_paths(&self.graph, &src, &dest, shorter, remaining, &found);
191 found.extend(extended);
192 }
193
194 found
195 } else {
196 compute_paths(
197 &self.graph,
198 &src,
199 &dest,
200 length,
201 self.max_paths,
202 EdgeCostFn::returning(length, DEFAULT_EDGE_PENALTY, DEFAULT_MIN_ACK_RATE),
203 )
204 };
205
206 if paths.is_empty() {
209 Err(PathPlannerError::Path(PathError::PathNotFound(
210 hops,
211 src.to_string(),
212 dest.to_string(),
213 )))
214 } else {
215 Ok(paths)
216 }
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use std::time::Duration;
223
224 use anyhow::Context;
225 use hex_literal::hex;
226 use hopr_api::graph::{
227 NetworkGraphWrite,
228 traits::{EdgeObservableWrite, EdgeWeightType},
229 };
230 use hopr_network_graph::ChannelGraph;
231 use hopr_types::{
232 crypto::prelude::{Keypair, OffchainKeypair},
233 internal::routing::RoutingOptions,
234 };
235
236 use super::*;
237 use crate::traits::PathSelector;
238
239 const SECRET_0: [u8; 32] = hex!("60741b83b99e36aa0c1331578156e16b8e21166d01834abb6c64b103f885734d");
240 const SECRET_1: [u8; 32] = hex!("71bf1f42ebbfcd89c3e197a3fd7cda79b92499e509b6fefa0fe44d02821d146a");
241 const SECRET_2: [u8; 32] = hex!("c24bd833704dd2abdae3933fcc9962c2ac404f84132224c474147382d4db2299");
242 const SECRET_3: [u8; 32] = hex!("e0bf93e9c916104da00b1850adc4608bd7e9087bbd3f805451f4556aa6b3fd6e");
243 const SECRET_4: [u8; 32] = hex!("cfc66f718ec66fb822391775d749d7a0d66b690927673634816b63339bc12a3c");
244
245 const MAX_PATHS: usize = 4;
246
247 fn pubkey(secret: &[u8; 32]) -> OffchainPublicKey {
248 *OffchainKeypair::from_secret(secret).expect("valid secret").public()
249 }
250
251 fn mark_edge_full(graph: &ChannelGraph, src: &OffchainPublicKey, dst: &OffchainPublicKey) {
253 graph.upsert_edge(src, dst, |obs| {
254 obs.record(EdgeWeightType::Connected(true));
255 obs.record(EdgeWeightType::Immediate(Ok(Duration::from_millis(50))));
256 obs.record(EdgeWeightType::Intermediate(Ok(Duration::from_millis(50))));
257 obs.record(EdgeWeightType::Capacity(Some(1000)));
258 });
259 }
260
261 fn mark_edge_last(graph: &ChannelGraph, src: &OffchainPublicKey, dst: &OffchainPublicKey) {
266 graph.upsert_edge(src, dst, |obs| {
267 obs.record(EdgeWeightType::Connected(true));
268 obs.record(EdgeWeightType::Immediate(Ok(Duration::from_millis(50))));
269 obs.record(EdgeWeightType::Intermediate(Ok(Duration::from_millis(50))));
270 obs.record(EdgeWeightType::Capacity(Some(1000)));
271 });
272 }
273
274 fn two_hop_graph() -> (OffchainPublicKey, OffchainPublicKey, OffchainPublicKey, ChannelGraph) {
276 let me = pubkey(&SECRET_0);
277 let hop = pubkey(&SECRET_1);
278 let dest = pubkey(&SECRET_2);
279 let graph = ChannelGraph::new(me);
280 graph.add_node(hop);
281 graph.add_node(dest);
282 graph.add_edge(&me, &hop).unwrap();
284 graph.add_edge(&hop, &dest).unwrap();
285 mark_edge_full(&graph, &me, &hop);
286 mark_edge_last(&graph, &hop, &dest);
287 graph.add_edge(&dest, &hop).unwrap();
289 graph.add_edge(&hop, &me).unwrap();
290 mark_edge_full(&graph, &dest, &hop);
291 mark_edge_last(&graph, &hop, &me);
292 (me, hop, dest, graph)
293 }
294
295 #[tokio::test]
296 async fn unreachable_dest_should_return_error() -> anyhow::Result<()> {
297 let me = pubkey(&SECRET_0);
298 let unreachable = pubkey(&SECRET_1);
299 let graph = ChannelGraph::new(me);
300 let selector = HoprGraphPathSelector::new(me, graph, MAX_PATHS);
302
303 let fwd = selector.select_path(me, unreachable, 1);
304 assert!(fwd.is_err(), "forward: should error when destination is unreachable");
305 assert!(matches!(
306 fwd.unwrap_err(),
307 PathPlannerError::Path(PathError::PathNotFound(..))
308 ));
309
310 let rev = selector.select_path(unreachable, me, 1);
311 assert!(rev.is_err(), "reverse: should error when destination is unreachable");
312 assert!(matches!(
313 rev.unwrap_err(),
314 PathPlannerError::Path(PathError::PathNotFound(..))
315 ));
316
317 Ok(())
318 }
319
320 #[tokio::test]
321 async fn path_should_exclude_source() -> anyhow::Result<()> {
322 let (me, _hop, dest, graph) = two_hop_graph();
323 let selector = HoprGraphPathSelector::new(me, graph, MAX_PATHS);
324
325 let fwd = selector.select_path(me, dest, 1).context("forward path")?;
326 assert!(!fwd.is_empty());
327 for pwc in &fwd {
328 assert!(!pwc.path.contains(&me), "forward path must not contain the source");
329 assert!(pwc.cost > 0.0, "cost must be positive");
330 }
331
332 let rev = selector.select_path(dest, me, 1).context("reverse path")?;
333 assert!(!rev.is_empty());
334 for pwc in &rev {
335 assert!(!pwc.path.contains(&dest), "reverse path must not contain the source");
336 assert!(pwc.cost > 0.0, "cost must be positive");
337 }
338
339 Ok(())
340 }
341
342 #[tokio::test]
343 async fn multi_hop_path_should_have_correct_length() -> anyhow::Result<()> {
344 let me = pubkey(&SECRET_0);
346 let a = pubkey(&SECRET_1);
347 let b = pubkey(&SECRET_2);
348 let dest = pubkey(&SECRET_3);
349 let graph = ChannelGraph::new(me);
350 for n in [a, b, dest] {
351 graph.add_node(n);
352 }
353 graph.add_edge(&me, &a).unwrap();
355 graph.add_edge(&a, &b).unwrap();
356 graph.add_edge(&b, &dest).unwrap();
357 mark_edge_full(&graph, &me, &a);
358 mark_edge_full(&graph, &a, &b);
359 mark_edge_last(&graph, &b, &dest);
360 graph.add_edge(&dest, &b).unwrap();
362 graph.add_edge(&b, &a).unwrap();
363 graph.add_edge(&a, &me).unwrap();
364 mark_edge_full(&graph, &dest, &b);
365 mark_edge_full(&graph, &b, &a);
366 mark_edge_last(&graph, &a, &me);
367
368 let selector = HoprGraphPathSelector::new(me, graph, MAX_PATHS);
369
370 let fwd = selector.select_path(me, dest, 2).context("forward 2-hop path")?;
371 assert!(!fwd.is_empty());
372 for pwc in &fwd {
373 assert_eq!(pwc.path.len(), 3, "forward 2-hop path: [A, B, dest]");
374 assert_eq!(pwc.path.last(), Some(&dest));
375 }
376
377 let rev = selector.select_path(dest, me, 2).context("reverse 2-hop path")?;
378 assert!(!rev.is_empty());
379 for pwc in &rev {
380 assert_eq!(pwc.path.len(), 3, "reverse 2-hop path: [B, A, me]");
381 assert_eq!(pwc.path.last(), Some(&me));
382 }
383
384 Ok(())
385 }
386
387 #[tokio::test]
388 async fn one_hop_path_should_include_relay_and_destination() -> anyhow::Result<()> {
389 let (me, relay, dest, graph) = two_hop_graph();
391 let selector = HoprGraphPathSelector::new(me, graph, MAX_PATHS);
392
393 let fwd = selector.select_path(me, dest, 1).context("forward 1-hop path")?;
394 assert!(!fwd.is_empty());
395 for pwc in &fwd {
396 assert_eq!(pwc.path.len(), 2, "forward: [relay, dest]");
397 assert_eq!(pwc.path.last(), Some(&dest));
398 assert!(!pwc.path.contains(&me));
399 }
400
401 let rev = selector.select_path(dest, me, 1).context("reverse 1-hop path")?;
402 assert!(!rev.is_empty());
403 for pwc in &rev {
404 assert_eq!(pwc.path.len(), 2, "reverse: [relay, me]");
405 assert_eq!(pwc.path.last(), Some(&me));
406 assert!(!pwc.path.contains(&dest));
407 }
408
409 let _ = relay;
410 Ok(())
411 }
412
413 #[tokio::test]
414 async fn diamond_topology_should_return_multiple_paths() -> anyhow::Result<()> {
415 let me = pubkey(&SECRET_0);
417 let a = pubkey(&SECRET_1);
418 let b = pubkey(&SECRET_2);
419 let dest = pubkey(&SECRET_3);
420 let graph = ChannelGraph::new(me);
421 for n in [a, b, dest] {
422 graph.add_node(n);
423 }
424 graph.add_edge(&me, &a).unwrap();
426 graph.add_edge(&me, &b).unwrap();
427 graph.add_edge(&a, &dest).unwrap();
428 graph.add_edge(&b, &dest).unwrap();
429 mark_edge_full(&graph, &me, &a);
430 mark_edge_full(&graph, &me, &b);
431 mark_edge_last(&graph, &a, &dest);
432 mark_edge_last(&graph, &b, &dest);
433 graph.add_edge(&dest, &a).unwrap();
435 graph.add_edge(&dest, &b).unwrap();
436 graph.add_edge(&a, &me).unwrap();
437 graph.add_edge(&b, &me).unwrap();
438 mark_edge_full(&graph, &dest, &a);
439 mark_edge_full(&graph, &dest, &b);
440 mark_edge_last(&graph, &a, &me);
441 mark_edge_last(&graph, &b, &me);
442
443 let selector = HoprGraphPathSelector::new(me, graph, MAX_PATHS);
444
445 let fwd = selector.select_path(me, dest, 1).context("forward path")?;
446 assert_eq!(fwd.len(), 2, "forward: both paths via a and b should be returned");
447 for pwc in &fwd {
448 assert_eq!(pwc.path.last(), Some(&dest));
449 }
450
451 let rev = selector.select_path(dest, me, 1).context("reverse path")?;
452 assert_eq!(rev.len(), 2, "reverse: both paths via a and b should be returned");
453 for pwc in &rev {
454 assert_eq!(pwc.path.last(), Some(&me));
455 }
456
457 Ok(())
458 }
459
460 #[tokio::test]
461 async fn zero_cost_paths_should_return_error() -> anyhow::Result<()> {
462 let me = pubkey(&SECRET_0);
464 let dest = pubkey(&SECRET_1);
465 let graph = ChannelGraph::new(me);
466 graph.add_node(dest);
467 graph.add_edge(&me, &dest).unwrap();
468 graph.add_edge(&dest, &me).unwrap();
469 let selector = HoprGraphPathSelector::new(me, graph, MAX_PATHS);
472 assert!(
473 selector.select_path(me, dest, 1).is_err(),
474 "forward: edge with no observations should produce no valid path"
475 );
476 assert!(
477 selector.select_path(dest, me, 1).is_err(),
478 "reverse: edge with no observations should produce no valid path"
479 );
480 Ok(())
481 }
482
483 #[tokio::test]
484 async fn no_path_at_requested_hop_count_should_return_error() -> anyhow::Result<()> {
485 let me = pubkey(&SECRET_0);
487 let dest = pubkey(&SECRET_1);
488 let graph = ChannelGraph::new(me);
489 graph.add_node(dest);
490 graph.add_edge(&me, &dest).unwrap();
491 graph.add_edge(&dest, &me).unwrap();
492 mark_edge_full(&graph, &me, &dest);
493 mark_edge_full(&graph, &dest, &me);
494
495 let selector = HoprGraphPathSelector::new(me, graph, MAX_PATHS);
496 assert!(
497 selector.select_path(me, dest, 2).is_err(),
498 "forward: no 2-hop path should exist for a direct edge"
499 );
500 assert!(
501 selector.select_path(dest, me, 2).is_err(),
502 "reverse: no 2-hop path should exist for a direct edge"
503 );
504 Ok(())
505 }
506
507 #[tokio::test]
508 async fn forward_path_should_work_without_last_edge() -> anyhow::Result<()> {
509 let me = pubkey(&SECRET_0);
513 let relay = pubkey(&SECRET_1);
514 let dest = pubkey(&SECRET_2);
515 let graph = ChannelGraph::new(me);
516 graph.add_node(relay);
517 graph.add_node(dest);
518 graph.add_edge(&me, &relay).unwrap();
520 mark_edge_full(&graph, &me, &relay);
521 graph.add_edge(&dest, &relay).unwrap();
523 graph.add_edge(&relay, &me).unwrap();
524 mark_edge_full(&graph, &dest, &relay);
525 mark_edge_last(&graph, &relay, &me);
526
527 let selector = HoprGraphPathSelector::new(me, graph, MAX_PATHS);
528
529 let fwd = selector
531 .select_path(me, dest, 1)
532 .context("forward path with virtual last hop")?;
533 assert!(!fwd.is_empty(), "forward path should find at least one route");
534 for pwc in &fwd {
535 assert_eq!(pwc.path.len(), 2, "forward: [relay, dest]");
536 assert_eq!(pwc.path[0], relay);
537 assert_eq!(pwc.path[1], dest);
538 }
539
540 let rev = selector.select_path(dest, me, 1).context("return path")?;
542 assert!(!rev.is_empty(), "return path should find at least one route");
543 for pwc in &rev {
544 assert_eq!(pwc.path.len(), 2, "return: [relay, me]");
545 assert_eq!(pwc.path.last(), Some(&me));
546 }
547
548 Ok(())
549 }
550
551 #[tokio::test]
552 async fn five_node_chain_should_support_max_hops() -> anyhow::Result<()> {
553 let me = pubkey(&SECRET_0);
555 let a = pubkey(&SECRET_1);
556 let b = pubkey(&SECRET_2);
557 let c = pubkey(&SECRET_3);
558 let dest = pubkey(&SECRET_4);
559 let graph = ChannelGraph::new(me);
560 for n in [a, b, c, dest] {
561 graph.add_node(n);
562 }
563 graph.add_edge(&me, &a).unwrap();
565 graph.add_edge(&a, &b).unwrap();
566 graph.add_edge(&b, &c).unwrap();
567 graph.add_edge(&c, &dest).unwrap();
568 mark_edge_full(&graph, &me, &a);
569 mark_edge_full(&graph, &a, &b);
570 mark_edge_full(&graph, &b, &c);
571 mark_edge_last(&graph, &c, &dest);
572 graph.add_edge(&dest, &c).unwrap();
574 graph.add_edge(&c, &b).unwrap();
575 graph.add_edge(&b, &a).unwrap();
576 graph.add_edge(&a, &me).unwrap();
577 mark_edge_full(&graph, &dest, &c);
578 mark_edge_full(&graph, &c, &b);
579 mark_edge_full(&graph, &b, &a);
580 mark_edge_last(&graph, &a, &me);
581
582 let selector = HoprGraphPathSelector::new(me, graph, MAX_PATHS);
583
584 let fwd = selector
585 .select_path(me, dest, RoutingOptions::MAX_INTERMEDIATE_HOPS)
586 .context("forward 3-hop path")?;
587 assert!(!fwd.is_empty());
588 for pwc in &fwd {
589 assert_eq!(pwc.path.len(), 4, "forward: [a, b, c, dest]");
590 assert_eq!(pwc.path.last(), Some(&dest));
591 assert!(!pwc.path.contains(&me));
592 }
593
594 let rev = selector
595 .select_path(dest, me, RoutingOptions::MAX_INTERMEDIATE_HOPS)
596 .context("reverse 3-hop path")?;
597 assert!(!rev.is_empty());
598 for pwc in &rev {
599 assert_eq!(pwc.path.len(), 4, "reverse: [c, b, a, me]");
600 assert_eq!(pwc.path.last(), Some(&me));
601 assert!(!pwc.path.contains(&dest));
602 }
603
604 Ok(())
605 }
606}