Skip to main content

hopr_network_graph/petgraph/
traverse.rs

1use std::{collections::HashSet, hash::RandomState, sync::Arc};
2
3use hopr_api::{
4    OffchainPublicKey,
5    graph::{
6        function::EdgeValueFn,
7        traits::{EdgeNetworkObservableRead, EdgeObservableRead, ValueFn},
8    },
9    types::internal::routing::PathId,
10};
11use petgraph::graph::NodeIndex;
12
13use crate::{ChannelGraph, algorithm::all_simple_paths_multi, graph::InnerGraph};
14
15/// A shared cost function that computes a cumulative cost from edge observations.
16pub(crate) type SharedValueFn<C> = Arc<dyn Fn(C, &crate::Observations, usize) -> C + Send + Sync>;
17
18/// Core path-finding routine that runs `all_simple_paths_multi` on the
19/// inner petgraph.
20#[allow(clippy::too_many_arguments)]
21pub(crate) fn find_paths<C>(
22    inner: &InnerGraph,
23    source: NodeIndex,
24    destinations: &HashSet<NodeIndex>,
25    length: usize,
26    take_count: Option<usize>,
27    initial_value: C,
28    min_value: Option<C>,
29    value_fn: SharedValueFn<C>,
30) -> Vec<(Vec<OffchainPublicKey>, PathId, C)>
31where
32    C: Clone + PartialOrd,
33{
34    if length == 0 {
35        return Default::default();
36    }
37
38    let intermediates = length - 1;
39
40    let paths = all_simple_paths_multi::<Vec<_>, _, RandomState, _, _>(
41        &inner.graph,
42        source,
43        destinations,
44        None,
45        intermediates,
46        Some(intermediates),
47        initial_value,
48        min_value,
49        move |c, w, i| value_fn(c, w, i),
50    )
51    .filter_map(|(node_indices, final_cost)| {
52        // Build PathId from node indices along the path
53        let mut path_id: PathId = [0u64; 5];
54        for (i, &node_idx) in node_indices.iter().enumerate() {
55            if i >= path_id.len() {
56                return None;
57            }
58            path_id[i] = node_idx.index() as u64;
59        }
60
61        // Convert node indices to public keys; strip `source` (first element).
62        // path_id retains all indices including source for stable caching.
63        let nodes = node_indices
64            .into_iter()
65            .skip(1)
66            .filter_map(|v| inner.indices.get_by_right(&v).copied())
67            .collect::<Vec<_>>();
68        // After stripping source: intermediates + destination = length nodes.
69        // `simple_paths` additionally pops the destination; the others return as-is.
70        (nodes.len() == length).then_some((nodes, path_id, final_cost))
71    });
72
73    if let Some(take_count) = take_count {
74        paths.take(take_count).collect::<Vec<_>>()
75    } else {
76        paths.collect::<Vec<_>>()
77    }
78}
79
80impl hopr_api::graph::NetworkGraphTraverse for ChannelGraph {
81    type NodeId = OffchainPublicKey;
82    type Observed = crate::Observations;
83
84    fn simple_paths<C: ValueFn<Weight = Self::Observed>>(
85        &self,
86        source: &Self::NodeId,
87        destination: &Self::NodeId,
88        length: usize,
89        take_count: Option<usize>,
90        value_fn: C,
91    ) -> Vec<(Vec<Self::NodeId>, PathId, C::Value)> {
92        if length == 0 {
93            return Default::default();
94        }
95
96        let inner = self.inner.read();
97        let Some(start) = inner.indices.get_by_left(source) else {
98            return Default::default();
99        };
100        let Some(end) = inner.indices.get_by_left(destination) else {
101            return Default::default();
102        };
103        let end = HashSet::from_iter([*end]);
104
105        // find_paths returns [intermediates…, destination]; pop destination since caller
106        // supplies it as an explicit input argument and it must not repeat in the path body.
107        find_paths(
108            &inner,
109            *start,
110            &end,
111            length,
112            take_count,
113            value_fn.initial_value(),
114            value_fn.min_value(),
115            value_fn.into_value_fn(),
116        )
117        .into_iter()
118        .map(|(mut nodes, path_id, cost)| {
119            nodes.pop(); // strip destination — caller already knows it
120            (nodes, path_id, cost)
121        })
122        .collect()
123    }
124
125    fn simple_paths_from<C: ValueFn<Weight = Self::Observed>>(
126        &self,
127        source: &Self::NodeId,
128        length: usize,
129        take_count: Option<usize>,
130        value_fn: C,
131    ) -> Vec<(Vec<Self::NodeId>, PathId, C::Value)> {
132        if length == 0 {
133            return Default::default();
134        }
135
136        let inner = self.inner.read();
137        let Some(start) = inner.indices.get_by_left(source) else {
138            return Default::default();
139        };
140
141        let destinations: HashSet<NodeIndex> = inner.graph.node_indices().filter(|idx| idx != start).collect();
142
143        find_paths(
144            &inner,
145            *start,
146            &destinations,
147            length,
148            take_count,
149            value_fn.initial_value(),
150            value_fn.min_value(),
151            value_fn.into_value_fn(),
152        )
153    }
154
155    fn simple_loopback_to_self(&self, length: usize, take_count: Option<usize>) -> Vec<(Vec<Self::NodeId>, PathId)> {
156        if length > 1 {
157            let inner = self.inner.read();
158
159            if let Some(me_idx) = inner.indices.get_by_left(&self.me) {
160                let connected_neighbors = inner
161                    .graph
162                    .neighbors(*me_idx)
163                    .filter(|neighbor| {
164                        inner
165                            .graph
166                            .edges_connecting(*me_idx, *neighbor)
167                            .next()
168                            .and_then(|e| e.weight().immediate_qos().map(|e| e.is_connected()))
169                            .unwrap_or(false)
170                    })
171                    .collect::<HashSet<_>>();
172
173                let value_fn = EdgeValueFn::forward_without_self_loopback(self.edge_penalty, self.min_ack_rate);
174
175                return find_paths(
176                    &inner,
177                    *me_idx,
178                    &connected_neighbors,
179                    length,
180                    take_count,
181                    value_fn.initial_value(),
182                    value_fn.min_value(),
183                    value_fn.into_value_fn(),
184                )
185                .into_iter()
186                .map(|(mut a, mut b, _c)| {
187                    // find_paths already strips the leading `me` (source), so `a` is
188                    // [intermediates…, connected_neighbor]. Append `me` to close the loopback;
189                    // this is the only sanctioned position where `me` appears as a "destination".
190                    //
191                    // b is filled by find_paths BEFORE skip(1), so b[0] = me_idx and
192                    // b[1..=path_node_count] = path nodes. Closing me goes at b[path_node_count + 1].
193                    let path_node_count = a.len();
194                    if path_node_count + 1 < b.len() {
195                        b[path_node_count + 1] = me_idx.index() as u64;
196                    }
197                    a.push(self.me);
198                    (a, b)
199                })
200                .collect();
201            };
202        }
203
204        vec![]
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use anyhow::Context;
211    use hex_literal::hex;
212    use hopr_api::{
213        graph::{
214            NetworkGraphTraverse, NetworkGraphWrite,
215            function::EdgeValueFn,
216            traits::{EdgeObservableWrite, EdgeWeightType},
217        },
218        types::{
219            crypto::prelude::{Keypair, OffchainKeypair},
220            internal::routing::PathId,
221        },
222    };
223
224    use super::*;
225
226    /// Deliberately different from the production default (0.5) so tests
227    /// verify that the configured penalty is actually propagated.
228    const TEST_EDGE_PENALTY: f64 = 0.73;
229    /// Disabled in tests — no protocol conformance data is recorded.
230    const TEST_MIN_ACK_RATE: f64 = 0.0;
231
232    /// Fixed test secret keys (reused from the broader codebase).
233    const SECRET_0: [u8; 32] = hex!("60741b83b99e36aa0c1331578156e16b8e21166d01834abb6c64b103f885734d");
234    const SECRET_1: [u8; 32] = hex!("71bf1f42ebbfcd89c3e197a3fd7cda79b92499e509b6fefa0fe44d02821d146a");
235    const SECRET_2: [u8; 32] = hex!("c24bd833704dd2abdae3933fcc9962c2ac404f84132224c474147382d4db2299");
236    const SECRET_3: [u8; 32] = hex!("e0bf93e9c916104da00b1850adc4608bd7e9087bbd3f805451f4556aa6b3fd6e");
237    const SECRET_4: [u8; 32] = hex!("cfc66f718ec66fb822391775d749d7a0d66b690927673634816b63339bc12a3c");
238    const SECRET_5: [u8; 32] = hex!("203ca4d3c5f98dd2066bb204b5930c10b15c095585c224c826b4e11f08bfa85d");
239    const SECRET_7: [u8; 32] = hex!("4ab03f6f75f845ca1bf8b7104804ea5bda18bda29d1ec5fc5d4267feca5fb8e1");
240
241    /// Creates an OffchainPublicKey from a fixed secret.
242    fn pubkey_from(secret: &[u8; 32]) -> OffchainPublicKey {
243        *OffchainKeypair::from_secret(secret).expect("valid secret key").public()
244    }
245
246    /// Marks an edge as connected with an immediate probe measurement, satisfying the
247    /// cost function's requirement for the last edge in a path.
248    fn mark_edge_connected(graph: &ChannelGraph, src: &OffchainPublicKey, dest: &OffchainPublicKey) {
249        graph.upsert_edge(src, dest, |obs| {
250            obs.record(EdgeWeightType::Connected(true));
251            obs.record(EdgeWeightType::Immediate(Ok(std::time::Duration::from_millis(50))));
252        });
253    }
254
255    #[test]
256    fn one_edge_path_should_return_direct_route() -> anyhow::Result<()> {
257        let me = pubkey_from(&SECRET_0);
258        let dest = pubkey_from(&SECRET_1);
259
260        let graph = ChannelGraph::new(me);
261        graph.add_node(dest);
262        graph.add_edge(&me, &dest)?;
263        mark_edge_loopback_ready(&graph, &me, &dest);
264
265        let routes = graph.simple_paths(
266            &me,
267            &dest,
268            1,
269            None,
270            EdgeValueFn::forward(
271                std::num::NonZeroUsize::new(1).context("should be non-zero")?,
272                TEST_EDGE_PENALTY,
273                TEST_MIN_ACK_RATE,
274            ),
275        );
276
277        assert_eq!(routes.len(), 1, "should find exactly one 1-edge route");
278
279        Ok(())
280    }
281
282    #[test]
283    fn two_edge_path_should_route_through_intermediate() -> anyhow::Result<()> {
284        let me = pubkey_from(&SECRET_0);
285        let hop = pubkey_from(&SECRET_1);
286        let dest = pubkey_from(&SECRET_2);
287
288        let graph = ChannelGraph::new(me);
289        graph.add_node(hop);
290        graph.add_node(dest);
291        graph.add_edge(&me, &hop)?;
292        graph.add_edge(&hop, &dest)?;
293        mark_edge_loopback_ready(&graph, &me, &hop);
294        mark_edge_connected(&graph, &hop, &dest);
295
296        let length = std::num::NonZeroUsize::new(2).context("should be non-zero")?;
297        let routes = graph.simple_paths(
298            &me,
299            &dest,
300            2,
301            None,
302            EdgeValueFn::forward(length, TEST_EDGE_PENALTY, TEST_MIN_ACK_RATE),
303        );
304
305        assert!(!routes.is_empty(), "should find at least one 2-edge route");
306
307        Ok(())
308    }
309
310    #[test]
311    fn penalty_should_affect_cost_of_unprobed_edges() -> anyhow::Result<()> {
312        let me = pubkey_from(&SECRET_0);
313        let hop = pubkey_from(&SECRET_1);
314        let dest = pubkey_from(&SECRET_2);
315
316        let graph = ChannelGraph::new(me);
317        graph.add_node(hop);
318        graph.add_node(dest);
319        graph.add_edge(&me, &hop)?;
320        graph.add_edge(&hop, &dest)?;
321
322        // First edge: fully probed — produces score ~1.0.
323        mark_edge_loopback_ready(&graph, &me, &hop);
324        // Last edge: no observations at all — penalty must kick in.
325
326        let length = std::num::NonZeroUsize::new(2).context("should be non-zero")?;
327
328        let routes_test = graph.simple_paths(
329            &me,
330            &dest,
331            2,
332            None,
333            EdgeValueFn::forward(length, TEST_EDGE_PENALTY, TEST_MIN_ACK_RATE),
334        );
335        let routes_other = graph.simple_paths(
336            &me,
337            &dest,
338            2,
339            None,
340            EdgeValueFn::forward(length, 0.99, TEST_MIN_ACK_RATE),
341        );
342
343        assert_eq!(routes_test.len(), 1);
344        assert_eq!(routes_other.len(), 1);
345
346        let (_, _, cost_test) = &routes_test[0];
347        let (_, _, cost_other) = &routes_other[0];
348        assert!(
349            (cost_test - cost_other).abs() > f64::EPSILON,
350            "different penalties ({TEST_EDGE_PENALTY} vs 0.99) should produce different costs for unprobed edges, got \
351             {cost_test} vs {cost_other}"
352        );
353
354        Ok(())
355    }
356
357    #[test]
358    fn unreachable_destination_should_return_empty() -> anyhow::Result<()> {
359        let me = pubkey_from(&SECRET_0);
360        let dest = pubkey_from(&SECRET_1);
361
362        let graph = ChannelGraph::new(me);
363        graph.add_node(dest);
364        // No edge between me and dest
365
366        let routes = graph.simple_paths(
367            &me,
368            &dest,
369            1,
370            None,
371            EdgeValueFn::forward(
372                std::num::NonZeroUsize::new(1).context("should be non-zero")?,
373                TEST_EDGE_PENALTY,
374                TEST_MIN_ACK_RATE,
375            ),
376        );
377
378        assert!(routes.is_empty(), "should return no routes when unreachable");
379
380        Ok(())
381    }
382
383    #[test]
384    fn unknown_destination_should_return_empty() -> anyhow::Result<()> {
385        let me = pubkey_from(&SECRET_0);
386        let unknown = pubkey_from(&SECRET_1);
387
388        let graph = ChannelGraph::new(me);
389        let routes = graph.simple_paths(
390            &me,
391            &unknown,
392            1,
393            None,
394            EdgeValueFn::forward(
395                std::num::NonZeroUsize::new(1).context("should be non-zero")?,
396                TEST_EDGE_PENALTY,
397                TEST_MIN_ACK_RATE,
398            ),
399        );
400
401        assert!(routes.is_empty());
402
403        Ok(())
404    }
405
406    #[test]
407    fn diamond_topology_should_yield_multiple_paths() -> anyhow::Result<()> {
408        //   me -> a -> dest
409        //   me -> b -> dest
410        let me = pubkey_from(&SECRET_0);
411        let a = pubkey_from(&SECRET_1);
412        let b = pubkey_from(&SECRET_2);
413        let dest = pubkey_from(&SECRET_3);
414
415        let graph = ChannelGraph::new(me);
416        graph.add_node(a);
417        graph.add_node(b);
418        graph.add_node(dest);
419        graph.add_edge(&me, &a)?;
420        graph.add_edge(&me, &b)?;
421        graph.add_edge(&a, &dest)?;
422        graph.add_edge(&b, &dest)?;
423        mark_edge_loopback_ready(&graph, &me, &a);
424        mark_edge_loopback_ready(&graph, &me, &b);
425        mark_edge_connected(&graph, &a, &dest);
426        mark_edge_connected(&graph, &b, &dest);
427
428        let routes = graph.simple_paths(
429            &me,
430            &dest,
431            2,
432            None,
433            EdgeValueFn::forward(
434                std::num::NonZeroUsize::new(2).context("should be non-zero")?,
435                TEST_EDGE_PENALTY,
436                TEST_MIN_ACK_RATE,
437            ),
438        );
439        assert_eq!(routes.len(), 2, "diamond topology should yield two 2-edge routes");
440        Ok(())
441    }
442
443    #[test]
444    fn three_edge_chain_should_find_single_path() -> anyhow::Result<()> {
445        // me -> a -> b -> dest
446        let me = pubkey_from(&SECRET_0);
447        let a = pubkey_from(&SECRET_1);
448        let b = pubkey_from(&SECRET_2);
449        let dest = pubkey_from(&SECRET_3);
450
451        let graph = ChannelGraph::new(me);
452        graph.add_node(a);
453        graph.add_node(b);
454        graph.add_node(dest);
455        graph.add_edge(&me, &a)?;
456        graph.add_edge(&a, &b)?;
457        graph.add_edge(&b, &dest)?;
458        mark_edge_loopback_ready(&graph, &me, &a);
459        mark_edge_with_capacity(&graph, &a, &b);
460
461        let routes = graph.simple_paths(
462            &me,
463            &dest,
464            3,
465            None,
466            EdgeValueFn::forward(
467                std::num::NonZeroUsize::new(3).context("should be non-zero")?,
468                TEST_EDGE_PENALTY,
469                TEST_MIN_ACK_RATE,
470            ),
471        );
472        assert_eq!(routes.len(), 1, "should find exactly one 3-edge route");
473        Ok(())
474    }
475
476    #[test]
477    fn back_edge_should_not_produce_cyclic_paths() -> anyhow::Result<()> {
478        // me -> a -> b -> dest, plus a -> me (back-edge creating cycle)
479        let me = pubkey_from(&SECRET_0);
480        let a = pubkey_from(&SECRET_1);
481        let b = pubkey_from(&SECRET_2);
482        let dest = pubkey_from(&SECRET_3);
483
484        let graph = ChannelGraph::new(me);
485        graph.add_node(a);
486        graph.add_node(b);
487        graph.add_node(dest);
488        graph.add_edge(&me, &a)?;
489        graph.add_edge(&a, &b)?;
490        graph.add_edge(&b, &dest)?;
491        graph.add_edge(&a, &me)?; // back-edge
492        mark_edge_loopback_ready(&graph, &me, &a);
493        mark_edge_with_capacity(&graph, &a, &b);
494
495        let routes = graph.simple_paths(
496            &me,
497            &dest,
498            3,
499            None,
500            EdgeValueFn::forward(
501                std::num::NonZeroUsize::new(3).context("should be non-zero")?,
502                TEST_EDGE_PENALTY,
503                TEST_MIN_ACK_RATE,
504            ),
505        );
506        assert_eq!(routes.len(), 1, "cycle should not produce extra paths");
507        Ok(())
508    }
509
510    #[test]
511    fn mismatched_edge_count_should_return_empty() -> anyhow::Result<()> {
512        // me -> dest (1 edge), but ask for 2 edges
513        let me = pubkey_from(&SECRET_0);
514        let dest = pubkey_from(&SECRET_1);
515        let graph = ChannelGraph::new(me);
516        graph.add_node(dest);
517        graph.add_edge(&me, &dest)?;
518
519        let routes = graph.simple_paths(
520            &me,
521            &dest,
522            2,
523            None,
524            EdgeValueFn::forward(
525                std::num::NonZeroUsize::new(2).context("should be non-zero")?,
526                TEST_EDGE_PENALTY,
527                TEST_MIN_ACK_RATE,
528            ),
529        );
530        assert!(routes.is_empty(), "no 2-edge route should exist for a single edge");
531        Ok(())
532    }
533
534    #[test]
535    fn zero_edge_should_always_return_empty() -> anyhow::Result<()> {
536        let me = pubkey_from(&SECRET_0);
537        let other = pubkey_from(&SECRET_1);
538        let graph = ChannelGraph::new(me);
539
540        // length=0 returns empty before the cost fn is used; any NonZeroUsize value is fine
541        let routes = graph.simple_paths(
542            &me,
543            &other,
544            0,
545            None,
546            EdgeValueFn::forward(
547                std::num::NonZeroUsize::new(1).context("should be non-zero")?,
548                TEST_EDGE_PENALTY,
549                TEST_MIN_ACK_RATE,
550            ),
551        );
552        assert!(routes.is_empty(), "zero-edge path should find no routes");
553        Ok(())
554    }
555
556    #[test]
557    fn reverse_edge_should_not_be_traversable() -> anyhow::Result<()> {
558        // me -> a, but no a -> dest, only dest -> a
559        let me = pubkey_from(&SECRET_0);
560        let a = pubkey_from(&SECRET_1);
561        let dest = pubkey_from(&SECRET_2);
562
563        let graph = ChannelGraph::new(me);
564        graph.add_node(a);
565        graph.add_node(dest);
566        graph.add_edge(&me, &a)?;
567        graph.add_edge(&dest, &a)?; // wrong direction
568
569        let routes = graph.simple_paths(
570            &me,
571            &dest,
572            2,
573            None,
574            EdgeValueFn::forward(
575                std::num::NonZeroUsize::new(2).context("should be non-zero")?,
576                TEST_EDGE_PENALTY,
577                TEST_MIN_ACK_RATE,
578            ),
579        );
580        assert!(routes.is_empty(), "should not traverse edge in wrong direction");
581        Ok(())
582    }
583
584    #[test]
585    fn non_trivial_graph_should_find_all_simple_paths() -> anyhow::Result<()> {
586        // Topology (7 nodes):
587        //
588        //   me(0) ──→ a(1)
589        //   me(0) ──→ b(2)
590        //   a(1)  ──→ c(3)   [capacity]
591        //   a(1)  ──→ d(4)   [capacity]
592        //   b(2)  ──→ c(3)   [capacity]
593        //   b(2)  ──→ d(4)   [capacity]
594        //   b(2)  ──→ e(5)   [capacity]
595        //   c(3)  ──→ f(7)
596        //   d(4)  ──→ f(7)
597        //   e(5)  ──→ f(7)
598        //
599        // Valid 3-edge paths (me → ? → ? → f):
600        //   1. me → a → c → f
601        //   2. me → a → d → f
602        //   3. me → b → c → f
603        //   4. me → b → d → f
604        //   5. me → b → e → f
605        //
606        // Blocked paths:
607        //   - me → a → e → f : edge a→e missing
608        //   - me → e → … → f : edge me→e missing
609
610        let me = pubkey_from(&SECRET_0);
611        let a = pubkey_from(&SECRET_1);
612        let b = pubkey_from(&SECRET_2);
613        let c = pubkey_from(&SECRET_3);
614        let d = pubkey_from(&SECRET_4);
615        let e = pubkey_from(&SECRET_5);
616        let f = pubkey_from(&SECRET_7);
617
618        let graph = ChannelGraph::new(me);
619        for node in [a, b, c, d, e, f] {
620            graph.add_node(node);
621        }
622
623        // Edges from me
624        graph.add_edge(&me, &a)?;
625        graph.add_edge(&me, &b)?;
626
627        // Edges from a
628        graph.add_edge(&a, &c)?;
629        graph.add_edge(&a, &d)?;
630
631        // Edges from b
632        graph.add_edge(&b, &c)?;
633        graph.add_edge(&b, &d)?;
634        graph.add_edge(&b, &e)?;
635
636        // Edges to f (last hop)
637        graph.add_edge(&c, &f)?;
638        graph.add_edge(&d, &f)?;
639        graph.add_edge(&e, &f)?;
640
641        // Mark first edges with full QoS (connected + intermediate capacity)
642        mark_edge_loopback_ready(&graph, &me, &a);
643        mark_edge_loopback_ready(&graph, &me, &b);
644
645        // Mark middle edges with capacity (required by EdgeValueFn::forward)
646        mark_edge_with_capacity(&graph, &a, &c);
647        mark_edge_with_capacity(&graph, &a, &d);
648        mark_edge_with_capacity(&graph, &b, &c);
649        mark_edge_with_capacity(&graph, &b, &d);
650        mark_edge_with_capacity(&graph, &b, &e);
651
652        // Last edges (c→f, d→f, e→f) are lenient with EdgeValueFn::forward
653
654        // --- 3-edge paths: should find exactly 5 ---
655        let routes_3 = graph.simple_paths(
656            &me,
657            &f,
658            3,
659            None,
660            EdgeValueFn::forward(
661                std::num::NonZeroUsize::new(3).context("should be non-zero")?,
662                TEST_EDGE_PENALTY,
663                TEST_MIN_ACK_RATE,
664            ),
665        );
666        assert_eq!(routes_3.len(), 5, "should find exactly 5 three-edge paths");
667
668        // Verify all returned paths have positive cost.
669        // simple_paths strips both src and dest, so a 3-edge path has 2 intermediates.
670        for (path, _path_id, cost) in &routes_3 {
671            assert!(*cost > 0.0, "path {path:?} should have positive cost, got {cost}");
672            assert_eq!(
673                path.len(),
674                2,
675                "3-edge path should contain 2 intermediates (src and dest stripped)"
676            );
677            assert!(!path.contains(&me), "path must not contain src");
678            assert!(!path.contains(&f), "path must not contain dest");
679        }
680
681        // --- 1-edge path: no direct me→f edge ---
682        let routes_1 = graph.simple_paths(
683            &me,
684            &f,
685            1,
686            None,
687            EdgeValueFn::forward(
688                std::num::NonZeroUsize::new(1).context("should be non-zero")?,
689                TEST_EDGE_PENALTY,
690                TEST_MIN_ACK_RATE,
691            ),
692        );
693        assert!(routes_1.is_empty(), "no direct edge from me to f");
694
695        Ok(())
696    }
697
698    #[test]
699    fn three_edge_loop_should_return_empty_because_source_is_visited() -> anyhow::Result<()> {
700        // Ring topology: me → a → b → me (3 edges forming a cycle)
701        //
702        // The underlying all_simple_paths_multi algorithm marks the source node
703        // as visited before traversal begins. Because the destination equals the
704        // source, the algorithm can never "reach" it — the visited-set check
705        // (`visited.contains(&child)`) rejects the back-edge to source, and the
706        // expansion guard (`to.iter().any(|n| !visited.contains(n))`) is always
707        // false since the only target (source) is always visited.
708        let me = pubkey_from(&SECRET_0);
709        let a = pubkey_from(&SECRET_1);
710        let b = pubkey_from(&SECRET_2);
711
712        let graph = ChannelGraph::new(me);
713        graph.add_node(a);
714        graph.add_node(b);
715        graph.add_edge(&me, &a)?;
716        graph.add_edge(&a, &b)?;
717        graph.add_edge(&b, &me)?;
718        mark_edge_connected(&graph, &b, &me);
719
720        let routes = graph.simple_paths(
721            &me,
722            &me,
723            3,
724            None,
725            EdgeValueFn::forward(
726                std::num::NonZeroUsize::new(3).context("should be non-zero")?,
727                TEST_EDGE_PENALTY,
728                TEST_MIN_ACK_RATE,
729            ),
730        );
731        assert!(
732            routes.is_empty(),
733            "simple_paths cannot discover cycles (source == destination) due to visited-set semantics"
734        );
735
736        Ok(())
737    }
738
739    #[test]
740    fn path_id_should_contain_node_indices_for_one_edge() -> anyhow::Result<()> {
741        // me = node 0, dest = node 1
742        let me = pubkey_from(&SECRET_0);
743        let dest = pubkey_from(&SECRET_1);
744
745        let graph = ChannelGraph::new(me);
746        graph.add_node(dest);
747        graph.add_edge(&me, &dest)?;
748        mark_edge_loopback_ready(&graph, &me, &dest);
749
750        let routes = graph.simple_paths(
751            &me,
752            &dest,
753            1,
754            None,
755            EdgeValueFn::forward(
756                std::num::NonZeroUsize::new(1).context("should be non-zero")?,
757                TEST_EDGE_PENALTY,
758                TEST_MIN_ACK_RATE,
759            ),
760        );
761        assert_eq!(routes.len(), 1);
762
763        let (_path, path_id, _cost) = &routes[0];
764        assert_eq!(path_id[0], 0, "first node should be me (node index 0)");
765        assert_eq!(path_id[1], 1, "second node should be dest (node index 1)");
766        assert_eq!(path_id[2..], [0, 0, 0], "unused positions should be 0");
767
768        Ok(())
769    }
770
771    #[test]
772    fn path_id_should_contain_node_indices_for_three_edges() -> anyhow::Result<()> {
773        // me = node 0, a = node 1, b = node 2, dest = node 3
774        let me = pubkey_from(&SECRET_0);
775        let a = pubkey_from(&SECRET_1);
776        let b = pubkey_from(&SECRET_2);
777        let dest = pubkey_from(&SECRET_3);
778
779        let graph = ChannelGraph::new(me);
780        graph.add_node(a);
781        graph.add_node(b);
782        graph.add_node(dest);
783        graph.add_edge(&me, &a)?;
784        graph.add_edge(&a, &b)?;
785        graph.add_edge(&b, &dest)?;
786        mark_edge_loopback_ready(&graph, &me, &a);
787        mark_edge_with_capacity(&graph, &a, &b);
788
789        let routes = graph.simple_paths(
790            &me,
791            &dest,
792            3,
793            None,
794            EdgeValueFn::forward(
795                std::num::NonZeroUsize::new(3).context("should be non-zero")?,
796                TEST_EDGE_PENALTY,
797                TEST_MIN_ACK_RATE,
798            ),
799        );
800        assert_eq!(routes.len(), 1);
801
802        let (_path, path_id, _cost) = &routes[0];
803        assert_eq!(path_id[0], 0, "me should be node index 0");
804        assert_eq!(path_id[1], 1, "a should be node index 1");
805        assert_eq!(path_id[2], 2, "b should be node index 2");
806        assert_eq!(path_id[3], 3, "dest should be node index 3");
807        assert_eq!(path_id[4], 0, "unused position should be 0");
808
809        Ok(())
810    }
811
812    #[test]
813    fn path_id_should_differ_for_distinct_paths_in_diamond() -> anyhow::Result<()> {
814        //   me → a → dest
815        //   me → b → dest
816        // me = node 0, a = node 1, b = node 2, dest = node 3
817        let me = pubkey_from(&SECRET_0);
818        let a = pubkey_from(&SECRET_1);
819        let b = pubkey_from(&SECRET_2);
820        let dest = pubkey_from(&SECRET_3);
821
822        let graph = ChannelGraph::new(me);
823        graph.add_node(a);
824        graph.add_node(b);
825        graph.add_node(dest);
826        graph.add_edge(&me, &a)?;
827        graph.add_edge(&me, &b)?;
828        graph.add_edge(&a, &dest)?;
829        graph.add_edge(&b, &dest)?;
830        mark_edge_loopback_ready(&graph, &me, &a);
831        mark_edge_loopback_ready(&graph, &me, &b);
832        mark_edge_connected(&graph, &a, &dest);
833        mark_edge_connected(&graph, &b, &dest);
834
835        let routes = graph.simple_paths(
836            &me,
837            &dest,
838            2,
839            None,
840            EdgeValueFn::forward(
841                std::num::NonZeroUsize::new(2).context("should be non-zero")?,
842                TEST_EDGE_PENALTY,
843                TEST_MIN_ACK_RATE,
844            ),
845        );
846        assert_eq!(routes.len(), 2, "diamond should yield two 2-edge routes");
847
848        let path_ids: Vec<PathId> = routes.iter().map(|(_, pid, _)| *pid).collect();
849        assert_ne!(path_ids[0], path_ids[1], "distinct paths should have different PathIds");
850
851        // Each path: [me(0), intermediate(1 or 2), dest(3), 0, 0]
852        for pid in &path_ids {
853            assert_eq!(pid[0], 0, "first node should be me (node index 0)");
854            assert!(pid[1] == 1 || pid[1] == 2, "second node should be a (1) or b (2)");
855            assert_eq!(pid[2], 3, "third node should be dest (node index 3)");
856            assert_eq!(pid[3..], [0, 0], "unused positions should be 0");
857        }
858
859        Ok(())
860    }
861
862    // ── return-path tests (EdgeValueFn::returning) ──────────────────────────
863
864    #[test]
865    fn return_path_one_edge_should_find_route() -> anyhow::Result<()> {
866        // Return path: dest -> me (1 edge)
867        // For length=1, path_index=0 matches the first-edge arm which requires capacity.
868        let me = pubkey_from(&SECRET_0);
869        let dest = pubkey_from(&SECRET_1);
870
871        let graph = ChannelGraph::new(me);
872        graph.add_node(dest);
873        graph.add_edge(&dest, &me)?;
874        // dest→me: for length=1 this is the last edge, requiring connectivity
875        mark_edge_connected(&graph, &dest, &me);
876
877        let routes = graph.simple_paths(
878            &dest,
879            &me,
880            1,
881            None,
882            EdgeValueFn::returning(
883                std::num::NonZeroUsize::new(1).context("should be non-zero")?,
884                TEST_EDGE_PENALTY,
885                TEST_MIN_ACK_RATE,
886            ),
887        );
888
889        assert_eq!(routes.len(), 1, "should find exactly one 1-edge return route");
890        Ok(())
891    }
892
893    #[test]
894    fn return_path_two_edge_should_route_through_intermediate() -> anyhow::Result<()> {
895        // Return path: dest -> relay -> me (2 edges)
896        // Edge 0 (dest→relay): needs capacity only
897        // Edge 1 (relay→me): needs connectivity (last edge)
898        let me = pubkey_from(&SECRET_0);
899        let relay = pubkey_from(&SECRET_1);
900        let dest = pubkey_from(&SECRET_2);
901
902        let graph = ChannelGraph::new(me);
903        graph.add_node(relay);
904        graph.add_node(dest);
905        graph.add_edge(&dest, &relay)?;
906        graph.add_edge(&relay, &me)?;
907        // dest→relay: first edge needs capacity
908        mark_edge_with_capacity(&graph, &dest, &relay);
909        // relay→me: last edge needs connectivity
910        mark_edge_connected(&graph, &relay, &me);
911
912        let routes = graph.simple_paths(
913            &dest,
914            &me,
915            2,
916            None,
917            EdgeValueFn::returning(
918                std::num::NonZeroUsize::new(2).context("should be non-zero")?,
919                TEST_EDGE_PENALTY,
920                TEST_MIN_ACK_RATE,
921            ),
922        );
923
924        assert!(!routes.is_empty(), "should find at least one 2-edge return route");
925        Ok(())
926    }
927
928    #[test]
929    fn return_path_last_edge_without_connectivity_should_be_pruned() -> anyhow::Result<()> {
930        // Return path: dest -> relay -> me (2 edges)
931        // relay→me lacks connectivity → last-edge cost goes negative
932        let me = pubkey_from(&SECRET_0);
933        let relay = pubkey_from(&SECRET_1);
934        let dest = pubkey_from(&SECRET_2);
935
936        let graph = ChannelGraph::new(me);
937        graph.add_node(relay);
938        graph.add_node(dest);
939        graph.add_edge(&dest, &relay)?;
940        graph.add_edge(&relay, &me)?;
941        // dest→relay: has capacity (passes edge-0)
942        mark_edge_with_capacity(&graph, &dest, &relay);
943        // relay→me: only capacity, NO connectivity → last edge fails
944
945        let routes = graph.simple_paths(
946            &dest,
947            &me,
948            2,
949            None,
950            EdgeValueFn::returning(
951                std::num::NonZeroUsize::new(2).context("should be non-zero")?,
952                TEST_EDGE_PENALTY,
953                TEST_MIN_ACK_RATE,
954            ),
955        );
956
957        assert!(
958            routes.is_empty(),
959            "return path should be pruned when last edge lacks connectivity"
960        );
961        Ok(())
962    }
963
964    #[test]
965    fn return_path_first_edge_without_capacity_should_be_pruned() -> anyhow::Result<()> {
966        // Return path: dest -> relay -> me (2 edges)
967        // dest→relay has no capacity → first-edge cost goes negative
968        let me = pubkey_from(&SECRET_0);
969        let relay = pubkey_from(&SECRET_1);
970        let dest = pubkey_from(&SECRET_2);
971
972        let graph = ChannelGraph::new(me);
973        graph.add_node(relay);
974        graph.add_node(dest);
975        graph.add_edge(&dest, &relay)?;
976        graph.add_edge(&relay, &me)?;
977        // dest→relay: no capacity (default edge)
978        // relay→me: connected
979        mark_edge_connected(&graph, &relay, &me);
980
981        let routes = graph.simple_paths(
982            &dest,
983            &me,
984            2,
985            None,
986            EdgeValueFn::returning(
987                std::num::NonZeroUsize::new(2).context("should be non-zero")?,
988                TEST_EDGE_PENALTY,
989                TEST_MIN_ACK_RATE,
990            ),
991        );
992
993        assert!(
994            routes.is_empty(),
995            "return path should be pruned when first edge lacks capacity"
996        );
997        Ok(())
998    }
999
1000    #[test]
1001    fn return_path_diamond_should_yield_multiple_paths() -> anyhow::Result<()> {
1002        // Return paths: dest -> a -> me, dest -> b -> me
1003        let me = pubkey_from(&SECRET_0);
1004        let a = pubkey_from(&SECRET_1);
1005        let b = pubkey_from(&SECRET_2);
1006        let dest = pubkey_from(&SECRET_3);
1007
1008        let graph = ChannelGraph::new(me);
1009        graph.add_node(a);
1010        graph.add_node(b);
1011        graph.add_node(dest);
1012        graph.add_edge(&dest, &a)?;
1013        graph.add_edge(&dest, &b)?;
1014        graph.add_edge(&a, &me)?;
1015        graph.add_edge(&b, &me)?;
1016        // First edges (dest→a, dest→b): need capacity
1017        mark_edge_with_capacity(&graph, &dest, &a);
1018        mark_edge_with_capacity(&graph, &dest, &b);
1019        // Last edges (a→me, b→me): need connectivity
1020        mark_edge_connected(&graph, &a, &me);
1021        mark_edge_connected(&graph, &b, &me);
1022
1023        let routes = graph.simple_paths(
1024            &dest,
1025            &me,
1026            2,
1027            None,
1028            EdgeValueFn::returning(
1029                std::num::NonZeroUsize::new(2).context("should be non-zero")?,
1030                TEST_EDGE_PENALTY,
1031                TEST_MIN_ACK_RATE,
1032            ),
1033        );
1034        assert_eq!(
1035            routes.len(),
1036            2,
1037            "diamond topology should yield two 2-edge return routes"
1038        );
1039        Ok(())
1040    }
1041
1042    // ── simple_loopback_to_self tests ──────────────────────────────────
1043
1044    /// Marks an edge as connected AND with intermediate capacity so that it
1045    /// satisfies the `EdgeValueFn::forward_without_self_loopback` at edge index 0 (connected + capacity)
1046    /// and at any other index (capacity).
1047    fn mark_edge_loopback_ready(graph: &ChannelGraph, src: &OffchainPublicKey, dest: &OffchainPublicKey) {
1048        graph.upsert_edge(src, dest, |obs| {
1049            obs.record(EdgeWeightType::Connected(true));
1050            obs.record(EdgeWeightType::Immediate(Ok(std::time::Duration::from_millis(50))));
1051            obs.record(EdgeWeightType::Intermediate(Ok(std::time::Duration::from_millis(50))));
1052            obs.record(EdgeWeightType::Capacity(Some(1000)));
1053        });
1054    }
1055
1056    /// Marks an edge with intermediate capacity and probe data (no connected flag).
1057    /// Satisfies `EdgeValueFn::forward_without_self_loopback` at index > 0 but NOT at index 0.
1058    fn mark_edge_with_capacity(graph: &ChannelGraph, src: &OffchainPublicKey, dest: &OffchainPublicKey) {
1059        graph.upsert_edge(src, dest, |obs| {
1060            obs.record(EdgeWeightType::Intermediate(Ok(std::time::Duration::from_millis(50))));
1061            obs.record(EdgeWeightType::Capacity(Some(1000)));
1062        });
1063    }
1064
1065    #[test]
1066    fn loopback_returns_empty_for_length_zero() {
1067        let me = pubkey_from(&SECRET_0);
1068        let graph = ChannelGraph::new(me);
1069        assert!(graph.simple_loopback_to_self(0, None).is_empty());
1070    }
1071
1072    #[test]
1073    fn loopback_returns_empty_for_length_one() {
1074        let me = pubkey_from(&SECRET_0);
1075        let a = pubkey_from(&SECRET_1);
1076        let graph = ChannelGraph::new(me);
1077        graph.add_node(a);
1078        graph.add_edge(&me, &a).unwrap();
1079        mark_edge_loopback_ready(&graph, &me, &a);
1080
1081        assert!(
1082            graph.simple_loopback_to_self(1, None).is_empty(),
1083            "length=1 is below the minimum threshold"
1084        );
1085    }
1086
1087    #[test]
1088    fn loopback_returns_empty_without_any_peers() {
1089        let me = pubkey_from(&SECRET_0);
1090        let graph = ChannelGraph::new(me);
1091        assert!(
1092            graph.simple_loopback_to_self(2, None).is_empty(),
1093            "no peers means no connected neighbors"
1094        );
1095    }
1096
1097    #[test]
1098    fn loopback_returns_empty_without_connected_neighbors() -> anyhow::Result<()> {
1099        // me → a → b, me → b exists but is NOT connected
1100        let me = pubkey_from(&SECRET_0);
1101        let a = pubkey_from(&SECRET_1);
1102        let b = pubkey_from(&SECRET_2);
1103
1104        let graph = ChannelGraph::new(me);
1105        graph.add_node(a);
1106        graph.add_node(b);
1107        graph.add_edge(&me, &a)?;
1108        graph.add_edge(&a, &b)?;
1109        graph.add_edge(&me, &b)?;
1110        // me→b is NOT marked connected, so b is not in connected_neighbors
1111        mark_edge_loopback_ready(&graph, &me, &a);
1112        mark_edge_with_capacity(&graph, &a, &b);
1113
1114        assert!(
1115            graph.simple_loopback_to_self(2, None).is_empty(),
1116            "b is not a connected neighbor, so no loopback destinations exist"
1117        );
1118
1119        Ok(())
1120    }
1121
1122    #[test]
1123    fn loopback_returns_empty_when_first_hop_lacks_capacity() -> anyhow::Result<()> {
1124        // me → a → b, me → b (connected)
1125        // me→a is connected but has NO intermediate capacity → edge-0 cost goes negative
1126        let me = pubkey_from(&SECRET_0);
1127        let a = pubkey_from(&SECRET_1);
1128        let b = pubkey_from(&SECRET_2);
1129
1130        let graph = ChannelGraph::new(me);
1131        graph.add_node(a);
1132        graph.add_node(b);
1133        graph.add_edge(&me, &a)?;
1134        graph.add_edge(&a, &b)?;
1135        graph.add_edge(&me, &b)?;
1136        // me→a: connected but no capacity (only Connected + Immediate)
1137        mark_edge_connected(&graph, &me, &a);
1138        // a→b: has capacity
1139        mark_edge_with_capacity(&graph, &a, &b);
1140        // me→b: connected (makes b a connected neighbor)
1141        mark_edge_connected(&graph, &me, &b);
1142
1143        assert!(
1144            graph.simple_loopback_to_self(2, None).is_empty(),
1145            "edge me→a lacks intermediate capacity, so EdgeValueFn::forward_without_self_loopback prunes it"
1146        );
1147
1148        Ok(())
1149    }
1150
1151    #[test]
1152    fn loopback_returns_empty_when_intermediate_edge_lacks_capacity() -> anyhow::Result<()> {
1153        // me → a → b, me → b (connected)
1154        // me→a passes cost-0, but a→b has NO capacity → cost goes negative at edge-1
1155        let me = pubkey_from(&SECRET_0);
1156        let a = pubkey_from(&SECRET_1);
1157        let b = pubkey_from(&SECRET_2);
1158
1159        let graph = ChannelGraph::new(me);
1160        graph.add_node(a);
1161        graph.add_node(b);
1162        graph.add_edge(&me, &a)?;
1163        graph.add_edge(&a, &b)?;
1164        graph.add_edge(&me, &b)?;
1165        // me→a: connected + capacity (passes edge-0)
1166        mark_edge_loopback_ready(&graph, &me, &a);
1167        // a→b: NO capacity — default edge weight
1168        // me→b: connected
1169        mark_edge_connected(&graph, &me, &b);
1170
1171        assert!(
1172            graph.simple_loopback_to_self(2, None).is_empty(),
1173            "edge a→b lacks capacity, so EdgeValueFn::forward_without_self_loopback prunes the path"
1174        );
1175
1176        Ok(())
1177    }
1178
1179    #[test]
1180    fn loopback_two_edge_triangle() -> anyhow::Result<()> {
1181        // Topology: me → a → b, me → b (connected)
1182        // Loopback path: me → a → b → me
1183        let me = pubkey_from(&SECRET_0);
1184        let a = pubkey_from(&SECRET_1);
1185        let b = pubkey_from(&SECRET_2);
1186
1187        let graph = ChannelGraph::new(me);
1188        graph.add_node(a);
1189        graph.add_node(b);
1190        graph.add_edge(&me, &a)?;
1191        graph.add_edge(&a, &b)?;
1192        graph.add_edge(&me, &b)?;
1193        // me→a: connected + capacity (edge-0 cost passes)
1194        mark_edge_loopback_ready(&graph, &me, &a);
1195        // a→b: capacity (edge-1 cost passes)
1196        mark_edge_with_capacity(&graph, &a, &b);
1197        // me→b: connected (makes b a connected neighbor destination)
1198        mark_edge_connected(&graph, &me, &b);
1199
1200        let routes = graph.simple_loopback_to_self(2, None);
1201        assert_eq!(routes.len(), 1, "should find exactly one 2-edge loopback");
1202
1203        let (path, _path_id) = &routes[0];
1204        // simple_loopback_to_self strips the leading `me` and keeps the closing `me`.
1205        // For a 2-edge internal path (me → a → b), the result is [a, b, me].
1206        assert_eq!(path.len(), 3, "loopback path: 2 internal nodes + closing me");
1207        assert_eq!(path.last(), Some(&me), "path should end with me (closing loopback)");
1208        assert_eq!(path[0], a, "first intermediate should be a");
1209        assert_eq!(path[1], b, "destination (connected neighbor) should be b");
1210
1211        Ok(())
1212    }
1213
1214    #[test]
1215    fn loopback_three_edge_chain() -> anyhow::Result<()> {
1216        // Topology: me → a → b → c, me → c (connected)
1217        // Loopback path: me → a → b → c → me
1218        let me = pubkey_from(&SECRET_0);
1219        let a = pubkey_from(&SECRET_1);
1220        let b = pubkey_from(&SECRET_2);
1221        let c = pubkey_from(&SECRET_3);
1222
1223        let graph = ChannelGraph::new(me);
1224        graph.add_node(a);
1225        graph.add_node(b);
1226        graph.add_node(c);
1227        graph.add_edge(&me, &a)?;
1228        graph.add_edge(&a, &b)?;
1229        graph.add_edge(&b, &c)?;
1230        graph.add_edge(&me, &c)?;
1231        mark_edge_loopback_ready(&graph, &me, &a);
1232        mark_edge_with_capacity(&graph, &a, &b);
1233        mark_edge_with_capacity(&graph, &b, &c);
1234        mark_edge_connected(&graph, &me, &c);
1235
1236        let routes = graph.simple_loopback_to_self(3, None);
1237        assert_eq!(routes.len(), 1, "should find exactly one 3-edge loopback");
1238
1239        let (path, _path_id) = &routes[0];
1240        // simple_loopback_to_self strips leading `me`, keeps closing `me`.
1241        // For a 3-edge internal path (me → a → b → c), result is [a, b, c, me].
1242        assert_eq!(path.len(), 4, "3-edge internal path + closing me = 4 nodes");
1243        assert_eq!(path.last(), Some(&me), "ends with me");
1244        assert_eq!(&path[0..3], &[a, b, c], "interior nodes");
1245
1246        Ok(())
1247    }
1248
1249    #[test]
1250    fn loopback_multiple_paths_through_diamond() -> anyhow::Result<()> {
1251        // Topology:
1252        //   me → a → c, me → b → c, me → c (connected)
1253        // Two 2-edge loopback paths: me → a → c → me, me → b → c → me
1254        let me = pubkey_from(&SECRET_0);
1255        let a = pubkey_from(&SECRET_1);
1256        let b = pubkey_from(&SECRET_2);
1257        let c = pubkey_from(&SECRET_3);
1258
1259        let graph = ChannelGraph::new(me);
1260        graph.add_node(a);
1261        graph.add_node(b);
1262        graph.add_node(c);
1263        graph.add_edge(&me, &a)?;
1264        graph.add_edge(&me, &b)?;
1265        graph.add_edge(&a, &c)?;
1266        graph.add_edge(&b, &c)?;
1267        graph.add_edge(&me, &c)?;
1268        mark_edge_loopback_ready(&graph, &me, &a);
1269        mark_edge_loopback_ready(&graph, &me, &b);
1270        mark_edge_with_capacity(&graph, &a, &c);
1271        mark_edge_with_capacity(&graph, &b, &c);
1272        mark_edge_connected(&graph, &me, &c);
1273
1274        let routes = graph.simple_loopback_to_self(2, None);
1275        assert_eq!(routes.len(), 2, "diamond should yield two 2-edge loopback paths");
1276
1277        for (path, _path_id) in &routes {
1278            // Leading `me` is stripped; path is [intermediate, c, me].
1279            assert_eq!(path.last(), Some(&me), "every path ends with me");
1280            assert_eq!(path[path.len() - 2], c, "penultimate node is c (connected neighbor)");
1281        }
1282
1283        // Verify distinct first intermediates (a and b)
1284        let intermediates: HashSet<_> = routes.iter().map(|(p, _)| p[0]).collect();
1285        assert!(intermediates.contains(&a), "should include path through a");
1286        assert!(intermediates.contains(&b), "should include path through b");
1287
1288        Ok(())
1289    }
1290
1291    #[test]
1292    fn loopback_to_multiple_connected_neighbors() -> anyhow::Result<()> {
1293        // Topology: me → a, me → b (both connected)
1294        // a and b are both connected neighbors of me.
1295        // With length=2: me → a → b → me and me → b → a → me
1296        let me = pubkey_from(&SECRET_0);
1297        let a = pubkey_from(&SECRET_1);
1298        let b = pubkey_from(&SECRET_2);
1299
1300        let graph = ChannelGraph::new(me);
1301        graph.add_node(a);
1302        graph.add_node(b);
1303        graph.add_edge(&me, &a)?;
1304        graph.add_edge(&me, &b)?;
1305        graph.add_edge(&a, &b)?;
1306        graph.add_edge(&b, &a)?;
1307        mark_edge_loopback_ready(&graph, &me, &a);
1308        mark_edge_loopback_ready(&graph, &me, &b);
1309        mark_edge_with_capacity(&graph, &a, &b);
1310        mark_edge_with_capacity(&graph, &b, &a);
1311
1312        let routes = graph.simple_loopback_to_self(2, None);
1313        assert_eq!(
1314            routes.len(),
1315            2,
1316            "should find loopback paths to both connected neighbors"
1317        );
1318
1319        // Leading `me` is stripped; path is [intermediate, connected_neighbor, me].
1320        for (path, _) in &routes {
1321            assert_eq!(path.last(), Some(&me));
1322        }
1323
1324        // Collect the connected-neighbor destinations (penultimate node)
1325        let destinations: HashSet<_> = routes.iter().map(|(p, _)| p[p.len() - 2]).collect();
1326        assert_eq!(destinations.len(), 2, "should reach both connected neighbors");
1327        assert!(destinations.contains(&a));
1328        assert!(destinations.contains(&b));
1329
1330        Ok(())
1331    }
1332
1333    #[test]
1334    fn loopback_disconnected_neighbor_is_excluded() -> anyhow::Result<()> {
1335        // me → a → b, me → a → c
1336        // me → b (connected), me → c (NOT connected)
1337        // length=2: only me → a → b → me should be found, not me → a → c → me
1338        let me = pubkey_from(&SECRET_0);
1339        let a = pubkey_from(&SECRET_1);
1340        let b = pubkey_from(&SECRET_2);
1341        let c = pubkey_from(&SECRET_3);
1342
1343        let graph = ChannelGraph::new(me);
1344        graph.add_node(a);
1345        graph.add_node(b);
1346        graph.add_node(c);
1347        graph.add_edge(&me, &a)?;
1348        graph.add_edge(&a, &b)?;
1349        graph.add_edge(&a, &c)?;
1350        graph.add_edge(&me, &b)?;
1351        graph.add_edge(&me, &c)?;
1352        mark_edge_loopback_ready(&graph, &me, &a);
1353        mark_edge_with_capacity(&graph, &a, &b);
1354        mark_edge_with_capacity(&graph, &a, &c);
1355        // me→b: connected (b IS a connected neighbor)
1356        mark_edge_connected(&graph, &me, &b);
1357        // me→c: NOT connected (c is NOT a connected neighbor)
1358
1359        let routes = graph.simple_loopback_to_self(2, None);
1360        assert_eq!(routes.len(), 1, "only the path to connected neighbor b should be found");
1361
1362        let (path, _) = &routes[0];
1363        assert_eq!(path[path.len() - 2], b, "destination should be b, not c");
1364
1365        Ok(())
1366    }
1367
1368    #[test]
1369    fn loopback_take_count_limits_results() -> anyhow::Result<()> {
1370        // Create 3 possible loopback paths, but take_count=1
1371        //   me → a → d, me → b → d, me → c → d, me → d (connected)
1372        let me = pubkey_from(&SECRET_0);
1373        let a = pubkey_from(&SECRET_1);
1374        let b = pubkey_from(&SECRET_2);
1375        let c = pubkey_from(&SECRET_3);
1376        let d = pubkey_from(&SECRET_4);
1377
1378        let graph = ChannelGraph::new(me);
1379        for node in [a, b, c, d] {
1380            graph.add_node(node);
1381        }
1382        graph.add_edge(&me, &a)?;
1383        graph.add_edge(&me, &b)?;
1384        graph.add_edge(&me, &c)?;
1385        graph.add_edge(&me, &d)?;
1386        graph.add_edge(&a, &d)?;
1387        graph.add_edge(&b, &d)?;
1388        graph.add_edge(&c, &d)?;
1389        mark_edge_loopback_ready(&graph, &me, &a);
1390        mark_edge_loopback_ready(&graph, &me, &b);
1391        mark_edge_loopback_ready(&graph, &me, &c);
1392        mark_edge_with_capacity(&graph, &a, &d);
1393        mark_edge_with_capacity(&graph, &b, &d);
1394        mark_edge_with_capacity(&graph, &c, &d);
1395        mark_edge_connected(&graph, &me, &d);
1396
1397        // Without limit: should find 3 paths
1398        let all_routes = graph.simple_loopback_to_self(2, None);
1399        assert_eq!(all_routes.len(), 3, "should find 3 loopback paths without limit");
1400
1401        // With take_count=1: should return exactly 1
1402        let limited = graph.simple_loopback_to_self(2, Some(1));
1403        assert_eq!(limited.len(), 1, "take_count=1 should limit to 1 result");
1404
1405        Ok(())
1406    }
1407
1408    #[test]
1409    fn loopback_path_ids_differ_for_distinct_routes() -> anyhow::Result<()> {
1410        // me → a → c, me → b → c, me → c (connected)
1411        let me = pubkey_from(&SECRET_0);
1412        let a = pubkey_from(&SECRET_1);
1413        let b = pubkey_from(&SECRET_2);
1414        let c = pubkey_from(&SECRET_3);
1415
1416        let graph = ChannelGraph::new(me);
1417        graph.add_node(a);
1418        graph.add_node(b);
1419        graph.add_node(c);
1420        graph.add_edge(&me, &a)?;
1421        graph.add_edge(&me, &b)?;
1422        graph.add_edge(&a, &c)?;
1423        graph.add_edge(&b, &c)?;
1424        graph.add_edge(&me, &c)?;
1425        mark_edge_loopback_ready(&graph, &me, &a);
1426        mark_edge_loopback_ready(&graph, &me, &b);
1427        mark_edge_with_capacity(&graph, &a, &c);
1428        mark_edge_with_capacity(&graph, &b, &c);
1429        mark_edge_connected(&graph, &me, &c);
1430
1431        let routes = graph.simple_loopback_to_self(2, None);
1432        assert_eq!(routes.len(), 2);
1433
1434        let path_ids: Vec<PathId> = routes.iter().map(|(_, pid)| *pid).collect();
1435        assert_ne!(
1436            path_ids[0], path_ids[1],
1437            "distinct loopback paths should have different PathIds"
1438        );
1439
1440        Ok(())
1441    }
1442
1443    #[test]
1444    fn loopback_mismatched_length_returns_empty() -> anyhow::Result<()> {
1445        // Topology only supports 2-edge internal path, but we request 3
1446        // me → a → b, me → b (connected)
1447        let me = pubkey_from(&SECRET_0);
1448        let a = pubkey_from(&SECRET_1);
1449        let b = pubkey_from(&SECRET_2);
1450
1451        let graph = ChannelGraph::new(me);
1452        graph.add_node(a);
1453        graph.add_node(b);
1454        graph.add_edge(&me, &a)?;
1455        graph.add_edge(&a, &b)?;
1456        graph.add_edge(&me, &b)?;
1457        mark_edge_loopback_ready(&graph, &me, &a);
1458        mark_edge_with_capacity(&graph, &a, &b);
1459        mark_edge_connected(&graph, &me, &b);
1460
1461        // length=2 works
1462        assert_eq!(graph.simple_loopback_to_self(2, None).len(), 1);
1463        // length=3 has no 3-edge path to any connected neighbor
1464        assert!(
1465            graph.simple_loopback_to_self(3, None).is_empty(),
1466            "no 3-edge internal path exists"
1467        );
1468
1469        Ok(())
1470    }
1471}