Skip to main content

hopr_transport_path/
selector.rs

1use hopr_api::graph::{CostFn, NetworkGraphTraverse, NetworkGraphView, costs::EdgeCostFn, traits::EdgeObservableRead};
2
3/// Default penalty factor applied to edge cost functions.
4const DEFAULT_EDGE_PENALTY: f64 = 0.5;
5
6/// Default minimum acceptable message acknowledgment rate for immediate peers.
7const 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
15/// Compute candidate paths from `src` to `dest` through `graph`.
16///
17/// `length` is the number of edges to traverse (= intermediate hops + 1).
18/// `take` caps the number of candidate paths returned.
19/// The source node is stripped from every returned path; callers receive
20/// `([intermediates..., dest], cost)`.
21fn 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                // Drop the first element (src) — callers expect [intermediates..., dest].
41                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
52/// Extended forward path search: find shorter paths using [`EdgeCostFn::forward_without_self_loopback`]
53/// and append `dest` to each one.
54///
55/// This handles the case where the last edge (relay -> dest) has no graph edge
56/// (e.g. no payment channel) but the path planner can still assume the last hop
57/// is reachable. Paths already found by Phase 1 are excluded via `existing`.
58///
59/// The cost from the shorter traversal is preserved as-is — the missing last
60/// edge contributes a neutral `1.0` multiplier (no quality data available).
61fn 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            // Strip source and append dest.
87            let mut candidate: Vec<_> = path.into_iter().skip(1).collect();
88
89            // Ensure dest is not already in the path (would create a repeated node).
90            if candidate.contains(dest) {
91                return None;
92            }
93
94            candidate.push(*dest);
95
96            // Skip paths already found by Phase 1 (compare path component only).
97            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/// A lightweight graph-backed path selector.
109///
110/// Returns all candidate paths for a `(src, dest, hops)` query directly from
111/// the network graph — no caching is performed here.  The caller (typically
112/// [`crate::planner::PathPlanner`]) is responsible for caching, TTL management,
113/// background refresh, and final path selection.
114///
115/// Stores the planner's own identity (`me`) so that it can choose the
116/// appropriate cost function:
117/// - forward path (`src == me`): [`EdgeCostFn::forward`]
118/// - return path (`dest == me`): [`EdgeCostFn::returning`]
119#[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    /// Create a new selector.
137    ///
138    /// * `me` – the planner's own offchain public key, used to determine path direction.
139    /// * `graph` – the network graph to query.
140    /// * `max_paths` – maximum number of candidate paths to return per query.
141    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    /// Return all candidate paths from `src` to `dest` via `hops` relays.
157    ///
158    /// Each returned tuple contains a path `Vec<OffchainPublicKey>` of length
159    /// `hops + 1` (`[intermediates..., dest]`; `src` excluded) paired with its
160    /// accumulated traversal cost.
161    ///
162    /// Returns `Err(PathNotFound)` when the graph yields no positive-cost paths.
163    ///
164    /// The function has a potential to run expensive operations, it should be benchmarked
165    /// in a production environment and possibly guarded (e.g. by offloading the long execution
166    /// in an async executor to avoid blocking the caller).
167    #[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            // Phase 1: search for full-length forward paths to dest.
175            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            // Phase 2: if not enough paths, do an extended search with EdgeCostFn::forward_without_self_loopback
185            // for (length - 1) edges and assume the last hop can be done by anybody.
186            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        // no need to deduplicate for now
207
208        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    /// Mark an edge as fully ready for intermediate routing.
252    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    /// Mark an edge as ready only as the last hop (forward or return).
262    ///
263    /// Forward last edge requires capacity; return last edge requires connectivity + score.
264    /// This helper sets all of them so it works for either direction.
265    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    // Helper: build a bidirectional 2-hop graph: me ↔ hop ↔ dest.
275    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        // Forward: me → hop → dest
283        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        // Reverse: dest → hop → me
288        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        // No edges at all — neither direction has a path.
301        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        // Bidirectional: me ↔ A ↔ B ↔ dest  (2 intermediate hops each way)
345        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        // Forward: me → A → B → dest
354        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        // Reverse: dest → B → A → me
361        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        // Bidirectional: me ↔ relay ↔ dest
390        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        // Bidirectional diamond: me ↔ a ↔ dest and me ↔ b ↔ dest
416        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        // Forward: me → a → dest,  me → b → dest
425        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        // Reverse: dest → a → me,  dest → b → me
434        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        // Graph has edges in both directions but no observations → all costs zero → pruned.
463        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        // No observations → cost function will return non-positive cost → pruned.
470
471        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        // Graph has direct edges both ways; requesting 2 hops should fail in both directions.
486        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        // In the real network, the last edge (relay → dest) may not have a
510        // payment channel. The graph has me → relay but NO relay → dest edge.
511        // The virtual last hop fallback should still find the forward path.
512        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        // Forward: me → relay (fully observed). NO relay → dest edge.
519        graph.add_edge(&me, &relay).unwrap();
520        mark_edge_full(&graph, &me, &relay);
521        // Reverse: dest → relay → me (both edges exist for the return path).
522        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        // Forward path should work via virtual last hop
530        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        // Return path should work normally (both edges exist)
541        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        // Bidirectional: me ↔ a ↔ b ↔ c ↔ dest  (3 intermediate hops each way)
554        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        // Forward: me → a → b → c → dest
564        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        // Reverse: dest → c → b → a → me
573        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}