1pub mod channel_graph;
5pub mod errors;
6pub mod selectors;
8
9use std::{
10 fmt::{Display, Formatter},
11 hash::Hash,
12 ops::Deref,
13};
14
15use hopr_crypto_types::prelude::*;
16use hopr_internal_types::prelude::*;
17use hopr_primitive_types::prelude::*;
18
19use crate::{
20 channel_graph::ChannelGraph,
21 errors::{
22 PathError,
23 PathError::{ChannelNotOpened, InvalidPeer, LoopsNotAllowed, MissingChannel, PathNotValid},
24 },
25};
26
27#[allow(clippy::large_enum_variant)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, strum::EnumTryAs, strum::EnumIs)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31pub enum PathAddress {
32 Chain(Address),
33 Transport(OffchainPublicKey),
34}
35
36impl From<Address> for PathAddress {
37 fn from(value: Address) -> Self {
38 PathAddress::Chain(value)
39 }
40}
41
42impl From<OffchainPublicKey> for PathAddress {
43 fn from(value: OffchainPublicKey) -> Self {
44 PathAddress::Transport(value)
45 }
46}
47
48impl Display for PathAddress {
49 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
50 match self {
51 PathAddress::Chain(addr) => write!(f, "{}", addr.to_hex()),
52 PathAddress::Transport(key) => write!(f, "{}", key.to_hex()),
53 }
54 }
55}
56
57pub trait Path<N>: Clone + Eq + PartialEq + Deref<Target = [N]> + IntoIterator<Item = N>
59where
60 N: Into<PathAddress> + Copy,
61{
62 fn hops(&self) -> &[N] {
65 self.deref()
66 }
67
68 fn num_hops(&self) -> usize {
70 self.hops().len()
71 }
72
73 fn invert(self) -> Option<Self>;
75
76 fn contains_cycle(&self) -> bool {
78 std::collections::HashSet::<_, std::hash::RandomState>::from_iter(self.iter().copied().map(|h| h.into())).len()
79 != self.num_hops()
80 }
81}
82
83pub trait NonEmptyPath<N: Into<PathAddress> + Copy>: Path<N> {
85 fn last_hop(&self) -> &N {
87 self.hops().last().expect("non-empty path must have at least one hop")
88 }
89}
90
91impl<T: Into<PathAddress> + Copy + PartialEq + Eq> Path<T> for Vec<T> {
92 fn invert(self) -> Option<Self> {
93 Some(self.into_iter().rev().collect())
94 }
95}
96
97pub type ChannelPath = Vec<Address>;
98
99#[derive(Clone, Debug, PartialEq, Eq)]
101pub struct TransportPath(Vec<OffchainPublicKey>);
102
103impl TransportPath {
104 pub fn new<T, I>(path: I) -> errors::Result<Self>
108 where
109 T: Into<OffchainPublicKey>,
110 I: IntoIterator<Item = T>,
111 {
112 let hops = path.into_iter().map(|t| t.into()).collect::<Vec<_>>();
113 if !hops.is_empty() {
114 Ok(Self(hops))
115 } else {
116 Err(PathNotValid)
117 }
118 }
119
120 pub fn direct(destination: OffchainPublicKey) -> Self {
122 Self(vec![destination])
123 }
124}
125
126impl Deref for TransportPath {
127 type Target = [OffchainPublicKey];
128
129 fn deref(&self) -> &Self::Target {
130 &self.0
131 }
132}
133
134impl IntoIterator for TransportPath {
135 type IntoIter = std::vec::IntoIter<Self::Item>;
136 type Item = OffchainPublicKey;
137
138 fn into_iter(self) -> Self::IntoIter {
139 self.0.into_iter()
140 }
141}
142
143impl Path<OffchainPublicKey> for TransportPath {
144 fn invert(self) -> Option<Self> {
145 Some(Self(self.0.into_iter().rev().collect()))
146 }
147}
148
149impl NonEmptyPath<OffchainPublicKey> for TransportPath {}
150
151#[derive(Clone, Debug, PartialEq, Eq)]
158pub struct ChainPath(Vec<Address>);
159
160impl ChainPath {
161 pub fn new<T, I>(path: I) -> errors::Result<Self>
165 where
166 T: Into<Address>,
167 I: IntoIterator<Item = T>,
168 {
169 let hops = path.into_iter().map(|t| t.into()).collect::<Vec<_>>();
170 if !hops.is_empty() {
171 Ok(Self(hops))
172 } else {
173 Err(PathNotValid)
174 }
175 }
176
177 pub fn from_channel_path(mut path: ChannelPath, destination: Address) -> Self {
179 path.push(destination);
180 Self(path)
181 }
182
183 pub fn direct(destination: Address) -> Self {
185 Self(vec![destination])
186 }
187
188 pub fn into_channel_path(mut self) -> ChannelPath {
190 self.0.pop();
191 self.0
192 }
193}
194
195impl Deref for ChainPath {
196 type Target = [Address];
197
198 fn deref(&self) -> &Self::Target {
199 &self.0
200 }
201}
202
203impl Display for ChainPath {
204 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
205 write!(
206 f,
207 "chain path [{}]",
208 self.0.iter().map(|p| p.to_hex()).collect::<Vec<String>>().join(", ")
209 )
210 }
211}
212
213impl From<ChainPath> for ChannelPath {
214 fn from(value: ChainPath) -> Self {
215 let len = value.0.len();
216 value.0.into_iter().take(len - 1).collect()
217 }
218}
219
220impl IntoIterator for ChainPath {
221 type IntoIter = std::vec::IntoIter<Self::Item>;
222 type Item = Address;
223
224 fn into_iter(self) -> Self::IntoIter {
225 self.0.into_iter()
226 }
227}
228
229impl Path<Address> for ChainPath {
230 fn invert(self) -> Option<Self> {
231 Some(Self(self.0.into_iter().rev().collect()))
232 }
233}
234
235impl NonEmptyPath<Address> for ChainPath {}
236
237#[cfg_attr(test, mockall::automock)]
239#[async_trait::async_trait]
240pub trait PathAddressResolver {
241 async fn resolve_transport_address(&self, address: &Address) -> Result<Option<OffchainPublicKey>, PathError>;
243 async fn resolve_chain_address(&self, key: &OffchainPublicKey) -> Result<Option<Address>, PathError>;
245}
246
247#[derive(Clone, Debug, PartialEq, Eq)]
251pub struct ValidatedPath(TransportPath, ChainPath);
252
253impl ValidatedPath {
254 pub fn direct(dst_key: OffchainPublicKey, dst_address: Address) -> Self {
256 Self(TransportPath(vec![dst_key]), ChainPath(vec![dst_address]))
257 }
258
259 pub async fn new<N, P, O, R>(origin: O, path: P, cg: &ChannelGraph, resolver: &R) -> errors::Result<ValidatedPath>
265 where
266 N: Into<PathAddress> + Copy,
267 P: NonEmptyPath<N>,
268 O: Into<PathAddress>,
269 R: PathAddressResolver,
270 {
271 let mut ticket_issuer = match origin.into() {
272 PathAddress::Chain(addr) => addr,
273 PathAddress::Transport(key) => resolver
274 .resolve_chain_address(&key)
275 .await?
276 .ok_or(InvalidPeer(key.to_string()))?,
277 };
278
279 let mut keys = Vec::with_capacity(path.num_hops());
280 let mut addrs = Vec::with_capacity(path.num_hops());
281
282 let num_hops = path.num_hops();
283 for (i, hop) in path.into_iter().enumerate() {
284 let ticket_receiver = match &hop.into() {
287 PathAddress::Chain(addr) => {
288 let key = resolver
289 .resolve_transport_address(addr)
290 .await?
291 .ok_or(InvalidPeer(addr.to_string()))?;
292 keys.push(key);
293 addrs.push(*addr);
294 *addr
295 }
296 PathAddress::Transport(key) => {
297 let addr = resolver
298 .resolve_chain_address(key)
299 .await?
300 .ok_or(InvalidPeer(key.to_string()))?;
301 addrs.push(addr);
302 keys.push(*key);
303 addr
304 }
305 };
306
307 if ticket_issuer == ticket_receiver {
309 return Err(LoopsNotAllowed(ticket_receiver.to_hex()));
310 }
311
312 if i < num_hops - 1 {
314 let channel = cg
315 .get_channel(&ticket_issuer, &ticket_receiver)
316 .ok_or(MissingChannel(ticket_issuer.to_hex(), ticket_receiver.to_hex()))?;
317
318 if channel.status != ChannelStatus::Open {
319 return Err(ChannelNotOpened(ticket_issuer.to_hex(), ticket_receiver.to_hex()));
320 }
321 }
322
323 ticket_issuer = ticket_receiver;
324 }
325
326 debug_assert_eq!(keys.len(), addrs.len());
327
328 Ok(ValidatedPath(TransportPath(keys), ChainPath(addrs)))
329 }
330
331 pub fn chain_path(&self) -> &ChainPath {
333 &self.1
334 }
335
336 pub fn transport_path(&self) -> &TransportPath {
338 &self.0
339 }
340}
341
342impl Deref for ValidatedPath {
343 type Target = [OffchainPublicKey];
344
345 fn deref(&self) -> &Self::Target {
346 &self.0
347 }
348}
349
350impl IntoIterator for ValidatedPath {
351 type IntoIter = std::vec::IntoIter<Self::Item>;
352 type Item = OffchainPublicKey;
353
354 fn into_iter(self) -> Self::IntoIter {
355 self.0.into_iter()
356 }
357}
358
359impl Path<OffchainPublicKey> for ValidatedPath {
360 fn invert(self) -> Option<Self> {
364 None
365 }
366}
367
368impl Display for ValidatedPath {
369 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
370 write!(
371 f,
372 "validated path [{}]",
373 self.1.0.iter().map(|p| p.to_hex()).collect::<Vec<String>>().join(", ")
374 )
375 }
376}
377
378impl NonEmptyPath<OffchainPublicKey> for ValidatedPath {}
379
380#[cfg(test)]
381pub(crate) mod tests {
382 use std::{
383 iter,
384 ops::Add,
385 str::FromStr,
386 time::{Duration, SystemTime},
387 };
388
389 use anyhow::{Context, ensure};
390 use async_trait::async_trait;
391 use hex_literal::hex;
392 use hopr_internal_types::channels::ChannelEntry;
393 use parameterized::parameterized;
394
395 use super::*;
396
397 lazy_static::lazy_static! {
398 pub static ref PATH_ADDRS: bimap::BiMap<OffchainPublicKey, Address> = bimap::BiMap::from_iter([
399 (OffchainPublicKey::from_privkey(&hex!("e0bf93e9c916104da00b1850adc4608bd7e9087bbd3f805451f4556aa6b3fd6e")).unwrap(), Address::from_str("0x0000c178cf70d966be0a798e666ce2782c7b2288").unwrap()),
400 (OffchainPublicKey::from_privkey(&hex!("cfc66f718ec66fb822391775d749d7a0d66b690927673634816b63339bc12a3c")).unwrap(), Address::from_str("0x1000d5786d9e6799b3297da1ad55605b91e2c882").unwrap()),
401 (OffchainPublicKey::from_privkey(&hex!("203ca4d3c5f98dd2066bb204b5930c10b15c095585c224c826b4e11f08bfa85d")).unwrap(), Address::from_str("0x200060ddced1e33c9647a71f4fc2cf4ed33e4a9d").unwrap()),
402 (OffchainPublicKey::from_privkey(&hex!("fc71590e473b3e64e498e8a7f03ed19d1d7ee5f438c5d41300e8bb228b6b5d3c")).unwrap(), Address::from_str("0x30004105095c8c10f804109b4d1199a9ac40ed46").unwrap()),
403 (OffchainPublicKey::from_privkey(&hex!("4ab03f6f75f845ca1bf8b7104804ea5bda18bda29d1ec5fc5d4267feca5fb8e1")).unwrap(), Address::from_str("0x4000a288c38fa8a0f4b79127747257af4a03a623").unwrap()),
404 (OffchainPublicKey::from_privkey(&hex!("a1859043a6ae231259ad0bccac9a62377cd2b0700ba2248fcfa52ae1461f7087")).unwrap(), Address::from_str("0x50002f462ec709cf181bbe44a7e952487bd4591d").unwrap()),
405 ]);
406 pub static ref ADDRESSES: Vec<Address> = sorted_peers().iter().map(|p| p.1).collect();
407 }
408
409 pub fn sorted_peers() -> Vec<(OffchainPublicKey, Address)> {
410 let mut peers = PATH_ADDRS.iter().map(|(pk, a)| (*pk, *a)).collect::<Vec<_>>();
411 peers.sort_by(|a, b| a.1.to_string().cmp(&b.1.to_string()));
412 peers
413 }
414
415 #[async_trait]
416 impl PathAddressResolver for bimap::BiMap<OffchainPublicKey, Address> {
417 async fn resolve_transport_address(&self, address: &Address) -> Result<Option<OffchainPublicKey>, PathError> {
418 Ok(self.get_by_right(address).copied())
419 }
420
421 async fn resolve_chain_address(&self, key: &OffchainPublicKey) -> Result<Option<Address>, PathError> {
422 Ok(self.get_by_left(key).copied())
423 }
424 }
425
426 pub fn dummy_channel(src: Address, dst: Address, status: ChannelStatus) -> ChannelEntry {
427 ChannelEntry::new(src, dst, 1.into(), 1u32.into(), status, 1u32.into())
428 }
429
430 fn create_graph_and_resolver_entries(me: Address) -> (ChannelGraph, Vec<(OffchainPublicKey, Address)>) {
431 let addrs = sorted_peers();
432
433 let ts = SystemTime::now().add(Duration::from_secs(10));
434
435 let mut cg = ChannelGraph::new(me, Default::default());
437 cg.update_channel(dummy_channel(addrs[0].1, addrs[1].1, ChannelStatus::Open));
438 cg.update_channel(dummy_channel(addrs[1].1, addrs[2].1, ChannelStatus::Open));
439 cg.update_channel(dummy_channel(addrs[2].1, addrs[3].1, ChannelStatus::Open));
440 cg.update_channel(dummy_channel(addrs[3].1, addrs[4].1, ChannelStatus::Open));
441 cg.update_channel(dummy_channel(addrs[3].1, addrs[1].1, ChannelStatus::Open));
442 cg.update_channel(dummy_channel(addrs[4].1, addrs[0].1, ChannelStatus::PendingToClose(ts)));
443
444 (cg, addrs)
445 }
446
447 #[test]
448 fn chain_path_zero_hop_should_fail() -> anyhow::Result<()> {
449 ensure!(ChainPath::new::<Address, _>([]).is_err(), "must fail for zero hop");
450 Ok(())
451 }
452
453 #[test]
454 fn transport_path_zero_hop_should_fail() -> anyhow::Result<()> {
455 ensure!(
456 TransportPath::new::<OffchainPublicKey, _>([]).is_err(),
457 "must fail for zero hop"
458 );
459 Ok(())
460 }
461
462 #[parameterized(hops = { 1, 2, 3 })]
463 #[parameterized_macro(tokio::test)]
464 async fn validated_path_multi_hop_validation(hops: usize) -> anyhow::Result<()> {
465 let (cg, peers) = create_graph_and_resolver_entries(ADDRESSES[0]);
466
467 let chain_path = ChainPath::new(peers.iter().skip(1).take(hops + 1).map(|(_, a)| *a))?;
469
470 assert_eq!(hops + 1, chain_path.num_hops(), "must be a {hops} hop path");
471 ensure!(!chain_path.contains_cycle(), "must not be cyclic");
472
473 let validated = ValidatedPath::new(ADDRESSES[0], chain_path.clone(), &cg, PATH_ADDRS.deref())
474 .await
475 .context(format!("must be valid {hops} hop path"))?;
476
477 assert_eq!(
478 chain_path.num_hops(),
479 validated.num_hops(),
480 "validated path must have the same length"
481 );
482 assert_eq!(
483 validated.chain_path(),
484 &chain_path,
485 "validated path must have the same chain path"
486 );
487
488 assert_eq!(
489 peers.into_iter().skip(1).take(hops + 1).collect::<Vec<_>>(),
490 validated
491 .transport_path()
492 .iter()
493 .copied()
494 .zip(validated.chain_path().iter().copied())
495 .collect::<Vec<_>>(),
496 "validated path must have the same transport path"
497 );
498
499 Ok(())
500 }
501
502 #[parameterized(hops = { 1, 2, 3 })]
503 #[parameterized_macro(tokio::test)]
504 async fn validated_path_revalidation_should_be_identity(hops: usize) -> anyhow::Result<()> {
505 let (cg, peers) = create_graph_and_resolver_entries(ADDRESSES[0]);
506
507 let chain_path = ChainPath::new(peers.iter().skip(1).take(hops + 1).map(|(_, a)| *a))?;
509
510 let validated_1 = ValidatedPath::new(ADDRESSES[0], chain_path.clone(), &cg, PATH_ADDRS.deref())
511 .await
512 .context(format!("must be valid {hops} hop path"))?;
513
514 let validated_2 = ValidatedPath::new(ADDRESSES[0], validated_1.clone(), &cg, PATH_ADDRS.deref())
515 .await
516 .context(format!("must be valid {hops} hop path"))?;
517
518 assert_eq!(validated_1, validated_2, "revalidation must be identity");
519
520 Ok(())
521 }
522
523 #[parameterized(hops = { 2, 3 })]
524 #[parameterized_macro(tokio::test)]
525 async fn validated_path_must_allow_cyclic(hops: usize) -> anyhow::Result<()> {
526 let (cg, peers) = create_graph_and_resolver_entries(ADDRESSES[0]);
527
528 let chain_path = ChainPath::new(
530 peers
531 .iter()
532 .skip(1)
533 .take(hops)
534 .map(|(_, a)| *a)
535 .chain(iter::once(peers[1].1)),
536 )?;
537
538 assert_eq!(hops + 1, chain_path.num_hops(), "must be a {hops} hop path");
539 assert!(chain_path.contains_cycle(), "must be cyclic");
540
541 let validated = ValidatedPath::new(ADDRESSES[0], chain_path.clone(), &cg, PATH_ADDRS.deref())
542 .await
543 .context(format!("must be valid {hops} hop path"))?;
544
545 assert_eq!(
546 chain_path.num_hops(),
547 validated.num_hops(),
548 "validated path must have the same length"
549 );
550 assert_eq!(
551 validated.chain_path(),
552 &chain_path,
553 "validated path must have the same chain path"
554 );
555
556 assert_eq!(
557 peers
558 .iter()
559 .copied()
560 .skip(1)
561 .take(hops)
562 .chain(iter::once(peers[1]))
563 .collect::<Vec<_>>(),
564 validated
565 .transport_path()
566 .iter()
567 .copied()
568 .zip(validated.chain_path().iter().copied())
569 .collect::<Vec<_>>(),
570 "validated path must have the same transport path"
571 );
572
573 Ok(())
574 }
575
576 #[tokio::test]
577 async fn validated_path_should_allow_zero_hop_with_non_existing_channel() -> anyhow::Result<()> {
578 let (cg, peers) = create_graph_and_resolver_entries(ADDRESSES[0]);
579
580 let chain_path = ChainPath::new([peers[3].1])?;
582
583 let validated = ValidatedPath::new(ADDRESSES[0], chain_path.clone(), &cg, PATH_ADDRS.deref())
584 .await
585 .context("must be valid path")?;
586
587 assert_eq!(&chain_path, validated.chain_path(), "path must be the same");
588
589 Ok(())
590 }
591
592 #[tokio::test]
593 async fn validated_path_should_allow_zero_hop_with_non_open_channel() -> anyhow::Result<()> {
594 let (cg, peers) = create_graph_and_resolver_entries(ADDRESSES[0]);
595
596 let chain_path = ChainPath::new([peers[0].1])?;
598
599 let validated = ValidatedPath::new(ADDRESSES[4], chain_path.clone(), &cg, PATH_ADDRS.deref())
600 .await
601 .context("must be valid path")?;
602
603 assert_eq!(&chain_path, validated.chain_path(), "path must be the same");
604
605 Ok(())
606 }
607
608 #[tokio::test]
609 async fn validated_path_should_allow_non_existing_channel_for_last_hop() -> anyhow::Result<()> {
610 let (cg, peers) = create_graph_and_resolver_entries(ADDRESSES[0]);
611
612 let chain_path = ChainPath::new([peers[1].1, peers[3].1])?;
614
615 let validated = ValidatedPath::new(ADDRESSES[0], chain_path.clone(), &cg, PATH_ADDRS.deref())
616 .await
617 .context("must be valid path")?;
618
619 assert_eq!(&chain_path, validated.chain_path(), "path must be the same");
620
621 Ok(())
622 }
623
624 #[tokio::test]
625 async fn validated_path_should_allow_non_open_channel_for_the_last_hop() -> anyhow::Result<()> {
626 let (cg, peers) = create_graph_and_resolver_entries(ADDRESSES[0]);
627
628 let chain_path = ChainPath::new([peers[4].1, peers[0].1])?;
630
631 let validated = ValidatedPath::new(ADDRESSES[3], chain_path.clone(), &cg, PATH_ADDRS.deref())
632 .await
633 .context("must be valid path")?;
634
635 assert_eq!(&chain_path, validated.chain_path(), "path must be the same");
636
637 Ok(())
638 }
639
640 #[tokio::test]
641 async fn validated_path_should_fail_for_non_open_channel_not_in_the_last_hop() -> anyhow::Result<()> {
642 let (cg, peers) = create_graph_and_resolver_entries(ADDRESSES[0]);
643
644 let chain_path = ChainPath::new([peers[0].1, peers[1].1])?;
646
647 ensure!(
648 ValidatedPath::new(ADDRESSES[4], chain_path, &cg, PATH_ADDRS.deref())
649 .await
650 .is_err(),
651 "path must not be constructible"
652 );
653
654 let chain_path = ChainPath::new([peers[4].1, peers[0].1, peers[1].1])?;
656
657 ensure!(
658 ValidatedPath::new(ADDRESSES[3], chain_path, &cg, PATH_ADDRS.deref())
659 .await
660 .is_err(),
661 "path must not be constructible"
662 );
663
664 let chain_path = ChainPath::new([peers[3].1, peers[4].1, peers[0].1, peers[1].1])?;
666
667 ensure!(
668 ValidatedPath::new(ADDRESSES[2], chain_path, &cg, PATH_ADDRS.deref())
669 .await
670 .is_err(),
671 "path must not be constructible"
672 );
673
674 Ok(())
675 }
676
677 #[tokio::test]
678 async fn validated_path_should_fail_for_non_existing_channel_not_in_the_last_hop() -> anyhow::Result<()> {
679 let (cg, peers) = create_graph_and_resolver_entries(ADDRESSES[0]);
680
681 let chain_path = ChainPath::new([peers[3].1, peers[4].1])?;
683
684 ensure!(
685 ValidatedPath::new(ADDRESSES[0], chain_path, &cg, PATH_ADDRS.deref())
686 .await
687 .is_err(),
688 "path must not be constructible"
689 );
690
691 let chain_path = ChainPath::new([peers[1].1, peers[3].1, peers[0].1])?;
693
694 ensure!(
695 ValidatedPath::new(ADDRESSES[0], chain_path, &cg, PATH_ADDRS.deref())
696 .await
697 .is_err(),
698 "path must not be constructible"
699 );
700
701 let chain_path = ChainPath::new([peers[1].1, peers[2].1, peers[2].1, peers[0].1])?;
703
704 ensure!(
705 ValidatedPath::new(ADDRESSES[0], chain_path, &cg, PATH_ADDRS.deref())
706 .await
707 .is_err(),
708 "path must not be constructible"
709 );
710
711 Ok(())
712 }
713
714 #[tokio::test]
715 async fn validated_path_should_not_allow_simple_loops() -> anyhow::Result<()> {
716 let (cg, peers) = create_graph_and_resolver_entries(ADDRESSES[0]);
717
718 let chain_path = ChainPath::new([peers[1].1, peers[1].1, peers[2].1])?;
720
721 assert!(chain_path.contains_cycle(), "path must contain a cycle");
722
723 ensure!(
724 ValidatedPath::new(ADDRESSES[0], chain_path, &cg, PATH_ADDRS.deref())
725 .await
726 .is_err(),
727 "path must not be constructible"
728 );
729
730 Ok(())
731 }
732
733 #[tokio::test]
734 async fn validated_path_should_allow_long_cycles() -> anyhow::Result<()> {
735 let (cg, peers) = create_graph_and_resolver_entries(ADDRESSES[0]);
736
737 let chain_path = ChainPath::new([peers[1].1, peers[2].1, peers[3].1, peers[1].1, peers[2].1])?;
739
740 assert!(chain_path.contains_cycle(), "path must contain a cycle");
741
742 let validated = ValidatedPath::new(ADDRESSES[0], chain_path.clone(), &cg, PATH_ADDRS.deref())
743 .await
744 .context("must be valid path")?;
745
746 assert_eq!(&chain_path, validated.chain_path(), "path must be the same");
747
748 Ok(())
749 }
750}