1use hopr_api::graph::{
2 NetworkGraphTraverse, NetworkGraphView, ValueFn, function::EdgeValueFn, traits::EdgeObservableRead,
3};
4use hopr_types::{crypto::types::OffchainPublicKey, internal::errors::PathError};
5
6use super::{
7 errors::{PathPlannerError, Result},
8 traits::{PathSelector, PathWithCost},
9};
10
11fn compute_paths<G, C>(
18 graph: &G,
19 src: &OffchainPublicKey,
20 dest: &OffchainPublicKey,
21 length: std::num::NonZeroUsize,
22 take: usize,
23 cost_fn: C,
24) -> Vec<PathWithCost>
25where
26 G: NetworkGraphTraverse<NodeId = OffchainPublicKey> + NetworkGraphView<NodeId = OffchainPublicKey>,
27 <G as NetworkGraphTraverse>::Observed: EdgeObservableRead + Send + 'static,
28 C: ValueFn<Weight = <G as NetworkGraphTraverse>::Observed, Value = f64>,
29{
30 let raw = graph.simple_paths(src, dest, length.get(), Some(take), cost_fn);
31
32 raw.into_iter()
33 .filter_map(|(path, _, cost)| {
34 tracing::trace!(?path, ?cost, "evaluating candidate path");
35 if cost > 0.0 {
36 Some(PathWithCost {
38 path: path.into_iter().skip(1).collect::<Vec<_>>(),
39 cost,
40 })
41 } else {
42 None
43 }
44 })
45 .collect::<Vec<_>>()
46}
47
48#[derive(Clone)]
60pub struct HoprGraphPathSelector<G> {
61 me: OffchainPublicKey,
62 graph: G,
63 max_paths: usize,
64 edge_penalty: f64,
65 min_ack_rate: f64,
66}
67
68impl<G> HoprGraphPathSelector<G>
69where
70 G: NetworkGraphTraverse<NodeId = OffchainPublicKey>
71 + NetworkGraphView<NodeId = OffchainPublicKey>
72 + Clone
73 + Send
74 + Sync
75 + 'static,
76 <G as NetworkGraphTraverse>::Observed: EdgeObservableRead + Send + 'static,
77{
78 pub fn new(me: OffchainPublicKey, graph: G, max_paths: usize, edge_penalty: f64, min_ack_rate: f64) -> Self {
86 Self {
87 me,
88 graph,
89 max_paths,
90 edge_penalty,
91 min_ack_rate,
92 }
93 }
94
95 fn compute_extended_forward_paths(
105 &self,
106 src: &OffchainPublicKey,
107 dest: &OffchainPublicKey,
108 shorter_length: std::num::NonZeroUsize,
109 take: usize,
110 existing: &[PathWithCost],
111 ) -> Vec<PathWithCost> {
112 let raw = self.graph.simple_paths_from(
113 src,
114 shorter_length.get(),
115 Some(take),
116 EdgeValueFn::forward_without_self_loopback(self.edge_penalty, self.min_ack_rate),
117 );
118
119 raw.into_iter()
120 .filter_map(|(path, _, cost)| {
121 if cost <= 0.0 {
122 return None;
123 }
124
125 let candidate: Vec<_> = path.into_iter().skip(1).collect();
130 if candidate.contains(dest) {
131 return None;
132 }
133 let mut candidate = candidate;
134 candidate.push(*dest);
135
136 if existing.iter().any(|pwc| pwc.path == candidate) {
138 return None;
139 }
140
141 tracing::trace!(?candidate, ?cost, "extended forward path candidate");
142 Some(PathWithCost { path: candidate, cost })
143 })
144 .take(take)
145 .collect()
146 }
147}
148
149impl<G> PathSelector for HoprGraphPathSelector<G>
150where
151 G: NetworkGraphTraverse<NodeId = OffchainPublicKey>
152 + NetworkGraphView<NodeId = OffchainPublicKey>
153 + Clone
154 + Send
155 + Sync
156 + 'static,
157 <G as NetworkGraphTraverse>::Observed: EdgeObservableRead + Send + 'static,
158{
159 #[tracing::instrument(level = "trace", skip(self), fields(src = %src, dest = %dest, hops), ret, err)]
171 fn select_path(&self, src: OffchainPublicKey, dest: OffchainPublicKey, hops: usize) -> Result<Vec<PathWithCost>> {
172 let direction = if src == self.me { "forward" } else { "return" };
173 tracing::debug!(%src, %dest, hops, direction, "computing paths from graph");
174
175 let length = std::num::NonZeroUsize::new(hops + 1)
176 .expect("can never fail, it is physically at least 1 after the addition");
177
178 let paths = if src == self.me {
179 let mut found = compute_paths(
181 &self.graph,
182 &src,
183 &dest,
184 length,
185 self.max_paths,
186 EdgeValueFn::forward(length, self.edge_penalty, self.min_ack_rate),
187 );
188 tracing::debug!(
189 direction,
190 phase = 1,
191 count = found.len(),
192 "[forward] phase 1 candidates"
193 );
194
195 if found.len() < self.max_paths
198 && let Some(shorter) = std::num::NonZeroUsize::new(length.get() - 1)
199 {
200 let remaining = self.max_paths - found.len();
201 let extended = self.compute_extended_forward_paths(&src, &dest, shorter, remaining, &found);
202 tracing::debug!(
203 direction,
204 phase = 2,
205 count = extended.len(),
206 "[forward] phase 2 extended candidates"
207 );
208 found.extend(extended);
209 }
210
211 found
212 } else {
213 let found = compute_paths(
214 &self.graph,
215 &src,
216 &dest,
217 length,
218 self.max_paths,
219 EdgeValueFn::returning(length, self.edge_penalty, self.min_ack_rate),
220 );
221 tracing::debug!(direction, count = found.len(), "[return] candidates");
222 found
223 };
224
225 for (i, pwc) in paths.iter().enumerate() {
226 tracing::debug!(
227 direction,
228 index = i,
229 path = ?pwc.path,
230 cost = pwc.cost,
231 "[{direction}] candidate path"
232 );
233 }
234
235 if paths.is_empty() {
236 Err(PathPlannerError::Path(PathError::PathNotFound(
237 hops,
238 src.to_string(),
239 dest.to_string(),
240 )))
241 } else {
242 Ok(paths)
243 }
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use std::time::Duration;
250
251 use anyhow::Context;
252 use hex_literal::hex;
253 use hopr_api::graph::{
254 NetworkGraphWrite,
255 traits::{EdgeObservableWrite, EdgeWeightType},
256 };
257 use hopr_network_graph::ChannelGraph;
258 use hopr_types::{
259 crypto::prelude::{Keypair, OffchainKeypair},
260 internal::routing::RoutingOptions,
261 };
262
263 use super::*;
264 use crate::path::{PathPlannerConfig, traits::PathSelector};
265
266 fn test_selector(
267 me: OffchainPublicKey,
268 graph: ChannelGraph,
269 max_paths: usize,
270 ) -> HoprGraphPathSelector<ChannelGraph> {
271 let cfg = PathPlannerConfig::default();
272 HoprGraphPathSelector::new(me, graph, max_paths, cfg.edge_penalty, cfg.min_ack_rate)
273 }
274
275 const SECRET_0: [u8; 32] = hex!("60741b83b99e36aa0c1331578156e16b8e21166d01834abb6c64b103f885734d");
276 const SECRET_1: [u8; 32] = hex!("71bf1f42ebbfcd89c3e197a3fd7cda79b92499e509b6fefa0fe44d02821d146a");
277 const SECRET_2: [u8; 32] = hex!("c24bd833704dd2abdae3933fcc9962c2ac404f84132224c474147382d4db2299");
278 const SECRET_3: [u8; 32] = hex!("e0bf93e9c916104da00b1850adc4608bd7e9087bbd3f805451f4556aa6b3fd6e");
279 const SECRET_4: [u8; 32] = hex!("cfc66f718ec66fb822391775d749d7a0d66b690927673634816b63339bc12a3c");
280
281 const MAX_PATHS: usize = 4;
282
283 fn pubkey(secret: &[u8; 32]) -> OffchainPublicKey {
284 *OffchainKeypair::from_secret(secret).expect("valid secret").public()
285 }
286
287 fn mark_edge_full(graph: &ChannelGraph, src: &OffchainPublicKey, dst: &OffchainPublicKey) {
289 graph.upsert_edge(src, dst, |obs| {
290 obs.record(EdgeWeightType::Connected(true));
291 obs.record(EdgeWeightType::Immediate(Ok(Duration::from_millis(50))));
292 obs.record(EdgeWeightType::Intermediate(Ok(Duration::from_millis(50))));
293 obs.record(EdgeWeightType::Capacity(Some(1000)));
294 });
295 }
296
297 fn mark_edge_last(graph: &ChannelGraph, src: &OffchainPublicKey, dst: &OffchainPublicKey) {
302 graph.upsert_edge(src, dst, |obs| {
303 obs.record(EdgeWeightType::Connected(true));
304 obs.record(EdgeWeightType::Immediate(Ok(Duration::from_millis(50))));
305 obs.record(EdgeWeightType::Intermediate(Ok(Duration::from_millis(50))));
306 obs.record(EdgeWeightType::Capacity(Some(1000)));
307 });
308 }
309
310 fn two_hop_graph() -> (OffchainPublicKey, OffchainPublicKey, OffchainPublicKey, ChannelGraph) {
312 let me = pubkey(&SECRET_0);
313 let hop = pubkey(&SECRET_1);
314 let dest = pubkey(&SECRET_2);
315 let graph = ChannelGraph::new(me);
316 graph.add_node(hop);
317 graph.add_node(dest);
318 graph.add_edge(&me, &hop).unwrap();
320 graph.add_edge(&hop, &dest).unwrap();
321 mark_edge_full(&graph, &me, &hop);
322 mark_edge_last(&graph, &hop, &dest);
323 graph.add_edge(&dest, &hop).unwrap();
325 graph.add_edge(&hop, &me).unwrap();
326 mark_edge_full(&graph, &dest, &hop);
327 mark_edge_last(&graph, &hop, &me);
328 (me, hop, dest, graph)
329 }
330
331 #[tokio::test]
332 async fn unreachable_dest_should_return_error() -> anyhow::Result<()> {
333 let me = pubkey(&SECRET_0);
334 let unreachable = pubkey(&SECRET_1);
335 let graph = ChannelGraph::new(me);
336 let selector = test_selector(me, graph, MAX_PATHS);
338
339 let fwd = selector.select_path(me, unreachable, 1);
340 assert!(fwd.is_err(), "forward: should error when destination is unreachable");
341 assert!(matches!(
342 fwd.unwrap_err(),
343 PathPlannerError::Path(PathError::PathNotFound(..))
344 ));
345
346 let rev = selector.select_path(unreachable, me, 1);
347 assert!(rev.is_err(), "reverse: should error when destination is unreachable");
348 assert!(matches!(
349 rev.unwrap_err(),
350 PathPlannerError::Path(PathError::PathNotFound(..))
351 ));
352
353 Ok(())
354 }
355
356 #[tokio::test]
357 async fn path_should_exclude_source() -> anyhow::Result<()> {
358 let (me, _hop, dest, graph) = two_hop_graph();
359 let selector = test_selector(me, graph, MAX_PATHS);
360
361 let fwd = selector.select_path(me, dest, 1).context("forward path")?;
362 assert!(!fwd.is_empty());
363 for pwc in &fwd {
364 assert!(!pwc.path.contains(&me), "forward path must not contain the source");
365 assert!(pwc.cost > 0.0, "cost must be positive");
366 }
367
368 let rev = selector.select_path(dest, me, 1).context("reverse path")?;
369 assert!(!rev.is_empty());
370 for pwc in &rev {
371 assert!(!pwc.path.contains(&dest), "reverse path must not contain the source");
372 assert!(pwc.cost > 0.0, "cost must be positive");
373 }
374
375 Ok(())
376 }
377
378 #[tokio::test]
379 async fn multi_hop_path_should_have_correct_length() -> anyhow::Result<()> {
380 let me = pubkey(&SECRET_0);
382 let a = pubkey(&SECRET_1);
383 let b = pubkey(&SECRET_2);
384 let dest = pubkey(&SECRET_3);
385 let graph = ChannelGraph::new(me);
386 for n in [a, b, dest] {
387 graph.add_node(n);
388 }
389 graph.add_edge(&me, &a).unwrap();
391 graph.add_edge(&a, &b).unwrap();
392 graph.add_edge(&b, &dest).unwrap();
393 mark_edge_full(&graph, &me, &a);
394 mark_edge_full(&graph, &a, &b);
395 mark_edge_last(&graph, &b, &dest);
396 graph.add_edge(&dest, &b).unwrap();
398 graph.add_edge(&b, &a).unwrap();
399 graph.add_edge(&a, &me).unwrap();
400 mark_edge_full(&graph, &dest, &b);
401 mark_edge_full(&graph, &b, &a);
402 mark_edge_last(&graph, &a, &me);
403
404 let selector = test_selector(me, graph, MAX_PATHS);
405
406 let fwd = selector.select_path(me, dest, 2).context("forward 2-hop path")?;
407 assert!(!fwd.is_empty());
408 for pwc in &fwd {
409 assert_eq!(pwc.path.len(), 3, "forward 2-hop path: [A, B, dest]");
410 assert_eq!(pwc.path.last(), Some(&dest));
411 }
412
413 let rev = selector.select_path(dest, me, 2).context("reverse 2-hop path")?;
414 assert!(!rev.is_empty());
415 for pwc in &rev {
416 assert_eq!(pwc.path.len(), 3, "reverse 2-hop path: [B, A, me]");
417 assert_eq!(pwc.path.last(), Some(&me));
418 }
419
420 Ok(())
421 }
422
423 #[tokio::test]
424 async fn one_hop_path_should_include_relay_and_destination() -> anyhow::Result<()> {
425 let (me, relay, dest, graph) = two_hop_graph();
427 let selector = test_selector(me, graph, MAX_PATHS);
428
429 let fwd = selector.select_path(me, dest, 1).context("forward 1-hop path")?;
430 assert!(!fwd.is_empty());
431 for pwc in &fwd {
432 assert_eq!(pwc.path.len(), 2, "forward: [relay, dest]");
433 assert_eq!(pwc.path.last(), Some(&dest));
434 assert!(!pwc.path.contains(&me));
435 }
436
437 let rev = selector.select_path(dest, me, 1).context("reverse 1-hop path")?;
438 assert!(!rev.is_empty());
439 for pwc in &rev {
440 assert_eq!(pwc.path.len(), 2, "reverse: [relay, me]");
441 assert_eq!(pwc.path.last(), Some(&me));
442 assert!(!pwc.path.contains(&dest));
443 }
444
445 let _ = relay;
446 Ok(())
447 }
448
449 #[tokio::test]
450 async fn diamond_topology_should_return_multiple_paths() -> anyhow::Result<()> {
451 let me = pubkey(&SECRET_0);
453 let a = pubkey(&SECRET_1);
454 let b = pubkey(&SECRET_2);
455 let dest = pubkey(&SECRET_3);
456 let graph = ChannelGraph::new(me);
457 for n in [a, b, dest] {
458 graph.add_node(n);
459 }
460 graph.add_edge(&me, &a).unwrap();
462 graph.add_edge(&me, &b).unwrap();
463 graph.add_edge(&a, &dest).unwrap();
464 graph.add_edge(&b, &dest).unwrap();
465 mark_edge_full(&graph, &me, &a);
466 mark_edge_full(&graph, &me, &b);
467 mark_edge_last(&graph, &a, &dest);
468 mark_edge_last(&graph, &b, &dest);
469 graph.add_edge(&dest, &a).unwrap();
471 graph.add_edge(&dest, &b).unwrap();
472 graph.add_edge(&a, &me).unwrap();
473 graph.add_edge(&b, &me).unwrap();
474 mark_edge_full(&graph, &dest, &a);
475 mark_edge_full(&graph, &dest, &b);
476 mark_edge_last(&graph, &a, &me);
477 mark_edge_last(&graph, &b, &me);
478
479 let selector = test_selector(me, graph, MAX_PATHS);
480
481 let fwd = selector.select_path(me, dest, 1).context("forward path")?;
482 assert_eq!(fwd.len(), 2, "forward: both paths via a and b should be returned");
483 for pwc in &fwd {
484 assert_eq!(pwc.path.last(), Some(&dest));
485 }
486
487 let rev = selector.select_path(dest, me, 1).context("reverse path")?;
488 assert_eq!(rev.len(), 2, "reverse: both paths via a and b should be returned");
489 for pwc in &rev {
490 assert_eq!(pwc.path.last(), Some(&me));
491 }
492
493 Ok(())
494 }
495
496 #[tokio::test]
497 async fn zero_cost_paths_should_return_error() -> anyhow::Result<()> {
498 let me = pubkey(&SECRET_0);
500 let dest = pubkey(&SECRET_1);
501 let graph = ChannelGraph::new(me);
502 graph.add_node(dest);
503 graph.add_edge(&me, &dest).unwrap();
504 graph.add_edge(&dest, &me).unwrap();
505 let selector = test_selector(me, graph, MAX_PATHS);
508 assert!(
509 selector.select_path(me, dest, 1).is_err(),
510 "forward: edge with no observations should produce no valid path"
511 );
512 assert!(
513 selector.select_path(dest, me, 1).is_err(),
514 "reverse: edge with no observations should produce no valid path"
515 );
516 Ok(())
517 }
518
519 #[tokio::test]
520 async fn no_path_at_requested_hop_count_should_return_error() -> anyhow::Result<()> {
521 let me = pubkey(&SECRET_0);
523 let dest = pubkey(&SECRET_1);
524 let graph = ChannelGraph::new(me);
525 graph.add_node(dest);
526 graph.add_edge(&me, &dest).unwrap();
527 graph.add_edge(&dest, &me).unwrap();
528 mark_edge_full(&graph, &me, &dest);
529 mark_edge_full(&graph, &dest, &me);
530
531 let selector = test_selector(me, graph, MAX_PATHS);
532 assert!(
533 selector.select_path(me, dest, 2).is_err(),
534 "forward: no 2-hop path should exist for a direct edge"
535 );
536 assert!(
537 selector.select_path(dest, me, 2).is_err(),
538 "reverse: no 2-hop path should exist for a direct edge"
539 );
540 Ok(())
541 }
542
543 #[tokio::test]
544 async fn forward_path_should_work_without_last_edge() -> anyhow::Result<()> {
545 let me = pubkey(&SECRET_0);
549 let relay = pubkey(&SECRET_1);
550 let dest = pubkey(&SECRET_2);
551 let graph = ChannelGraph::new(me);
552 graph.add_node(relay);
553 graph.add_node(dest);
554 graph.add_edge(&me, &relay).unwrap();
556 mark_edge_full(&graph, &me, &relay);
557 graph.add_edge(&dest, &relay).unwrap();
559 graph.add_edge(&relay, &me).unwrap();
560 mark_edge_full(&graph, &dest, &relay);
561 mark_edge_last(&graph, &relay, &me);
562
563 let selector = test_selector(me, graph, MAX_PATHS);
564
565 let fwd = selector
567 .select_path(me, dest, 1)
568 .context("forward path with virtual last hop")?;
569 assert!(!fwd.is_empty(), "forward path should find at least one route");
570 for pwc in &fwd {
571 assert_eq!(pwc.path.len(), 2, "forward: [relay, dest]");
572 assert_eq!(pwc.path[0], relay);
573 assert_eq!(pwc.path[1], dest);
574 }
575
576 let rev = selector.select_path(dest, me, 1).context("return path")?;
578 assert!(!rev.is_empty(), "return path should find at least one route");
579 for pwc in &rev {
580 assert_eq!(pwc.path.len(), 2, "return: [relay, me]");
581 assert_eq!(pwc.path.last(), Some(&me));
582 }
583
584 Ok(())
585 }
586
587 #[tokio::test]
588 async fn five_node_chain_should_support_max_hops() -> anyhow::Result<()> {
589 let me = pubkey(&SECRET_0);
591 let a = pubkey(&SECRET_1);
592 let b = pubkey(&SECRET_2);
593 let c = pubkey(&SECRET_3);
594 let dest = pubkey(&SECRET_4);
595 let graph = ChannelGraph::new(me);
596 for n in [a, b, c, dest] {
597 graph.add_node(n);
598 }
599 graph.add_edge(&me, &a).unwrap();
601 graph.add_edge(&a, &b).unwrap();
602 graph.add_edge(&b, &c).unwrap();
603 graph.add_edge(&c, &dest).unwrap();
604 mark_edge_full(&graph, &me, &a);
605 mark_edge_full(&graph, &a, &b);
606 mark_edge_full(&graph, &b, &c);
607 mark_edge_last(&graph, &c, &dest);
608 graph.add_edge(&dest, &c).unwrap();
610 graph.add_edge(&c, &b).unwrap();
611 graph.add_edge(&b, &a).unwrap();
612 graph.add_edge(&a, &me).unwrap();
613 mark_edge_full(&graph, &dest, &c);
614 mark_edge_full(&graph, &c, &b);
615 mark_edge_full(&graph, &b, &a);
616 mark_edge_last(&graph, &a, &me);
617
618 let selector = test_selector(me, graph, MAX_PATHS);
619
620 let fwd = selector
621 .select_path(me, dest, RoutingOptions::MAX_INTERMEDIATE_HOPS)
622 .context("forward 3-hop path")?;
623 assert!(!fwd.is_empty());
624 for pwc in &fwd {
625 assert_eq!(pwc.path.len(), 4, "forward: [a, b, c, dest]");
626 assert_eq!(pwc.path.last(), Some(&dest));
627 assert!(!pwc.path.contains(&me));
628 }
629
630 let rev = selector
631 .select_path(dest, me, RoutingOptions::MAX_INTERMEDIATE_HOPS)
632 .context("reverse 3-hop path")?;
633 assert!(!rev.is_empty());
634 for pwc in &rev {
635 assert_eq!(pwc.path.len(), 4, "reverse: [c, b, a, me]");
636 assert_eq!(pwc.path.last(), Some(&me));
637 assert!(!pwc.path.contains(&dest));
638 }
639
640 Ok(())
641 }
642
643 #[tokio::test]
644 async fn selector_should_reject_extended_path_containing_destination() -> anyhow::Result<()> {
645 let me = pubkey(&SECRET_0);
650 let relay = pubkey(&SECRET_1);
651 let dest = pubkey(&SECRET_2);
652 let graph = ChannelGraph::new(me);
653 graph.add_node(relay);
654 graph.add_node(dest);
655 graph.add_edge(&me, &dest).unwrap();
657 mark_edge_full(&graph, &me, &dest);
658 graph.add_edge(&me, &relay).unwrap();
660 mark_edge_full(&graph, &me, &relay);
661 graph.add_edge(&dest, &relay).unwrap();
663 graph.add_edge(&relay, &me).unwrap();
664 mark_edge_full(&graph, &dest, &relay);
665 mark_edge_last(&graph, &relay, &me);
666
667 let selector = test_selector(me, graph, MAX_PATHS);
668
669 let fwd = selector
670 .select_path(me, dest, 1)
671 .context("forward path with dest as direct neighbor")?;
672 assert!(!fwd.is_empty(), "should find at least one path via relay");
673 for pwc in &fwd {
674 assert_eq!(pwc.path.len(), 2, "path must be [relay, dest]");
675 assert_eq!(pwc.path[0], relay, "first node must be relay, not dest");
676 assert_eq!(pwc.path[1], dest);
677 }
678 Ok(())
679 }
680
681 #[tokio::test]
682 async fn selector_should_reject_one_hop_path_where_relay_equals_destination() -> anyhow::Result<()> {
683 let me = pubkey(&SECRET_0);
686 let relay = pubkey(&SECRET_1);
687 let dest = pubkey(&SECRET_2);
688 let graph = ChannelGraph::new(me);
689 graph.add_node(relay);
690 graph.add_node(dest);
691 graph.add_edge(&me, &dest).unwrap();
693 mark_edge_full(&graph, &me, &dest);
694 graph.add_edge(&me, &relay).unwrap();
696 mark_edge_full(&graph, &me, &relay);
697 graph.add_edge(&dest, &relay).unwrap();
699 graph.add_edge(&relay, &me).unwrap();
700 mark_edge_full(&graph, &dest, &relay);
701 mark_edge_last(&graph, &relay, &me);
702
703 let selector = test_selector(me, graph, MAX_PATHS);
704
705 let fwd = selector
706 .select_path(me, dest, 1)
707 .context("forward path — dest is direct neighbor, relay is intermediate")?;
708 assert!(!fwd.is_empty(), "should find path via relay (virtual last hop)");
709 for pwc in &fwd {
710 assert_eq!(pwc.path[0], relay, "intermediate must be relay, not dest");
711 assert_ne!(pwc.path[0], dest, "dest must not appear as intermediate");
712 }
713 Ok(())
714 }
715
716 #[tokio::test]
717 async fn selector_should_skip_zero_cost_paths() -> anyhow::Result<()> {
718 let me = pubkey(&SECRET_0);
720 let hop = pubkey(&SECRET_1);
721 let dest = pubkey(&SECRET_2);
722 let graph = ChannelGraph::new(me);
723 graph.add_node(hop);
724 graph.add_node(dest);
725 graph.add_edge(&me, &hop).context("adding edge me -> hop")?;
726 graph.add_edge(&hop, &dest).context("adding edge hop -> dest")?;
727 let selector = test_selector(me, graph, MAX_PATHS);
730
731 let err = selector
732 .select_path(me, dest, 1)
733 .expect_err("zero-cost paths should be filtered out");
734 anyhow::ensure!(
735 matches!(err, PathPlannerError::Path(PathError::PathNotFound(..))),
736 "expected PathNotFound, got: {err}"
737 );
738 Ok(())
739 }
740}