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