1use std::sync::Arc;
2
3use futures::{SinkExt, StreamExt, pin_mut};
4use hopr_api::{
5 HoprBalance, Multiaddr, OffchainPublicKey, PeerId,
6 chain::{ChainKeyOperations, WinningProbability},
7 graph::{EdgeBalanceUpdate, MeasurableEdge, NetworkGraphUpdate},
8 types::{
9 chain::chain_events::ChainEvent,
10 internal::prelude::ChannelStatus,
11 primitive::{
12 prelude::{Address, UnitaryFloatOps},
13 traits::KeyIdMapping,
14 },
15 },
16};
17use hopr_transport::{NeighborTelemetry, PathTelemetry, SurbStore};
18use parking_lot::RwLock;
19use tracing::Instrument;
20
21#[cfg(all(feature = "telemetry", not(test)))]
22lazy_static::lazy_static! {
23 static ref METRIC_CHANNELS_COUNT: hopr_api::types::telemetry::MultiGauge = hopr_api::types::telemetry::MultiGauge::new(
24 "hopr_channels_count",
25 "Number of open channels of the node per direction",
26 &["direction"]
27 ).unwrap();
28}
29
30#[allow(clippy::too_many_arguments)]
42pub(super) async fn process_chain_events<C, G, S>(
43 chain_reader: C,
44 graph_updater: G,
45 surb_store: S,
46 events: impl futures::Stream<Item = ChainEvent> + Send + 'static,
47 own_chain_addr: Address,
48 own_packet_key: OffchainPublicKey,
49 ticket_price: Arc<RwLock<HoprBalance>>,
50 win_probability: Arc<RwLock<WinningProbability>>,
51 mut peer_discovery_tx: Option<hopr_utils::network_types::crossfire_sink::CrossfireSink<(PeerId, Vec<Multiaddr>)>>,
52) where
53 C: ChainKeyOperations + Clone + Send + Sync + 'static,
54 G: NetworkGraphUpdate + Send + Sync + 'static,
55 S: SurbStore + Send + Sync + 'static,
56{
57 pin_mut!(events);
58
59 push_ticket_face_value(&graph_updater, &ticket_price, &win_probability);
64
65 #[cfg(all(feature = "telemetry", not(test)))]
71 let (mut incoming_open, mut outgoing_open) = (std::collections::HashSet::new(), std::collections::HashSet::new());
72
73 while let Some(chain_event) = events.next().await {
74 tracing::debug!(event = %chain_event, "processing chain event");
75 match chain_event {
76 ChainEvent::Announcement(account) => {
77 tracing::debug!(
78 account = %account.public_key,
79 "recording graph node for announced account"
80 );
81 graph_updater.record_node(account.public_key);
82 if let Some(ref mut tx) = peer_discovery_tx {
83 let peer_id: PeerId = account.public_key.into();
84 let multiaddrs = account.get_multiaddrs();
85 let span = tracing::info_span!(
86 "peer_announcement",
87 peer = %peer_id,
88 multiaddresses = ?multiaddrs,
89 );
90 if let Err(e) = tx.send((peer_id, multiaddrs.to_vec())).instrument(span.clone()).await {
91 tracing::error!(parent: &span, %e, "peer-discovery channel closed; announcement dropped");
92 }
93 }
94 }
95 ChainEvent::ChannelOpened(channel)
96 | ChainEvent::ChannelClosureInitiated(channel)
97 | ChainEvent::ChannelClosed(channel)
98 | ChainEvent::ChannelBalanceIncreased(channel, _)
99 | ChainEvent::ChannelBalanceDecreased(channel, _) => {
100 let src_addr = channel.source;
101 let dst_addr = channel.destination;
102
103 #[cfg(all(feature = "telemetry", not(test)))]
104 {
105 let channel_id = *channel.get_id();
106 let is_open = matches!(channel.status, ChannelStatus::Open);
107 if src_addr == own_chain_addr {
108 if is_open {
109 outgoing_open.insert(channel_id);
110 } else {
111 outgoing_open.remove(&channel_id);
112 }
113 METRIC_CHANNELS_COUNT.set(&["outgoing"], outgoing_open.len() as f64);
114 } else if dst_addr == own_chain_addr {
115 if is_open {
116 incoming_open.insert(channel_id);
117 } else {
118 incoming_open.remove(&channel_id);
119 }
120 METRIC_CHANNELS_COUNT.set(&["incoming"], incoming_open.len() as f64);
121 }
122 }
123
124 let reader = chain_reader.clone();
125 let keys = hopr_utils::runtime::prelude::spawn_blocking(move || {
126 let resolve = |addr: Address| {
127 if addr == own_chain_addr {
128 return Ok(Some(own_packet_key));
129 }
130 reader.chain_key_to_packet_key(&addr).map_err(anyhow::Error::from)
131 };
132 resolve(src_addr).and_then(|src| resolve(dst_addr).map(|dst| src.zip(dst)))
133 })
134 .await
135 .map_err(anyhow::Error::from)
136 .flatten();
137
138 match keys {
139 Ok(Some((from, to))) => {
140 let balance = match channel.status {
145 ChannelStatus::Closed | ChannelStatus::PendingToClose(_) => None,
146 _ => Some(channel.balance.amount()),
147 };
148
149 tracing::debug!(
150 %channel, ?balance,
151 "recording graph edge for channel balance"
152 );
153 graph_updater.record_edge(MeasurableEdge::<NeighborTelemetry, PathTelemetry>::Balance(
154 Box::new(EdgeBalanceUpdate {
155 balance,
156 src: from,
157 dest: to,
158 }),
159 ));
160
161 if src_addr == own_chain_addr {
164 match chain_reader.key_id_mapper_ref().map_key_to_id(&to) {
165 Some(relayer) => match channel.status {
169 ChannelStatus::Closed | ChannelStatus::PendingToClose(_) => {
170 surb_store.invalidate_relayer(&relayer)
171 }
172 ChannelStatus::Open => surb_store.revalidate_relayer(&relayer),
173 },
174 None => tracing::warn!(
175 %channel,
176 "no key id for own channel counterparty; SURB validity not updated"
177 ),
178 }
179 }
180 }
181 Ok(None) => {
182 tracing::error!(
183 %channel,
184 "could not find packet keys for channel endpoints"
185 );
186 }
187 Err(error) => {
188 tracing::error!(
189 %error, %channel,
190 "failed to convert chain keys to packet keys"
191 );
192 }
193 }
194 }
195 ChainEvent::WinningProbabilityIncreased(prob) | ChainEvent::WinningProbabilityDecreased(prob) => {
196 tracing::debug!(%prob, "recording winning probability change");
197 *win_probability.write() = prob;
198 push_ticket_face_value(&graph_updater, &ticket_price, &win_probability);
199 }
200 ChainEvent::TicketPriceChanged(price) => {
201 tracing::debug!(%price, "recording ticket price change");
202 *ticket_price.write() = price;
203 push_ticket_face_value(&graph_updater, &ticket_price, &win_probability);
204 }
205 _ => {}
206 }
207 }
208}
209
210fn push_ticket_face_value<G>(
216 graph: &G,
217 ticket_price: &Arc<RwLock<HoprBalance>>,
218 win_probability: &Arc<RwLock<WinningProbability>>,
219) where
220 G: NetworkGraphUpdate,
221{
222 let price = *ticket_price.read();
225 let probability = win_probability.read().as_f64();
226
227 match price.div_f64(probability) {
232 Ok(face_value) => {
233 let face_value = face_value.amount();
234 tracing::debug!(%face_value, "recording ticket face value change");
235 graph.set_ticket_face_value(face_value);
236 }
237 Err(error) => tracing::error!(%error, "failed to derive the ticket face value; leaving the previous one"),
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use std::{
244 collections::HashMap,
245 sync::{Arc, Mutex},
246 time::SystemTime,
247 };
248
249 use anyhow::Context as _;
250 use hopr_api::{
251 HoprBalance, OffchainPublicKey,
252 chain::{ChainKeyOperations, HoprKeyIdent, KeyIdMapping, WinningProbability},
253 graph::{EdgeBalanceUpdate, MeasurableEdge, MeasurablePath, MeasurablePeer, NetworkGraphUpdate},
254 types::{
255 chain::chain_events::ChainEvent,
256 crypto::prelude::{ChainKeypair, Keypair, OffchainKeypair},
257 internal::prelude::{AccountEntry, AccountType, ChannelEntry, ChannelStatus},
258 primitive::prelude::Address,
259 },
260 };
261 use hopr_transport::MemorySurbStore;
262 use parking_lot::RwLock;
263
264 use super::process_chain_events;
265
266 #[derive(Debug, Clone, thiserror::Error)]
271 #[error("stub: {0}")]
272 struct StubError(String);
273
274 #[derive(Debug, Clone, Default)]
276 struct StubMapper(HashMap<OffchainPublicKey, HoprKeyIdent>);
277
278 impl KeyIdMapping<HoprKeyIdent, OffchainPublicKey> for StubMapper {
279 fn map_key_to_id(&self, key: &OffchainPublicKey) -> Option<HoprKeyIdent> {
280 self.0.get(key).copied()
281 }
282
283 fn map_id_to_public(&self, id: &HoprKeyIdent) -> Option<OffchainPublicKey> {
284 self.0.iter().find_map(|(k, v)| (v == id).then_some(*k))
285 }
286 }
287
288 #[derive(Debug, Clone)]
289 struct StubChainKeys {
290 keys: HashMap<Address, OffchainPublicKey>,
291 mapper: StubMapper,
292 }
293
294 impl StubChainKeys {
295 fn new(pairs: impl IntoIterator<Item = (Address, OffchainPublicKey)>) -> Self {
296 Self {
297 keys: pairs.into_iter().collect(),
298 mapper: StubMapper::default(),
299 }
300 }
301
302 fn with_key_ids(mut self, ids: impl IntoIterator<Item = (OffchainPublicKey, HoprKeyIdent)>) -> Self {
303 self.mapper = StubMapper(ids.into_iter().collect());
304 self
305 }
306 }
307
308 impl ChainKeyOperations for StubChainKeys {
309 type Error = StubError;
310 type Mapper = StubMapper;
311
312 fn chain_key_to_packet_key(&self, chain: &Address) -> Result<Option<OffchainPublicKey>, Self::Error> {
313 Ok(self.keys.get(chain).copied())
314 }
315
316 fn packet_key_to_chain_key(&self, packet: &OffchainPublicKey) -> Result<Option<Address>, Self::Error> {
317 Ok(self.keys.iter().find_map(|(a, k)| (k == packet).then_some(*a)))
318 }
319
320 fn key_id_mapper_ref(&self) -> &Self::Mapper {
321 &self.mapper
322 }
323 }
324
325 #[derive(Debug, Clone)]
326 enum GraphCall {
327 Node(OffchainPublicKey),
328 Edge(Box<EdgeBalanceUpdate>),
329 FaceValue(hopr_api::graph::traits::Balance),
330 }
331
332 #[derive(Debug, Clone, Default)]
333 struct RecordingGraph {
334 calls: Arc<Mutex<Vec<GraphCall>>>,
335 }
336
337 impl RecordingGraph {
338 fn recorded(&self) -> Vec<GraphCall> {
339 self.calls.lock().unwrap().clone()
340 }
341
342 fn edges(&self) -> Vec<EdgeBalanceUpdate> {
343 self.recorded()
344 .into_iter()
345 .filter_map(|c| if let GraphCall::Edge(e) = c { Some(*e) } else { None })
346 .collect()
347 }
348
349 fn face_values(&self) -> Vec<hopr_api::graph::traits::Balance> {
350 self.recorded()
351 .into_iter()
352 .filter_map(|c| if let GraphCall::FaceValue(v) = c { Some(v) } else { None })
353 .collect()
354 }
355
356 fn nodes(&self) -> Vec<OffchainPublicKey> {
357 self.recorded()
358 .into_iter()
359 .filter_map(|c| if let GraphCall::Node(n) = c { Some(n) } else { None })
360 .collect()
361 }
362 }
363
364 impl NetworkGraphUpdate for RecordingGraph {
365 fn set_ticket_face_value(&self, ticket_face_value: hopr_api::graph::traits::Balance) {
366 self.calls.lock().unwrap().push(GraphCall::FaceValue(ticket_face_value));
367 }
368
369 fn record_edge<N, P>(&self, update: MeasurableEdge<N, P>)
370 where
371 N: MeasurablePeer + Clone + Send + Sync + 'static,
372 P: MeasurablePath + Clone + Send + Sync + 'static,
373 {
374 if let MeasurableEdge::Balance(balance) = update {
375 self.calls.lock().unwrap().push(GraphCall::Edge(balance));
376 }
377 }
378
379 fn record_node<N>(&self, update: N)
380 where
381 N: hopr_api::graph::MeasurableNode + Clone + Send + Sync + 'static,
382 {
383 self.calls.lock().unwrap().push(GraphCall::Node(update.into()));
384 }
385 }
386
387 fn make_keypairs() -> (OffchainKeypair, ChainKeypair) {
392 (OffchainKeypair::random(), ChainKeypair::random())
393 }
394
395 fn channel(src: Address, dst: Address, balance: u128, status: ChannelStatus) -> ChannelEntry {
396 ChannelEntry::builder()
397 .source(src)
398 .destination(dst)
399 .amount(balance)
400 .status(status)
401 .build()
402 .expect("valid channel")
403 }
404
405 fn account(key: OffchainPublicKey, addr: Address) -> AccountEntry {
406 use hopr_api::types::primitive::prelude::KeyIdent;
407 AccountEntry {
408 public_key: key,
409 chain_addr: addr,
410 entry_type: AccountType::NotAnnounced,
411 safe_address: None,
412 key_id: KeyIdent::default(),
413 }
414 }
415
416 async fn run(
417 events: Vec<ChainEvent>,
418 chain: StubChainKeys,
419 graph: RecordingGraph,
420 own_chain_addr: Address,
421 own_packet_key: OffchainPublicKey,
422 ticket_price: HoprBalance,
423 win_probability: WinningProbability,
424 ) {
425 let _ = run_with_peer_discovery(
426 events,
427 chain,
428 graph,
429 own_chain_addr,
430 own_packet_key,
431 ticket_price,
432 win_probability,
433 )
434 .await;
435 }
436
437 async fn run_with_peer_discovery(
438 events: Vec<ChainEvent>,
439 chain: StubChainKeys,
440 graph: RecordingGraph,
441 own_chain_addr: Address,
442 own_packet_key: OffchainPublicKey,
443 ticket_price: HoprBalance,
444 win_probability: WinningProbability,
445 ) -> Vec<(hopr_api::PeerId, Vec<hopr_api::Multiaddr>)> {
446 use futures::StreamExt;
447 let (tx, rx) = hopr_utils::network_types::crossfire_sink::bounded_sink_channel(64);
448 process_chain_events(
449 chain,
450 graph,
451 MemorySurbStore::default(),
452 futures::stream::iter(events),
453 own_chain_addr,
454 own_packet_key,
455 Arc::new(RwLock::new(ticket_price)),
456 Arc::new(RwLock::new(win_probability)),
457 Some(tx),
458 )
459 .await;
460 rx.collect().await
461 }
462
463 async fn run_with_surb_store(
465 events: Vec<ChainEvent>,
466 chain: StubChainKeys,
467 surb_store: MemorySurbStore,
468 own_chain_addr: Address,
469 own_packet_key: OffchainPublicKey,
470 ) {
471 process_chain_events(
472 chain,
473 RecordingGraph::default(),
474 surb_store,
475 futures::stream::iter(events),
476 own_chain_addr,
477 own_packet_key,
478 Arc::new(RwLock::new(HoprBalance::from(1u32))),
479 Arc::new(RwLock::new(WinningProbability::ALWAYS)),
480 None,
481 )
482 .await;
483 }
484
485 #[tokio::test]
490 async fn announcement_records_node() {
491 let (offchain, chain) = make_keypairs();
492 let addr = chain.public().to_address();
493 let graph = RecordingGraph::default();
494
495 run(
496 vec![ChainEvent::Announcement(account(*offchain.public(), addr))],
497 StubChainKeys::new([]),
498 graph.clone(),
499 addr,
500 *offchain.public(),
501 HoprBalance::from(10u64),
502 WinningProbability::ALWAYS,
503 )
504 .await;
505
506 assert_eq!(graph.nodes(), vec![*offchain.public()]);
507 assert!(graph.edges().is_empty());
508 }
509
510 #[tokio::test]
511 async fn announcement_should_forward_to_peer_discovery_when_tx_is_set() -> anyhow::Result<()> {
512 use std::str::FromStr;
513
514 use hopr_api::types::internal::prelude::AccountType;
515
516 let (offchain, chain) = make_keypairs();
517 let addr = chain.public().to_address();
518 let multiaddr = hopr_api::Multiaddr::from_str("/ip4/1.2.3.4/tcp/9000").context("parse multiaddr")?;
519 let entry = AccountEntry {
520 entry_type: AccountType::Announced(vec![multiaddr.clone()]),
521 ..account(*offchain.public(), addr)
522 };
523 let graph = RecordingGraph::default();
524
525 let received = run_with_peer_discovery(
526 vec![ChainEvent::Announcement(entry)],
527 StubChainKeys::new([]),
528 graph.clone(),
529 addr,
530 *offchain.public(),
531 HoprBalance::from(10u64),
532 WinningProbability::ALWAYS,
533 )
534 .await;
535
536 assert_eq!(received.len(), 1, "expected exactly one peer-discovery event");
537 let (peer_id, addrs) = &received[0];
538 assert_eq!(
539 *peer_id,
540 hopr_api::PeerId::from(*offchain.public()),
541 "peer id must match the announced account's public key"
542 );
543 assert_eq!(addrs, &vec![multiaddr], "multiaddrs must be forwarded unchanged");
544 assert_eq!(
545 graph.nodes(),
546 vec![*offchain.public()],
547 "graph must also record the node"
548 );
549 Ok(())
550 }
551
552 #[tokio::test]
553 async fn channel_opened_records_capacity() {
554 let (src_offchain, src_chain) = make_keypairs();
555 let (dst_offchain, dst_chain) = make_keypairs();
556 let src_addr = src_chain.public().to_address();
557 let dst_addr = dst_chain.public().to_address();
558
559 let graph = RecordingGraph::default();
560 let stub = StubChainKeys::new([(src_addr, *src_offchain.public()), (dst_addr, *dst_offchain.public())]);
561
562 run(
564 vec![ChainEvent::ChannelOpened(channel(
565 src_addr,
566 dst_addr,
567 100,
568 ChannelStatus::Open,
569 ))],
570 stub,
571 graph.clone(),
572 src_addr,
573 *src_offchain.public(),
574 HoprBalance::from(10u64),
575 WinningProbability::ALWAYS,
576 )
577 .await;
578
579 let edges = graph.edges();
580 assert_eq!(edges.len(), 1);
581 assert_eq!(edges[0].balance, Some(hopr_api::graph::traits::Balance::from(100u64)));
582 assert_eq!(edges[0].src, *src_offchain.public());
583 assert_eq!(edges[0].dest, *dst_offchain.public());
584 }
585
586 #[tokio::test]
587 async fn channel_balance_decreased_records_updated_capacity() {
588 let (src_offchain, src_chain) = make_keypairs();
589 let (dst_offchain, dst_chain) = make_keypairs();
590 let src_addr = src_chain.public().to_address();
591 let dst_addr = dst_chain.public().to_address();
592
593 let graph = RecordingGraph::default();
594 let stub = StubChainKeys::new([(src_addr, *src_offchain.public()), (dst_addr, *dst_offchain.public())]);
595
596 run(
598 vec![ChainEvent::ChannelBalanceDecreased(
599 channel(src_addr, dst_addr, 50, ChannelStatus::Open),
600 HoprBalance::from(50u64),
601 )],
602 stub,
603 graph.clone(),
604 src_addr,
605 *src_offchain.public(),
606 HoprBalance::from(10u64),
607 WinningProbability::ALWAYS,
608 )
609 .await;
610
611 let edges = graph.edges();
612 assert_eq!(edges.len(), 1);
613 assert_eq!(edges[0].balance, Some(hopr_api::graph::traits::Balance::from(50u64)));
614 }
615
616 #[tokio::test]
617 async fn channel_closed_records_capacity_none() {
618 let (src_offchain, src_chain) = make_keypairs();
619 let (dst_offchain, dst_chain) = make_keypairs();
620 let src_addr = src_chain.public().to_address();
621 let dst_addr = dst_chain.public().to_address();
622
623 let graph = RecordingGraph::default();
624 let stub = StubChainKeys::new([(src_addr, *src_offchain.public()), (dst_addr, *dst_offchain.public())]);
625
626 run(
627 vec![ChainEvent::ChannelClosed(channel(
628 src_addr,
629 dst_addr,
630 0,
631 ChannelStatus::Closed,
632 ))],
633 stub,
634 graph.clone(),
635 src_addr,
636 *src_offchain.public(),
637 HoprBalance::from(10u64),
638 WinningProbability::ALWAYS,
639 )
640 .await;
641
642 let edges = graph.edges();
643 assert_eq!(edges.len(), 1);
644 assert_eq!(edges[0].balance, None);
645 }
646
647 #[tokio::test]
651 async fn channel_closure_initiated_records_capacity_none() {
652 let (src_offchain, src_chain) = make_keypairs();
653 let (dst_offchain, dst_chain) = make_keypairs();
654 let src_addr = src_chain.public().to_address();
655 let dst_addr = dst_chain.public().to_address();
656
657 let graph = RecordingGraph::default();
658 let stub = StubChainKeys::new([(src_addr, *src_offchain.public()), (dst_addr, *dst_offchain.public())]);
659
660 run(
661 vec![ChainEvent::ChannelClosureInitiated(channel(
662 src_addr,
663 dst_addr,
664 100,
665 ChannelStatus::PendingToClose(SystemTime::now()),
666 ))],
667 stub,
668 graph.clone(),
669 src_addr,
670 *src_offchain.public(),
671 HoprBalance::from(10u64),
672 WinningProbability::ALWAYS,
673 )
674 .await;
675
676 let edges = graph.edges();
677 assert_eq!(edges.len(), 1, "closure-initiated must emit a graph update");
678 assert_eq!(
679 edges[0].balance, None,
680 "closure-initiated must clear the balance so routing stops using this edge"
681 );
682 }
683
684 #[tokio::test]
685 async fn ticket_price_change_pushes_a_new_face_value() {
686 let (src_offchain, src_chain) = make_keypairs();
687 let (dst_offchain, dst_chain) = make_keypairs();
688 let src_addr = src_chain.public().to_address();
689 let dst_addr = dst_chain.public().to_address();
690
691 let graph = RecordingGraph::default();
692 let stub = StubChainKeys::new([(src_addr, *src_offchain.public()), (dst_addr, *dst_offchain.public())]);
693
694 run(
696 vec![
697 ChainEvent::TicketPriceChanged(HoprBalance::from(20u64)),
698 ChainEvent::ChannelOpened(channel(src_addr, dst_addr, 200, ChannelStatus::Open)),
699 ],
700 stub,
701 graph.clone(),
702 src_addr,
703 *src_offchain.public(),
704 HoprBalance::from(10u64),
705 WinningProbability::ALWAYS,
706 )
707 .await;
708
709 assert_eq!(
710 graph.face_values(),
711 vec![
712 hopr_api::graph::traits::Balance::from(10u64),
713 hopr_api::graph::traits::Balance::from(20u64),
714 ],
715 "the seeded face value, then the one recomputed from the price change"
716 );
717
718 let edges = graph.edges();
719 assert_eq!(edges.len(), 1);
720 assert_eq!(
721 edges[0].balance,
722 Some(hopr_api::graph::traits::Balance::from(200u64)),
723 "the emitted balance must not depend on the price"
724 );
725 }
726
727 #[tokio::test]
728 async fn win_probability_change_pushes_a_new_face_value() -> anyhow::Result<()> {
729 let (src_offchain, src_chain) = make_keypairs();
730 let (dst_offchain, dst_chain) = make_keypairs();
731 let src_addr = src_chain.public().to_address();
732 let dst_addr = dst_chain.public().to_address();
733
734 let graph = RecordingGraph::default();
735 let stub = StubChainKeys::new([(src_addr, *src_offchain.public()), (dst_addr, *dst_offchain.public())]);
736
737 let new_prob = WinningProbability::try_from_f64(0.5).context("0.5 is a valid winning probability")?;
739 run(
740 vec![
741 ChainEvent::WinningProbabilityDecreased(new_prob),
742 ChainEvent::ChannelOpened(channel(src_addr, dst_addr, 100, ChannelStatus::Open)),
743 ],
744 stub,
745 graph.clone(),
746 src_addr,
747 *src_offchain.public(),
748 HoprBalance::from(10u64),
749 WinningProbability::ALWAYS,
750 )
751 .await;
752
753 assert_eq!(
754 graph.face_values(),
755 vec![
756 hopr_api::graph::traits::Balance::from(10u64),
757 hopr_api::graph::traits::Balance::from(20u64),
758 ],
759 "the seeded face value, then the one recomputed from the probability change"
760 );
761
762 let edges = graph.edges();
763 assert_eq!(edges.len(), 1);
764 assert_eq!(
765 edges[0].balance,
766 Some(hopr_api::graph::traits::Balance::from(100u64)),
767 "the emitted balance must not depend on the winning probability"
768 );
769 Ok(())
770 }
771
772 #[tokio::test]
777 async fn a_face_value_is_seeded_before_any_pricing_event() {
778 let (src_offchain, src_chain) = make_keypairs();
779 let (dst_offchain, dst_chain) = make_keypairs();
780 let src_addr = src_chain.public().to_address();
781 let dst_addr = dst_chain.public().to_address();
782
783 let graph = RecordingGraph::default();
784 let stub = StubChainKeys::new([(src_addr, *src_offchain.public()), (dst_addr, *dst_offchain.public())]);
785
786 run(
788 vec![ChainEvent::ChannelOpened(channel(
789 src_addr,
790 dst_addr,
791 200,
792 ChannelStatus::Open,
793 ))],
794 stub,
795 graph.clone(),
796 src_addr,
797 *src_offchain.public(),
798 HoprBalance::from(10u64),
799 WinningProbability::ALWAYS,
800 )
801 .await;
802
803 assert_eq!(
804 graph.face_values(),
805 vec![hopr_api::graph::traits::Balance::from(10u64)],
806 "the graph must know the price before it records a balance it will be compared against"
807 );
808 }
809
810 #[tokio::test]
811 async fn unknown_chain_key_produces_no_graph_update() {
812 let (src_offchain, src_chain) = make_keypairs();
813 let (_, dst_chain) = make_keypairs();
814 let src_addr = src_chain.public().to_address();
815 let dst_addr = dst_chain.public().to_address();
816
817 let graph = RecordingGraph::default();
818 let stub = StubChainKeys::new([(src_addr, *src_offchain.public())]);
820
821 run(
822 vec![ChainEvent::ChannelOpened(channel(
823 src_addr,
824 dst_addr,
825 100,
826 ChannelStatus::Open,
827 ))],
828 stub,
829 graph.clone(),
830 src_addr,
831 *src_offchain.public(),
832 HoprBalance::from(10u64),
833 WinningProbability::ALWAYS,
834 )
835 .await;
836
837 assert!(graph.edges().is_empty(), "unknown key must produce no graph update");
838 }
839
840 #[tokio::test]
841 async fn self_address_resolved_via_own_packet_key() {
842 let (own_offchain, own_chain) = make_keypairs();
843 let (dst_offchain, dst_chain) = make_keypairs();
844 let own_chain_addr = own_chain.public().to_address();
845 let dst_addr = dst_chain.public().to_address();
846
847 let graph = RecordingGraph::default();
848 let stub = StubChainKeys::new([(dst_addr, *dst_offchain.public())]);
850
851 run(
852 vec![ChainEvent::ChannelOpened(channel(
853 own_chain_addr,
854 dst_addr,
855 100,
856 ChannelStatus::Open,
857 ))],
858 stub,
859 graph.clone(),
860 own_chain_addr,
861 *own_offchain.public(),
862 HoprBalance::from(10u64),
863 WinningProbability::ALWAYS,
864 )
865 .await;
866
867 let edges = graph.edges();
868 assert_eq!(edges.len(), 1);
869 assert_eq!(edges[0].src, *own_offchain.public());
870 assert_eq!(edges[0].dest, *dst_offchain.public());
871 }
872
873 #[tokio::test]
874 async fn announcement_should_handle_disconnected_peer_discovery_tx_gracefully() {
875 let (offchain, chain) = make_keypairs();
876 let addr = chain.public().to_address();
877 let (tx, rx) = hopr_utils::network_types::crossfire_sink::bounded_sink_channel(1);
878 drop(rx); process_chain_events(
881 StubChainKeys::new([]),
882 RecordingGraph::default(),
883 MemorySurbStore::default(),
884 futures::stream::iter(vec![ChainEvent::Announcement(account(*offchain.public(), addr))]),
885 addr,
886 *offchain.public(),
887 Arc::new(RwLock::new(HoprBalance::from(10u64))),
888 Arc::new(RwLock::new(WinningProbability::ALWAYS)),
889 Some(tx),
890 )
891 .await;
892 }
893
894 async fn run_own_channel_events(
900 statuses: impl IntoIterator<Item = ChannelStatus>,
901 ) -> (MemorySurbStore, HoprKeyIdent) {
902 let (me_offchain, me_chain) = make_keypairs();
903 let (peer_offchain, peer_chain) = make_keypairs();
904 let (me_addr, peer_addr) = (me_chain.public().to_address(), peer_chain.public().to_address());
905 let peer_id = HoprKeyIdent::from(1u32);
906
907 let stub = StubChainKeys::new([(me_addr, *me_offchain.public()), (peer_addr, *peer_offchain.public())])
908 .with_key_ids([(*peer_offchain.public(), peer_id)]);
909
910 let surb_store = MemorySurbStore::default();
911 let events = statuses
912 .into_iter()
913 .map(|status| ChainEvent::ChannelOpened(channel(me_addr, peer_addr, 100, status)))
914 .collect();
915
916 run_with_surb_store(events, stub, surb_store.clone(), me_addr, *me_offchain.public()).await;
917
918 (surb_store, peer_id)
919 }
920
921 #[tokio::test]
922 async fn closing_an_own_outgoing_channel_should_invalidate_that_relayer() {
923 let (store, peer_id) =
924 run_own_channel_events([ChannelStatus::PendingToClose(std::time::SystemTime::now())]).await;
925 assert!(store.is_relayer_invalidated(&peer_id), "PendingToClose must invalidate");
926
927 let (store, peer_id) = run_own_channel_events([ChannelStatus::Closed]).await;
928 assert!(store.is_relayer_invalidated(&peer_id), "Closed must invalidate");
929 }
930
931 #[tokio::test]
932 async fn reopening_an_own_outgoing_channel_should_revalidate_that_relayer() {
933 let (store, peer_id) = run_own_channel_events([ChannelStatus::Closed, ChannelStatus::Open]).await;
934 assert!(!store.is_relayer_invalidated(&peer_id), "re-opening must revalidate");
935 }
936
937 #[tokio::test]
938 async fn a_channel_that_is_not_ours_should_not_invalidate_anything() {
939 let (me_offchain, me_chain) = make_keypairs();
941 let (a_offchain, a_chain) = make_keypairs();
942 let (b_offchain, b_chain) = make_keypairs();
943 let (me_addr, a_addr, b_addr) = (
944 me_chain.public().to_address(),
945 a_chain.public().to_address(),
946 b_chain.public().to_address(),
947 );
948 let b_id = HoprKeyIdent::from(1u32);
949
950 let stub = StubChainKeys::new([
951 (me_addr, *me_offchain.public()),
952 (a_addr, *a_offchain.public()),
953 (b_addr, *b_offchain.public()),
954 ])
955 .with_key_ids([(*b_offchain.public(), b_id)]);
956
957 let store = MemorySurbStore::default();
958 run_with_surb_store(
959 vec![ChainEvent::ChannelClosed(channel(
960 a_addr,
961 b_addr,
962 100,
963 ChannelStatus::Closed,
964 ))],
965 stub,
966 store.clone(),
967 me_addr,
968 *me_offchain.public(),
969 )
970 .await;
971
972 assert!(
973 !store.is_relayer_invalidated(&b_id),
974 "someone else's channel must not affect our SURBs"
975 );
976 }
977}