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