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 | ChainEvent::TicketRedeemed(channel, _) => {
105 let src_addr = channel.source;
106 let dst_addr = channel.destination;
107
108 #[cfg(all(feature = "telemetry", not(test)))]
109 {
110 let channel_id = *channel.get_id();
111 let is_open = matches!(channel.status, ChannelStatus::Open);
112 if src_addr == own_chain_addr {
113 if is_open {
114 outgoing_open.insert(channel_id);
115 } else {
116 outgoing_open.remove(&channel_id);
117 }
118 METRIC_CHANNELS_COUNT.set(&["outgoing"], outgoing_open.len() as f64);
119 } else if dst_addr == own_chain_addr {
120 if is_open {
121 incoming_open.insert(channel_id);
122 } else {
123 incoming_open.remove(&channel_id);
124 }
125 METRIC_CHANNELS_COUNT.set(&["incoming"], incoming_open.len() as f64);
126 }
127 }
128
129 let reader = chain_reader.clone();
130 let keys = hopr_utils::runtime::prelude::spawn_blocking(move || {
131 let resolve = |addr: Address| {
132 if addr == own_chain_addr {
133 return Ok(Some(own_packet_key));
134 }
135 reader.chain_key_to_packet_key(&addr).map_err(anyhow::Error::from)
136 };
137 resolve(src_addr).and_then(|src| resolve(dst_addr).map(|dst| src.zip(dst)))
138 })
139 .await
140 .map_err(anyhow::Error::from)
141 .flatten();
142
143 match keys {
144 Ok(Some((from, to))) => {
145 let balance = match channel.status {
150 ChannelStatus::Closed | ChannelStatus::PendingToClose(_) => None,
151 _ => Some(channel.balance.amount()),
152 };
153
154 tracing::debug!(
155 %channel, ?balance,
156 "recording graph edge for channel balance"
157 );
158 graph_updater.record_edge(MeasurableEdge::<NeighborTelemetry, PathTelemetry>::Balance(
159 Box::new(EdgeBalanceUpdate {
160 balance,
161 src: from,
162 dest: to,
163 }),
164 ));
165
166 if src_addr == own_chain_addr {
169 match chain_reader.key_id_mapper_ref().map_key_to_id(&to) {
170 Some(relayer) => match channel.status {
174 ChannelStatus::Closed | ChannelStatus::PendingToClose(_) => {
175 surb_store.invalidate_relayer(&relayer)
176 }
177 ChannelStatus::Open => surb_store.revalidate_relayer(&relayer),
178 },
179 None => tracing::warn!(
180 %channel,
181 "no key id for own channel counterparty; SURB validity not updated"
182 ),
183 }
184 }
185 }
186 Ok(None) => {
187 tracing::error!(
188 %channel,
189 "could not find packet keys for channel endpoints"
190 );
191 }
192 Err(error) => {
193 tracing::error!(
194 %error, %channel,
195 "failed to convert chain keys to packet keys"
196 );
197 }
198 }
199 }
200 ChainEvent::WinningProbabilityIncreased(prob) | ChainEvent::WinningProbabilityDecreased(prob) => {
201 tracing::debug!(%prob, "recording winning probability change");
202 *win_probability.write() = prob;
203 push_ticket_face_value(&graph_updater, &ticket_price, &win_probability);
204 }
205 ChainEvent::TicketPriceChanged(price) => {
206 tracing::debug!(%price, "recording ticket price change");
207 *ticket_price.write() = price;
208 push_ticket_face_value(&graph_updater, &ticket_price, &win_probability);
209 }
210 ChainEvent::ServiceRegistered(_)
211 | ChainEvent::ServiceUpdated(_)
212 | ChainEvent::ServiceDeregistered(..)
213 | ChainEvent::ServiceTypeRegistered(..)
214 | ChainEvent::ServiceTypeOwnerChanged(..)
215 | ChainEvent::ServiceTypeRequirementChanged(..)
216 | ChainEvent::ServiceTypeRegistrationBurnChanged(..)
217 | ChainEvent::ServiceTypeUpdateBurnChanged(..)
218 | ChainEvent::ServiceTypeRegistrationFeeChanged(_)
219 | ChainEvent::ServiceRegistryPointerChanged(_) => {}
220 }
221 }
222}
223
224fn push_ticket_face_value<G>(
230 graph: &G,
231 ticket_price: &Arc<RwLock<HoprBalance>>,
232 win_probability: &Arc<RwLock<WinningProbability>>,
233) where
234 G: NetworkGraphUpdate,
235{
236 let price = *ticket_price.read();
239 let probability = win_probability.read().as_f64();
240
241 match price.div_f64(probability) {
246 Ok(face_value) => {
247 let face_value = face_value.amount();
248 tracing::debug!(%face_value, "recording ticket face value change");
249 graph.set_ticket_face_value(face_value);
250 }
251 Err(error) => tracing::error!(%error, "failed to derive the ticket face value; leaving the previous one"),
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use std::{
258 collections::HashMap,
259 sync::{Arc, Mutex},
260 time::SystemTime,
261 };
262
263 use anyhow::Context as _;
264 use hopr_api::{
265 HoprBalance, OffchainPublicKey,
266 chain::{ChainKeyOperations, HoprKeyIdent, KeyIdMapping, WinningProbability},
267 graph::{EdgeBalanceUpdate, MeasurableEdge, MeasurablePath, MeasurablePeer, NetworkGraphUpdate},
268 types::{
269 chain::chain_events::ChainEvent,
270 crypto::prelude::{ChainKeypair, Keypair, OffchainKeypair},
271 internal::prelude::{
272 AccountEntry, AccountType, ChannelEntry, ChannelStatus, ServiceEntry, ServiceMetadata, ServiceType,
273 },
274 primitive::prelude::Address,
275 },
276 };
277 use hopr_transport::MemorySurbStore;
278 use parking_lot::RwLock;
279 use rstest::rstest;
280
281 use super::process_chain_events;
282
283 #[derive(Debug, Clone, thiserror::Error)]
288 #[error("stub: {0}")]
289 struct StubError(String);
290
291 #[derive(Debug, Clone, Default)]
293 struct StubMapper(HashMap<OffchainPublicKey, HoprKeyIdent>);
294
295 impl KeyIdMapping<HoprKeyIdent, OffchainPublicKey> for StubMapper {
296 fn map_key_to_id(&self, key: &OffchainPublicKey) -> Option<HoprKeyIdent> {
297 self.0.get(key).copied()
298 }
299
300 fn map_id_to_public(&self, id: &HoprKeyIdent) -> Option<OffchainPublicKey> {
301 self.0.iter().find_map(|(k, v)| (v == id).then_some(*k))
302 }
303 }
304
305 #[derive(Debug, Clone)]
306 struct StubChainKeys {
307 keys: HashMap<Address, OffchainPublicKey>,
308 mapper: StubMapper,
309 }
310
311 impl StubChainKeys {
312 fn new(pairs: impl IntoIterator<Item = (Address, OffchainPublicKey)>) -> Self {
313 Self {
314 keys: pairs.into_iter().collect(),
315 mapper: StubMapper::default(),
316 }
317 }
318
319 fn with_key_ids(mut self, ids: impl IntoIterator<Item = (OffchainPublicKey, HoprKeyIdent)>) -> Self {
320 self.mapper = StubMapper(ids.into_iter().collect());
321 self
322 }
323 }
324
325 impl ChainKeyOperations for StubChainKeys {
326 type Error = StubError;
327 type Mapper = StubMapper;
328
329 fn chain_key_to_packet_key(&self, chain: &Address) -> Result<Option<OffchainPublicKey>, Self::Error> {
330 Ok(self.keys.get(chain).copied())
331 }
332
333 fn packet_key_to_chain_key(&self, packet: &OffchainPublicKey) -> Result<Option<Address>, Self::Error> {
334 Ok(self.keys.iter().find_map(|(a, k)| (k == packet).then_some(*a)))
335 }
336
337 fn key_id_mapper_ref(&self) -> &Self::Mapper {
338 &self.mapper
339 }
340 }
341
342 #[derive(Debug, Clone)]
343 enum GraphCall {
344 Node(OffchainPublicKey),
345 Edge(Box<EdgeBalanceUpdate>),
346 FaceValue(hopr_api::graph::traits::Balance),
347 }
348
349 #[derive(Debug, Clone, Default)]
350 struct RecordingGraph {
351 calls: Arc<Mutex<Vec<GraphCall>>>,
352 }
353
354 impl RecordingGraph {
355 fn recorded(&self) -> Vec<GraphCall> {
356 self.calls.lock().unwrap().clone()
357 }
358
359 fn edges(&self) -> Vec<EdgeBalanceUpdate> {
360 self.recorded()
361 .into_iter()
362 .filter_map(|c| if let GraphCall::Edge(e) = c { Some(*e) } else { None })
363 .collect()
364 }
365
366 fn face_values(&self) -> Vec<hopr_api::graph::traits::Balance> {
367 self.recorded()
368 .into_iter()
369 .filter_map(|c| if let GraphCall::FaceValue(v) = c { Some(v) } else { None })
370 .collect()
371 }
372
373 fn nodes(&self) -> Vec<OffchainPublicKey> {
374 self.recorded()
375 .into_iter()
376 .filter_map(|c| if let GraphCall::Node(n) = c { Some(n) } else { None })
377 .collect()
378 }
379 }
380
381 impl NetworkGraphUpdate for RecordingGraph {
382 fn set_ticket_face_value(&self, ticket_face_value: hopr_api::graph::traits::Balance) {
383 self.calls.lock().unwrap().push(GraphCall::FaceValue(ticket_face_value));
384 }
385
386 fn record_edge<N, P>(&self, update: MeasurableEdge<N, P>)
387 where
388 N: MeasurablePeer + Clone + Send + Sync + 'static,
389 P: MeasurablePath + Clone + Send + Sync + 'static,
390 {
391 if let MeasurableEdge::Balance(balance) = update {
392 self.calls.lock().unwrap().push(GraphCall::Edge(balance));
393 }
394 }
395
396 fn record_node<N>(&self, update: N)
397 where
398 N: hopr_api::graph::MeasurableNode + Clone + Send + Sync + 'static,
399 {
400 self.calls.lock().unwrap().push(GraphCall::Node(update.into()));
401 }
402 }
403
404 fn make_keypairs() -> (OffchainKeypair, ChainKeypair) {
409 (OffchainKeypair::random(), ChainKeypair::random())
410 }
411
412 fn channel(src: Address, dst: Address, balance: u128, status: ChannelStatus) -> ChannelEntry {
413 ChannelEntry::builder()
414 .source(src)
415 .destination(dst)
416 .amount(balance)
417 .status(status)
418 .build()
419 .expect("valid channel")
420 }
421
422 fn account(key: OffchainPublicKey, addr: Address) -> AccountEntry {
423 use hopr_api::types::primitive::prelude::KeyIdent;
424 AccountEntry {
425 public_key: key,
426 chain_addr: addr,
427 entry_type: AccountType::NotAnnounced,
428 safe_address: None,
429 key_id: KeyIdent::default(),
430 }
431 }
432
433 async fn run(
434 events: Vec<ChainEvent>,
435 chain: StubChainKeys,
436 graph: RecordingGraph,
437 own_chain_addr: Address,
438 own_packet_key: OffchainPublicKey,
439 ticket_price: HoprBalance,
440 win_probability: WinningProbability,
441 ) {
442 let _ = run_with_peer_discovery(
443 events,
444 chain,
445 graph,
446 own_chain_addr,
447 own_packet_key,
448 ticket_price,
449 win_probability,
450 )
451 .await;
452 }
453
454 async fn run_with_peer_discovery(
455 events: Vec<ChainEvent>,
456 chain: StubChainKeys,
457 graph: RecordingGraph,
458 own_chain_addr: Address,
459 own_packet_key: OffchainPublicKey,
460 ticket_price: HoprBalance,
461 win_probability: WinningProbability,
462 ) -> Vec<(hopr_api::PeerId, Vec<hopr_api::Multiaddr>)> {
463 use futures::StreamExt;
464 let (tx, rx) = hopr_utils::network_types::crossfire_sink::bounded_sink_channel(64);
465 process_chain_events(
466 chain,
467 graph,
468 MemorySurbStore::default(),
469 futures::stream::iter(events),
470 own_chain_addr,
471 own_packet_key,
472 Arc::new(RwLock::new(ticket_price)),
473 Arc::new(RwLock::new(win_probability)),
474 Some(tx),
475 )
476 .await;
477 rx.collect().await
478 }
479
480 async fn run_with_surb_store(
482 events: Vec<ChainEvent>,
483 chain: StubChainKeys,
484 surb_store: MemorySurbStore,
485 own_chain_addr: Address,
486 own_packet_key: OffchainPublicKey,
487 ) {
488 process_chain_events(
489 chain,
490 RecordingGraph::default(),
491 surb_store,
492 futures::stream::iter(events),
493 own_chain_addr,
494 own_packet_key,
495 Arc::new(RwLock::new(HoprBalance::from(1u32))),
496 Arc::new(RwLock::new(WinningProbability::ALWAYS)),
497 None,
498 )
499 .await;
500 }
501
502 #[tokio::test]
507 async fn announcement_records_node() {
508 let (offchain, chain) = make_keypairs();
509 let addr = chain.public().to_address();
510 let graph = RecordingGraph::default();
511
512 run(
513 vec![ChainEvent::Announcement(account(*offchain.public(), addr))],
514 StubChainKeys::new([]),
515 graph.clone(),
516 addr,
517 *offchain.public(),
518 HoprBalance::from(10u64),
519 WinningProbability::ALWAYS,
520 )
521 .await;
522
523 assert_eq!(graph.nodes(), vec![*offchain.public()]);
524 assert!(graph.edges().is_empty());
525 }
526
527 #[tokio::test]
528 async fn announcement_should_forward_to_peer_discovery_when_tx_is_set() -> anyhow::Result<()> {
529 use std::str::FromStr;
530
531 use hopr_api::types::internal::prelude::AccountType;
532
533 let (offchain, chain) = make_keypairs();
534 let addr = chain.public().to_address();
535 let multiaddr = hopr_api::Multiaddr::from_str("/ip4/1.2.3.4/tcp/9000").context("parse multiaddr")?;
536 let entry = AccountEntry {
537 entry_type: AccountType::Announced(vec![multiaddr.clone()]),
538 ..account(*offchain.public(), addr)
539 };
540 let graph = RecordingGraph::default();
541
542 let received = run_with_peer_discovery(
543 vec![ChainEvent::Announcement(entry)],
544 StubChainKeys::new([]),
545 graph.clone(),
546 addr,
547 *offchain.public(),
548 HoprBalance::from(10u64),
549 WinningProbability::ALWAYS,
550 )
551 .await;
552
553 assert_eq!(received.len(), 1, "expected exactly one peer-discovery event");
554 let (peer_id, addrs) = &received[0];
555 assert_eq!(
556 *peer_id,
557 hopr_api::PeerId::from(*offchain.public()),
558 "peer id must match the announced account's public key"
559 );
560 assert_eq!(addrs, &vec![multiaddr], "multiaddrs must be forwarded unchanged");
561 assert_eq!(
562 graph.nodes(),
563 vec![*offchain.public()],
564 "graph must also record the node"
565 );
566 Ok(())
567 }
568
569 #[tokio::test]
570 async fn channel_opened_records_capacity() {
571 let (src_offchain, src_chain) = make_keypairs();
572 let (dst_offchain, dst_chain) = make_keypairs();
573 let src_addr = src_chain.public().to_address();
574 let dst_addr = dst_chain.public().to_address();
575
576 let graph = RecordingGraph::default();
577 let stub = StubChainKeys::new([(src_addr, *src_offchain.public()), (dst_addr, *dst_offchain.public())]);
578
579 run(
581 vec![ChainEvent::ChannelOpened(channel(
582 src_addr,
583 dst_addr,
584 100,
585 ChannelStatus::Open,
586 ))],
587 stub,
588 graph.clone(),
589 src_addr,
590 *src_offchain.public(),
591 HoprBalance::from(10u64),
592 WinningProbability::ALWAYS,
593 )
594 .await;
595
596 let edges = graph.edges();
597 assert_eq!(edges.len(), 1);
598 assert_eq!(edges[0].balance, Some(hopr_api::graph::traits::Balance::from(100u64)));
599 assert_eq!(edges[0].src, *src_offchain.public());
600 assert_eq!(edges[0].dest, *dst_offchain.public());
601 }
602
603 #[tokio::test]
604 async fn channel_balance_decreased_records_updated_capacity() {
605 let (src_offchain, src_chain) = make_keypairs();
606 let (dst_offchain, dst_chain) = make_keypairs();
607 let src_addr = src_chain.public().to_address();
608 let dst_addr = dst_chain.public().to_address();
609
610 let graph = RecordingGraph::default();
611 let stub = StubChainKeys::new([(src_addr, *src_offchain.public()), (dst_addr, *dst_offchain.public())]);
612
613 run(
615 vec![ChainEvent::ChannelBalanceDecreased(
616 channel(src_addr, dst_addr, 50, ChannelStatus::Open),
617 HoprBalance::from(50u64),
618 )],
619 stub,
620 graph.clone(),
621 src_addr,
622 *src_offchain.public(),
623 HoprBalance::from(10u64),
624 WinningProbability::ALWAYS,
625 )
626 .await;
627
628 let edges = graph.edges();
629 assert_eq!(edges.len(), 1);
630 assert_eq!(edges[0].balance, Some(hopr_api::graph::traits::Balance::from(50u64)));
631 }
632
633 #[tokio::test]
634 async fn ticket_redeemed_records_updated_balance() {
635 let (src_offchain, src_chain) = make_keypairs();
639 let (dst_offchain, dst_chain) = make_keypairs();
640 let src_addr = src_chain.public().to_address();
641 let dst_addr = dst_chain.public().to_address();
642
643 let graph = RecordingGraph::default();
644 let stub = StubChainKeys::new([(src_addr, *src_offchain.public()), (dst_addr, *dst_offchain.public())]);
645
646 run(
648 vec![ChainEvent::TicketRedeemed(
649 channel(src_addr, dst_addr, 3, ChannelStatus::Open),
650 None,
651 )],
652 stub,
653 graph.clone(),
654 src_addr,
655 *src_offchain.public(),
656 HoprBalance::from(10u64),
657 WinningProbability::ALWAYS,
658 )
659 .await;
660
661 let edges = graph.edges();
662 assert_eq!(edges.len(), 1, "redemption must record a graph edge balance update");
663 assert_eq!(edges[0].balance, Some(hopr_api::graph::traits::Balance::from(3u64)));
664 }
665
666 #[tokio::test]
667 async fn channel_closed_records_capacity_none() {
668 let (src_offchain, src_chain) = make_keypairs();
669 let (dst_offchain, dst_chain) = make_keypairs();
670 let src_addr = src_chain.public().to_address();
671 let dst_addr = dst_chain.public().to_address();
672
673 let graph = RecordingGraph::default();
674 let stub = StubChainKeys::new([(src_addr, *src_offchain.public()), (dst_addr, *dst_offchain.public())]);
675
676 run(
677 vec![ChainEvent::ChannelClosed(channel(
678 src_addr,
679 dst_addr,
680 0,
681 ChannelStatus::Closed,
682 ))],
683 stub,
684 graph.clone(),
685 src_addr,
686 *src_offchain.public(),
687 HoprBalance::from(10u64),
688 WinningProbability::ALWAYS,
689 )
690 .await;
691
692 let edges = graph.edges();
693 assert_eq!(edges.len(), 1);
694 assert_eq!(edges[0].balance, None);
695 }
696
697 #[tokio::test]
701 async fn channel_closure_initiated_records_capacity_none() {
702 let (src_offchain, src_chain) = make_keypairs();
703 let (dst_offchain, dst_chain) = make_keypairs();
704 let src_addr = src_chain.public().to_address();
705 let dst_addr = dst_chain.public().to_address();
706
707 let graph = RecordingGraph::default();
708 let stub = StubChainKeys::new([(src_addr, *src_offchain.public()), (dst_addr, *dst_offchain.public())]);
709
710 run(
711 vec![ChainEvent::ChannelClosureInitiated(channel(
712 src_addr,
713 dst_addr,
714 100,
715 ChannelStatus::PendingToClose(SystemTime::now()),
716 ))],
717 stub,
718 graph.clone(),
719 src_addr,
720 *src_offchain.public(),
721 HoprBalance::from(10u64),
722 WinningProbability::ALWAYS,
723 )
724 .await;
725
726 let edges = graph.edges();
727 assert_eq!(edges.len(), 1, "closure-initiated must emit a graph update");
728 assert_eq!(
729 edges[0].balance, None,
730 "closure-initiated must clear the balance so routing stops using this edge"
731 );
732 }
733
734 #[tokio::test]
735 async fn ticket_price_change_pushes_a_new_face_value() {
736 let (src_offchain, src_chain) = make_keypairs();
737 let (dst_offchain, dst_chain) = make_keypairs();
738 let src_addr = src_chain.public().to_address();
739 let dst_addr = dst_chain.public().to_address();
740
741 let graph = RecordingGraph::default();
742 let stub = StubChainKeys::new([(src_addr, *src_offchain.public()), (dst_addr, *dst_offchain.public())]);
743
744 run(
746 vec![
747 ChainEvent::TicketPriceChanged(HoprBalance::from(20u64)),
748 ChainEvent::ChannelOpened(channel(src_addr, dst_addr, 200, ChannelStatus::Open)),
749 ],
750 stub,
751 graph.clone(),
752 src_addr,
753 *src_offchain.public(),
754 HoprBalance::from(10u64),
755 WinningProbability::ALWAYS,
756 )
757 .await;
758
759 assert_eq!(
760 graph.face_values(),
761 vec![
762 hopr_api::graph::traits::Balance::from(10u64),
763 hopr_api::graph::traits::Balance::from(20u64),
764 ],
765 "the seeded face value, then the one recomputed from the price change"
766 );
767
768 let edges = graph.edges();
769 assert_eq!(edges.len(), 1);
770 assert_eq!(
771 edges[0].balance,
772 Some(hopr_api::graph::traits::Balance::from(200u64)),
773 "the emitted balance must not depend on the price"
774 );
775 }
776
777 #[tokio::test]
778 async fn win_probability_change_pushes_a_new_face_value() -> anyhow::Result<()> {
779 let (src_offchain, src_chain) = make_keypairs();
780 let (dst_offchain, dst_chain) = make_keypairs();
781 let src_addr = src_chain.public().to_address();
782 let dst_addr = dst_chain.public().to_address();
783
784 let graph = RecordingGraph::default();
785 let stub = StubChainKeys::new([(src_addr, *src_offchain.public()), (dst_addr, *dst_offchain.public())]);
786
787 let new_prob = WinningProbability::try_from_f64(0.5).context("0.5 is a valid winning probability")?;
789 run(
790 vec![
791 ChainEvent::WinningProbabilityDecreased(new_prob),
792 ChainEvent::ChannelOpened(channel(src_addr, dst_addr, 100, 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![
806 hopr_api::graph::traits::Balance::from(10u64),
807 hopr_api::graph::traits::Balance::from(20u64),
808 ],
809 "the seeded face value, then the one recomputed from the probability change"
810 );
811
812 let edges = graph.edges();
813 assert_eq!(edges.len(), 1);
814 assert_eq!(
815 edges[0].balance,
816 Some(hopr_api::graph::traits::Balance::from(100u64)),
817 "the emitted balance must not depend on the winning probability"
818 );
819 Ok(())
820 }
821
822 #[tokio::test]
827 async fn a_face_value_is_seeded_before_any_pricing_event() {
828 let (src_offchain, src_chain) = make_keypairs();
829 let (dst_offchain, dst_chain) = make_keypairs();
830 let src_addr = src_chain.public().to_address();
831 let dst_addr = dst_chain.public().to_address();
832
833 let graph = RecordingGraph::default();
834 let stub = StubChainKeys::new([(src_addr, *src_offchain.public()), (dst_addr, *dst_offchain.public())]);
835
836 run(
838 vec![ChainEvent::ChannelOpened(channel(
839 src_addr,
840 dst_addr,
841 200,
842 ChannelStatus::Open,
843 ))],
844 stub,
845 graph.clone(),
846 src_addr,
847 *src_offchain.public(),
848 HoprBalance::from(10u64),
849 WinningProbability::ALWAYS,
850 )
851 .await;
852
853 assert_eq!(
854 graph.face_values(),
855 vec![hopr_api::graph::traits::Balance::from(10u64)],
856 "the graph must know the price before it records a balance it will be compared against"
857 );
858 }
859
860 #[tokio::test]
861 async fn unknown_chain_key_produces_no_graph_update() {
862 let (src_offchain, src_chain) = make_keypairs();
863 let (_, dst_chain) = make_keypairs();
864 let src_addr = src_chain.public().to_address();
865 let dst_addr = dst_chain.public().to_address();
866
867 let graph = RecordingGraph::default();
868 let stub = StubChainKeys::new([(src_addr, *src_offchain.public())]);
870
871 run(
872 vec![ChainEvent::ChannelOpened(channel(
873 src_addr,
874 dst_addr,
875 100,
876 ChannelStatus::Open,
877 ))],
878 stub,
879 graph.clone(),
880 src_addr,
881 *src_offchain.public(),
882 HoprBalance::from(10u64),
883 WinningProbability::ALWAYS,
884 )
885 .await;
886
887 assert!(graph.edges().is_empty(), "unknown key must produce no graph update");
888 }
889
890 #[tokio::test]
891 async fn self_address_resolved_via_own_packet_key() {
892 let (own_offchain, own_chain) = make_keypairs();
893 let (dst_offchain, dst_chain) = make_keypairs();
894 let own_chain_addr = own_chain.public().to_address();
895 let dst_addr = dst_chain.public().to_address();
896
897 let graph = RecordingGraph::default();
898 let stub = StubChainKeys::new([(dst_addr, *dst_offchain.public())]);
900
901 run(
902 vec![ChainEvent::ChannelOpened(channel(
903 own_chain_addr,
904 dst_addr,
905 100,
906 ChannelStatus::Open,
907 ))],
908 stub,
909 graph.clone(),
910 own_chain_addr,
911 *own_offchain.public(),
912 HoprBalance::from(10u64),
913 WinningProbability::ALWAYS,
914 )
915 .await;
916
917 let edges = graph.edges();
918 assert_eq!(edges.len(), 1);
919 assert_eq!(edges[0].src, *own_offchain.public());
920 assert_eq!(edges[0].dest, *dst_offchain.public());
921 }
922
923 #[tokio::test]
924 async fn announcement_should_handle_disconnected_peer_discovery_tx_gracefully() {
925 let (offchain, chain) = make_keypairs();
926 let addr = chain.public().to_address();
927 let (tx, rx) = hopr_utils::network_types::crossfire_sink::bounded_sink_channel(1);
928 drop(rx); process_chain_events(
931 StubChainKeys::new([]),
932 RecordingGraph::default(),
933 MemorySurbStore::default(),
934 futures::stream::iter(vec![ChainEvent::Announcement(account(*offchain.public(), addr))]),
935 addr,
936 *offchain.public(),
937 Arc::new(RwLock::new(HoprBalance::from(10u64))),
938 Arc::new(RwLock::new(WinningProbability::ALWAYS)),
939 Some(tx),
940 )
941 .await;
942 }
943
944 type MakeServiceEvent = fn(Address, Address, ServiceEntry, HoprBalance) -> ChainEvent;
945
946 #[rstest]
947 #[case::registered(|_, _, entry, _| ChainEvent::ServiceRegistered(entry))]
948 #[case::updated(|_, _, entry, _| ChainEvent::ServiceUpdated(entry))]
949 #[case::deregistered(|node, _, _, _| ChainEvent::ServiceDeregistered(ServiceType::GVPN_EXIT, node))]
950 #[case::type_registered(|_, owner, _, _| ChainEvent::ServiceTypeRegistered(ServiceType::GVPN_EXIT, owner))]
951 #[case::type_owner_changed(|_, owner, _, _| ChainEvent::ServiceTypeOwnerChanged(ServiceType::GVPN_EXIT, Some(owner)))]
952 #[case::type_requirement_changed(
953 |_, owner, _, _| ChainEvent::ServiceTypeRequirementChanged(ServiceType::GVPN_EXIT, Some(owner))
954 )]
955 #[case::type_registration_burn_changed(
956 |_, _, _, burn| ChainEvent::ServiceTypeRegistrationBurnChanged(ServiceType::GVPN_EXIT, burn)
957 )]
958 #[case::type_update_burn_changed(
959 |_, _, _, burn| ChainEvent::ServiceTypeUpdateBurnChanged(ServiceType::GVPN_EXIT, burn)
960 )]
961 #[case::type_registration_fee_changed(|_, _, _, burn| ChainEvent::ServiceTypeRegistrationFeeChanged(burn))]
962 #[case::registry_pointer_changed(|_, owner, _, _| ChainEvent::ServiceRegistryPointerChanged(owner))]
963 #[tokio::test]
964 async fn service_events_are_ignored_without_stopping_processing(
965 #[case] make_event: MakeServiceEvent,
966 ) -> anyhow::Result<()> {
967 let (src_offchain, src_chain) = make_keypairs();
968 let (dst_offchain, dst_chain) = make_keypairs();
969 let src_addr = src_chain.public().to_address();
970 let dst_addr = dst_chain.public().to_address();
971 let stub = StubChainKeys::new([(src_addr, *src_offchain.public()), (dst_addr, *dst_offchain.public())]);
972
973 let now = SystemTime::now();
974 let entry = ServiceEntry::new(
975 ServiceType::GVPN_EXIT,
976 dst_addr,
977 src_addr,
978 ServiceMetadata::try_from(b"exit-node".to_vec())?,
979 now,
980 now,
981 )?;
982 let event = make_event(dst_addr, src_addr, entry, HoprBalance::from(7u64));
983
984 let ticket_price = Arc::new(RwLock::new(HoprBalance::from(10u64)));
986 let win_probability = Arc::new(RwLock::new(WinningProbability::ALWAYS));
987 let graph = RecordingGraph::default();
988
989 let events = vec![
992 event,
993 ChainEvent::ChannelOpened(channel(src_addr, dst_addr, 100, ChannelStatus::Open)),
994 ];
995
996 process_chain_events(
997 stub,
998 graph.clone(),
999 MemorySurbStore::default(),
1000 futures::stream::iter(events),
1001 src_addr,
1002 *src_offchain.public(),
1003 ticket_price.clone(),
1004 win_probability.clone(),
1005 None,
1006 )
1007 .await;
1008
1009 assert_eq!(
1010 graph.face_values(),
1011 vec![hopr_api::graph::traits::Balance::from(10u64)],
1012 "only the unconditional startup face-value seed should be recorded"
1013 );
1014 assert_eq!(
1015 *ticket_price.read(),
1016 HoprBalance::from(10u64),
1017 "a service event must not change the ticket price"
1018 );
1019 assert_eq!(
1020 win_probability.read().as_f64(),
1021 WinningProbability::ALWAYS.as_f64(),
1022 "a service event must not change the winning probability"
1023 );
1024 assert!(graph.nodes().is_empty(), "a service event must not record a graph node");
1025
1026 let edges = graph.edges();
1027 assert_eq!(
1028 edges.len(),
1029 1,
1030 "only the channel event may record an edge, and it must still be routed"
1031 );
1032 assert_eq!(
1033 edges[0].balance,
1034 Some(hopr_api::graph::traits::Balance::from(100u64)),
1035 "the balance inputs must have survived the service event"
1036 );
1037 assert_eq!(edges[0].src, *src_offchain.public());
1038 assert_eq!(edges[0].dest, *dst_offchain.public());
1039
1040 Ok(())
1041 }
1042
1043 async fn run_own_channel_events(
1049 statuses: impl IntoIterator<Item = ChannelStatus>,
1050 ) -> (MemorySurbStore, HoprKeyIdent) {
1051 let (me_offchain, me_chain) = make_keypairs();
1052 let (peer_offchain, peer_chain) = make_keypairs();
1053 let (me_addr, peer_addr) = (me_chain.public().to_address(), peer_chain.public().to_address());
1054 let peer_id = HoprKeyIdent::from(1u32);
1055
1056 let stub = StubChainKeys::new([(me_addr, *me_offchain.public()), (peer_addr, *peer_offchain.public())])
1057 .with_key_ids([(*peer_offchain.public(), peer_id)]);
1058
1059 let surb_store = MemorySurbStore::default();
1060 let events = statuses
1061 .into_iter()
1062 .map(|status| ChainEvent::ChannelOpened(channel(me_addr, peer_addr, 100, status)))
1063 .collect();
1064
1065 run_with_surb_store(events, stub, surb_store.clone(), me_addr, *me_offchain.public()).await;
1066
1067 (surb_store, peer_id)
1068 }
1069
1070 #[tokio::test]
1071 async fn closing_an_own_outgoing_channel_should_invalidate_that_relayer() {
1072 let (store, peer_id) =
1073 run_own_channel_events([ChannelStatus::PendingToClose(std::time::SystemTime::now())]).await;
1074 assert!(store.is_relayer_invalidated(&peer_id), "PendingToClose must invalidate");
1075
1076 let (store, peer_id) = run_own_channel_events([ChannelStatus::Closed]).await;
1077 assert!(store.is_relayer_invalidated(&peer_id), "Closed must invalidate");
1078 }
1079
1080 #[tokio::test]
1081 async fn reopening_an_own_outgoing_channel_should_revalidate_that_relayer() {
1082 let (store, peer_id) = run_own_channel_events([ChannelStatus::Closed, ChannelStatus::Open]).await;
1083 assert!(!store.is_relayer_invalidated(&peer_id), "re-opening must revalidate");
1084 }
1085
1086 #[tokio::test]
1087 async fn a_channel_that_is_not_ours_should_not_invalidate_anything() {
1088 let (me_offchain, me_chain) = make_keypairs();
1090 let (a_offchain, a_chain) = make_keypairs();
1091 let (b_offchain, b_chain) = make_keypairs();
1092 let (me_addr, a_addr, b_addr) = (
1093 me_chain.public().to_address(),
1094 a_chain.public().to_address(),
1095 b_chain.public().to_address(),
1096 );
1097 let b_id = HoprKeyIdent::from(1u32);
1098
1099 let stub = StubChainKeys::new([
1100 (me_addr, *me_offchain.public()),
1101 (a_addr, *a_offchain.public()),
1102 (b_addr, *b_offchain.public()),
1103 ])
1104 .with_key_ids([(*b_offchain.public(), b_id)]);
1105
1106 let store = MemorySurbStore::default();
1107 run_with_surb_store(
1108 vec![ChainEvent::ChannelClosed(channel(
1109 a_addr,
1110 b_addr,
1111 100,
1112 ChannelStatus::Closed,
1113 ))],
1114 stub,
1115 store.clone(),
1116 me_addr,
1117 *me_offchain.public(),
1118 )
1119 .await;
1120
1121 assert!(
1122 !store.is_relayer_invalidated(&b_id),
1123 "someone else's channel must not affect our SURBs"
1124 );
1125 }
1126}