1pub mod config;
17pub mod constants;
19pub mod errors;
21pub mod path;
23pub mod protocol;
25#[cfg(feature = "testing")]
28pub mod testing;
29
30mod multiaddrs;
31
32#[cfg(feature = "capture")]
33mod capture;
34mod pipeline;
35pub mod socket;
36
37use std::{
38 sync::{Arc, OnceLock},
39 time::Duration,
40};
41
42use constants::MAXIMUM_MSG_OUTGOING_BUFFER_SIZE;
43use futures::{FutureExt, SinkExt, StreamExt, channel::mpsc::Sender, stream::select_with_strategy};
44use futures_concurrency::stream::StreamExt as ConcurrentStreamExt;
45pub use hopr_api::{
46 Multiaddr, PeerId,
47 network::{Health, traits::NetworkView},
48 types::{
49 crypto::{
50 keypairs::{ChainKeypair, Keypair, OffchainKeypair},
51 types::{HalfKeyChallenge, Hash, OffchainPublicKey, SimplePseudonym},
52 },
53 internal::{prelude::HoprPseudonym, routing::RoutingOptions},
54 },
55};
56use hopr_api::{
57 chain::{ChainKeyOperations, ChainReadAccountOperations, ChainReadChannelOperations, ChainValues},
58 ct::{CoverTrafficGeneration, ProbingTrafficGeneration},
59 graph::{NetworkGraphUpdate, NetworkGraphView, traits::EdgeObservableRead},
60 network::{BoxedProcessFn, NetworkStreamControl},
61 types::primitive::prelude::*,
62};
63pub use hopr_crypto_packet::HoprPixSpec;
64pub use hopr_protocol_app::prelude::{ApplicationData, ApplicationDataIn, ApplicationDataOut, Tag};
65pub use hopr_protocol_hopr::{MemorySurbStore, SurbStore};
66pub use hopr_protocol_pix::PixSpec;
71pub use hopr_protocol_pix::RecoveredSsa;
72use hopr_protocol_pix::ShareResolution;
73pub use hopr_transport_probe::{NeighborTelemetry, PathTelemetry, errors::ProbeError, ping::PingQueryReplier};
74use hopr_transport_probe::{
75 Probe,
76 ping::{PingConfig, Pinger},
77};
78pub use hopr_transport_session as session;
79#[cfg(feature = "runtime-tokio")]
80pub use hopr_transport_session::transfer_session;
81use hopr_transport_session::{
82 AgreedSsaQuota, DispatchResult, HoprSessionInPixEvent, HoprSessionOutPixEvent, PixToolbox, SessionManager,
83 SessionManagerConfig,
84};
85pub use hopr_transport_session::{
86 Capabilities as SessionCapabilities, Capability as SessionCapability, FlowControlConfig, HoprSession,
87 IncomingSession, InvalidPixParams, LOCAL_PIX_SUITE, PixParams, SESSION_MTU, SURB_SIZE, ServiceId,
88 SessionClientConfig, SessionId, SessionTarget, SurbBalancerConfig,
89 errors::{SessionManagerError, TransportSessionError},
90};
91#[cfg(feature = "telemetry")]
92pub use hopr_transport_session::{SessionAckMode, SessionLifecycleState};
93pub use hopr_transport_tag_allocator::TagAllocatorConfig;
94use hopr_utils::{
95 network_types::{
96 crossfire_sink::{CrossfireSink, bounded_sink_channel},
97 prelude::*,
98 },
99 runtime::AbortableList,
100};
101pub use multiaddr::Protocol;
102use tracing::{debug, warn};
103
104#[cfg(feature = "runtime-tokio")]
105use crate::path::BackgroundPathCacheRefreshable;
106pub use crate::{config::HoprProtocolConfig, protocol::PeerProtocolCounterRegistry};
107use crate::{
108 config::PixGlobalConfig,
109 constants::SESSION_INITIATION_TIMEOUT_BASE,
110 errors::HoprTransportError,
111 multiaddrs::strip_p2p_protocol,
112 path::{HoprGraphPathSelector, PathPlanner},
113 pipeline::HoprPacketPipelineBuilder,
114 socket::HoprSocket,
115};
116
117pub const APPLICATION_TAG_RANGE: std::ops::Range<Tag> = Tag::APPLICATION_TAG_RANGE;
118
119pub use hopr_api as api;
120use hopr_api::{
121 chain::{ChainReadTicketOperations, ChainWriteTicketOperations, PixDepositSecret},
122 node::{PixDepositAddressReceived, PixEvent, PixNewDepositAddress, PixPrivateKeyRecovered},
123 tickets::TicketFactory,
124 types::internal::routing::DestinationRouting,
125};
126use hopr_crypto_packet::HoprShareResolution;
127use rust_stream_ext_concurrent::then_concurrent::StreamThenConcurrentExt;
128
129lazy_static::lazy_static! {
131 static ref SESSION_INITIATION_TIMEOUT_MAX: Duration = 2 * SESSION_INITIATION_TIMEOUT_BASE * RoutingOptions::MAX_INTERMEDIATE_HOPS as u32;
132
133 static ref PEER_ID_CACHE: moka::sync::Cache<PeerId, OffchainPublicKey> = moka::sync::Cache::builder()
134 .time_to_idle(Duration::from_mins(15))
135 .max_capacity(10_000)
136 .build();
137
138 static ref RANDOM_DATA: [u8; 400] = hopr_api::types::crypto_random::random_bytes();
139}
140
141const SURB_BUFFER_TARGET_NUMERATOR: usize = 2;
149const SURB_BUFFER_TARGET_DENOMINATOR: usize = 3;
150
151const fn surb_buffer_target_ceiling(rb_capacity: usize) -> usize {
156 rb_capacity.saturating_mul(SURB_BUFFER_TARGET_NUMERATOR) / SURB_BUFFER_TARGET_DENOMINATOR
159}
160
161pub fn peer_id_to_public_key(peer_id: &PeerId) -> crate::errors::Result<OffchainPublicKey> {
166 PEER_ID_CACHE
167 .try_get_with_by_ref(peer_id, move || {
168 OffchainPublicKey::from_peerid(peer_id).map_err(|e| e.into())
169 })
170 .map_err(|e: Arc<HoprTransportError>| {
171 crate::errors::HoprTransportError::Other(anyhow::anyhow!(
172 "failed to convert peer_id ({:?}) to an offchain public key: {e}",
173 peer_id
174 ))
175 })
176}
177
178#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, strum::Display)]
179pub enum HoprTransportProcess {
180 #[strum(to_string = "component responsible for the transport medium (libp2p swarm)")]
181 Medium,
182 #[strum(to_string = "HOPR packet pipeline ({0})")]
183 Pipeline(protocol::PacketPipelineProcesses),
184 #[strum(to_string = "session manager sub-process #{0}")]
185 SessionsManagement(usize),
186 #[strum(to_string = "network probing sub-process: {0}")]
187 Probing(hopr_transport_probe::HoprProbeProcess),
188 #[cfg(feature = "runtime-tokio")]
189 #[strum(to_string = "path cache refresh")]
190 PathRefresh,
191 #[strum(to_string = "pix protocol event transformation")]
192 PixEvents,
193 #[strum(to_string = "sync of outgoing ticket indices")]
194 OutgoingIndexSync,
195 #[strum(to_string = "periodic protocol counter flush")]
196 CounterFlush,
197 SurbFlush,
199 #[strum(to_string = "mixer→wire forwarder")]
200 MixerForwarder,
201 #[cfg(feature = "capture")]
202 #[strum(to_string = "packet capture")]
203 Capture,
204}
205
206type HoprSessionManager = SessionManager<CrossfireSink<(DestinationRouting, ApplicationDataOut)>>;
208
209#[derive(Debug, Clone)]
214pub struct HoprSessionConfigurator {
215 id: SessionId,
216 smgr: std::sync::Weak<HoprSessionManager>,
218}
219
220impl HoprSessionConfigurator {
221 pub fn id(&self) -> &SessionId {
223 &self.id
224 }
225
226 pub async fn ping(&self) -> errors::Result<()> {
234 Ok(self
235 .smgr
236 .upgrade()
237 .ok_or(HoprTransportError::Other(anyhow::anyhow!("session manager is dropped")))?
238 .ping_session(&self.id)
239 .await?)
240 }
241
242 pub fn get_surb_balancer_config(&self) -> errors::Result<Option<SurbBalancerConfig>> {
248 Ok(self
249 .smgr
250 .upgrade()
251 .ok_or(HoprTransportError::Other(anyhow::anyhow!("session manager is dropped")))?
252 .get_surb_balancer_config(&self.id)?)
253 }
254
255 pub fn update_surb_balancer_config(&self, config: SurbBalancerConfig) -> errors::Result<()> {
260 Ok(self
261 .smgr
262 .upgrade()
263 .ok_or(HoprTransportError::Other(anyhow::anyhow!("session manager is dropped")))?
264 .update_surb_balancer_config(&self.id, config)?)
265 }
266
267 pub fn close(&self) -> bool {
274 match self.smgr.upgrade() {
275 Some(smgr) => smgr.close_session(&self.id),
276 None => false,
277 }
278 }
279}
280
281pub struct HoprTransport<Chain, Graph, Net> {
284 packet_key: OffchainKeypair,
285 chain_key: ChainKeypair,
286 chain_api: Chain,
287 ping: Arc<OnceLock<Pinger>>,
288 network: Arc<OnceLock<Net>>,
289 graph: Graph,
290 path_planner: PathPlanner<MemorySurbStore, Chain, HoprGraphPathSelector<Graph>>,
291 my_multiaddresses: Vec<Multiaddr>,
292 smgr: Arc<HoprSessionManager>,
293 session_telemetry_tag_allocator: Arc<dyn hopr_transport_tag_allocator::TagAllocator + Send + Sync>,
294 probing_tag_allocator: Arc<dyn hopr_transport_tag_allocator::TagAllocator + Send + Sync>,
295 counters: PeerProtocolCounterRegistry,
296 cfg: HoprProtocolConfig,
297}
298
299impl<Chain, Graph, Net> HoprTransport<Chain, Graph, Net>
300where
301 Chain: ChainReadChannelOperations
302 + ChainReadAccountOperations
303 + ChainWriteTicketOperations
304 + ChainKeyOperations
305 + ChainReadTicketOperations
306 + ChainValues
307 + Clone
308 + Send
309 + Sync
310 + 'static,
311 Graph: NetworkGraphView<NodeId = OffchainPublicKey>
312 + NetworkGraphUpdate
313 + hopr_api::graph::NetworkGraphWrite<NodeId = OffchainPublicKey>
314 + hopr_api::graph::NetworkGraphTraverse<NodeId = OffchainPublicKey>
315 + Clone
316 + Send
317 + Sync
318 + 'static,
319 <Graph as NetworkGraphView>::Observed: hopr_api::graph::traits::EdgeObservableRead + Send,
320 <Graph as hopr_api::graph::NetworkGraphTraverse>::Observed:
321 hopr_api::graph::traits::EdgeObservableRead + Send + 'static,
322 <Graph as hopr_api::graph::NetworkGraphWrite>::Observed: hopr_api::graph::traits::EdgeObservableWrite + Send,
323 Net: NetworkView + NetworkStreamControl + Clone + Send + Sync + 'static,
324{
325 pub fn new(
326 identity: (&ChainKeypair, &OffchainKeypair),
327 resolver: Chain,
328 graph: Graph,
329 my_multiaddresses: Vec<Multiaddr>,
330 cfg: HoprProtocolConfig,
331 ) -> errors::Result<Self> {
332 let me_offchain = *identity.1.public();
333 let planner_config = cfg.path_planner;
334 let selector = HoprGraphPathSelector::new(
335 me_offchain,
336 graph.clone(),
337 planner_config.max_cached_paths,
338 planner_config.edge_penalty,
339 planner_config.min_ack_rate,
340 planner_config.min_paths_anonymity_floor,
341 );
342
343 let tag_allocators = hopr_transport_tag_allocator::create_allocators_from_config(&cfg.session.tag_allocator)?;
344
345 let mut session_telemetry_tag_allocator = None;
346 let mut probing_tag_allocator = None;
347 for (usage, alloc) in tag_allocators {
348 match usage {
349 hopr_transport_tag_allocator::Usage::Session => {}
351 hopr_transport_tag_allocator::Usage::SessionTerminalTelemetry => {
352 session_telemetry_tag_allocator = Some(alloc)
353 }
354 hopr_transport_tag_allocator::Usage::ProvingTelemetry => probing_tag_allocator = Some(alloc),
355 }
356 }
357 let session_telemetry_tag_allocator = session_telemetry_tag_allocator
358 .ok_or_else(|| HoprTransportError::Api("session telemetry tag allocator missing".into()))?;
359 let probing_tag_allocator =
360 probing_tag_allocator.ok_or_else(|| HoprTransportError::Api("probing tag allocator missing".into()))?;
361
362 let surb_store_cfg = hopr_protocol_hopr::SurbStoreConfig {
370 pseudonyms_lifetime: std::env::var("HOPR_INTERNAL_SURB_PSEUDONYM_LIFETIME_MS")
371 .ok()
372 .and_then(|s| s.trim().parse::<u64>().ok())
373 .map(Duration::from_millis)
374 .map(|d| d.max(hopr_protocol_hopr::MINIMUM_SURB_LIFETIME))
375 .unwrap_or(cfg.packet.surb_store.pseudonyms_lifetime),
376 ..cfg.packet.surb_store
377 };
378
379 let path_planner = PathPlanner::new(
382 me_offchain,
383 MemorySurbStore::new(surb_store_cfg),
384 resolver.clone(),
385 selector,
386 planner_config,
387 );
388
389 Ok(Self {
390 packet_key: identity.1.clone(),
391 chain_key: identity.0.clone(),
392 ping: Arc::new(OnceLock::new()),
393 network: Arc::new(OnceLock::new()),
394 graph,
395 path_planner,
396 my_multiaddresses,
397 smgr: Arc::new(SessionManager::new(SessionManagerConfig {
398 frame_mtu: std::env::var("HOPR_SESSION_FRAME_SIZE")
399 .ok()
400 .and_then(|s| s.parse::<usize>().ok())
401 .unwrap_or_else(|| SessionManagerConfig::default().frame_mtu)
402 .max(SESSION_MTU),
403 max_frame_timeout: std::env::var("HOPR_SESSION_FRAME_TIMEOUT_MS")
404 .ok()
405 .and_then(|s| s.parse::<u64>().ok().map(Duration::from_millis))
406 .unwrap_or_else(|| SessionManagerConfig::default().max_frame_timeout)
407 .max(Duration::from_millis(100)),
408 max_frames_behind_gap: cfg.session.max_frames_behind_gap,
409 max_buffered_segments: std::env::var("HOPR_SESSION_MAX_BUFFERED_SEGMENTS")
410 .ok()
411 .and_then(|s| s.parse::<usize>().ok())
412 .unwrap_or_else(|| SessionManagerConfig::default().max_buffered_segments),
413 initiation_timeout_base: SESSION_INITIATION_TIMEOUT_BASE,
414 idle_timeout: cfg.session.idle_timeout,
415 balancer_sampling_interval: cfg.session.balancer_sampling_interval,
416 initial_return_session_egress_rate: 10,
417 minimum_surb_buffer_duration: cfg.session.balancer_minimum_surb_buffer_duration,
418 maximum_surb_buffer_size: surb_buffer_target_ceiling(cfg.packet.surb_store.rb_capacity),
419 surb_balance_notify_period: std::env::var("HOPR_SESSION_SURB_BALANCE_NOTIFY_PERIOD_MS")
422 .ok()
423 .and_then(|s| s.parse::<u64>().ok())
424 .map(|ms| Some(Duration::from_millis(ms)))
425 .unwrap_or(cfg.session.surb_balance_notify_period),
426 surb_target_notify: true,
427 maximum_sessions: cfg.session.maximum_managed_sessions,
428 pix_config: cfg.incoming_session_pix_config.clone(),
429 max_ssas_per_ssa_request: cfg.pix.max_ssas_per_request,
430 ..Default::default()
431 })),
432 chain_api: resolver,
433 session_telemetry_tag_allocator,
434 probing_tag_allocator,
435 counters: PeerProtocolCounterRegistry::default(),
436 cfg,
437 })
438 }
439
440 #[allow(clippy::too_many_arguments)]
449 pub async fn run_relay<T, TFact, Ct, PixEvt>(
450 &self,
451 cover_traffic: Ct,
452 network: Net,
453 network_process: BoxedProcessFn,
454 ticket_events: T,
455 ticket_factory: TFact,
456 exit_ack_share: Option<PixEvt>,
457 on_incoming_session: Sender<IncomingSession>,
458 ) -> errors::Result<(
459 HoprSocket<
460 futures::stream::BoxStream<'static, ApplicationDataIn>,
461 CrossfireSink<(DestinationRouting, ApplicationDataOut)>,
462 >,
463 AbortableList<HoprTransportProcess>,
464 )>
465 where
466 T: futures::Sink<hopr_api::node::TicketEvent> + Clone + Send + Unpin + 'static,
467 T::Error: std::error::Error + Clone + Send,
468 Ct: ProbingTrafficGeneration + CoverTrafficGeneration + Send + Sync + 'static,
469 TFact: TicketFactory + Clone + Send + Sync + 'static,
470 PixEvt: futures::Sink<PixEvent> + Clone + Unpin + Send + 'static,
471 PixEvt::Error: std::error::Error + Clone + Sync + Send + 'static,
472 {
473 self.run_inner(
474 protocol::NodeType::Relay,
475 cover_traffic,
476 network,
477 network_process,
478 ticket_events,
479 ticket_factory,
480 exit_ack_share,
481 Some(on_incoming_session),
482 )
483 .await
484 }
485
486 pub async fn run_exit<TFact, Ct, PixEvt>(
494 &self,
495 cover_traffic: Ct,
496 network: Net,
497 network_process: BoxedProcessFn,
498 ticket_factory: TFact,
499 pix_events: Option<PixEvt>,
500 on_incoming_session: Sender<IncomingSession>,
501 ) -> errors::Result<(
502 HoprSocket<
503 futures::stream::BoxStream<'static, ApplicationDataIn>,
504 CrossfireSink<(DestinationRouting, ApplicationDataOut)>,
505 >,
506 AbortableList<HoprTransportProcess>,
507 )>
508 where
509 Ct: ProbingTrafficGeneration + CoverTrafficGeneration + Send + Sync + 'static,
510 TFact: TicketFactory + Clone + Send + Sync + 'static,
511 PixEvt: futures::Sink<PixEvent> + Clone + Unpin + Send + 'static,
512 PixEvt::Error: std::error::Error + Clone + Sync + Send + 'static,
513 {
514 self.run_inner(
515 protocol::NodeType::Exit,
516 cover_traffic,
517 network,
518 network_process,
519 futures::sink::drain(),
520 ticket_factory,
521 pix_events,
522 Some(on_incoming_session),
523 )
524 .await
525 }
526
527 pub async fn run_entry<TFact, Ct, PixEvt>(
533 &self,
534 cover_traffic: Ct,
535 network: Net,
536 network_process: BoxedProcessFn,
537 ticket_factory: TFact,
538 pix_events: Option<PixEvt>,
539 ) -> errors::Result<(
540 HoprSocket<
541 futures::stream::BoxStream<'static, ApplicationDataIn>,
542 CrossfireSink<(DestinationRouting, ApplicationDataOut)>,
543 >,
544 AbortableList<HoprTransportProcess>,
545 )>
546 where
547 Ct: ProbingTrafficGeneration + CoverTrafficGeneration + Send + Sync + 'static,
548 TFact: TicketFactory + Clone + Send + Sync + 'static,
549 PixEvt: futures::Sink<PixEvent> + Clone + Unpin + Send + 'static,
550 PixEvt::Error: std::error::Error + Clone + Sync + Send + 'static,
551 {
552 self.run_inner(
553 protocol::NodeType::Entry,
554 cover_traffic,
555 network,
556 network_process,
557 futures::sink::drain(),
558 ticket_factory,
559 pix_events,
560 None,
561 )
562 .await
563 }
564
565 #[allow(clippy::too_many_arguments)]
572 async fn run_inner<T, TFact, Ct, PixEvt>(
573 &self,
574 role: protocol::NodeType,
575 cover_traffic: Ct,
576 network: Net,
577 network_process: BoxedProcessFn,
578 ticket_events: T,
579 ticket_factory: TFact,
580 exit_ack_share: Option<PixEvt>,
581 on_incoming_session: Option<Sender<IncomingSession>>,
582 ) -> errors::Result<(
583 HoprSocket<
584 futures::stream::BoxStream<'static, ApplicationDataIn>,
585 CrossfireSink<(DestinationRouting, ApplicationDataOut)>,
586 >,
587 AbortableList<HoprTransportProcess>,
588 )>
589 where
590 T: futures::Sink<hopr_api::node::TicketEvent> + Clone + Send + Unpin + 'static,
591 T::Error: std::error::Error + Clone + Send,
592 Ct: ProbingTrafficGeneration + CoverTrafficGeneration + Send + Sync + 'static,
593 TFact: TicketFactory + Clone + Send + Sync + 'static,
594 PixEvt: futures::Sink<PixEvent> + Clone + Unpin + Send + 'static,
595 PixEvt::Error: std::error::Error + Clone + Sync + Send + 'static,
596 {
597 let mut processes = AbortableList::<HoprTransportProcess>::default();
598
599 let (unresolved_routing_msg_tx, unresolved_routing_msg_rx) =
600 bounded_sink_channel::<(DestinationRouting, ApplicationDataOut)>(MAXIMUM_MSG_OUTGOING_BUFFER_SIZE);
601
602 let transport_network = network;
605 let transport_layer_process = network_process;
606
607 let msg_codec = crate::protocol::HoprBinaryCodec {};
608 let (wire_msg_tx, wire_msg_rx) =
609 protocol::stream::process_stream_protocol(msg_codec, transport_network.clone(), self.cfg.stream).await?;
610
611 let mut mixer_cfg = self.cfg.mixer;
616 mixer_cfg.metric_delay_window = u64::try_from(5 * mixer_cfg.delay_range.as_millis())
617 .unwrap_or(u64::MAX)
618 .max(1);
619 let (mixing_channel_tx, mix_rx) = hopr_transport_mixer::channel(mixer_cfg);
620 let transit_latency_cfg = self.cfg.transit_latency;
621 processes.insert(
622 HoprTransportProcess::MixerForwarder,
623 hopr_utils::spawn_as_abortable!(async move {
624 let mut mix_rx = mix_rx;
625 let mut wire_sink = wire_msg_tx;
626
627 if let Some(lat) = transit_latency_cfg {
628 #[cfg(feature = "runtime-tokio")]
634 {
635 let (tx, rx) = futures::channel::mpsc::unbounded();
636 let fan_out = async move {
637 while let Some(item) = futures::StreamExt::next(&mut mix_rx).await {
638 let mean_us = lat.mean.as_micros() as f64;
639 let std_us = lat.std_dev.as_micros() as f64;
640 let delay_us = if std_us > 0.0 {
641 use rand_distr::{Distribution, Normal};
642 Normal::new(mean_us, std_us)
643 .expect("transit latency Normal params are valid")
644 .sample(&mut rand::rng())
645 .max(0.0_f64)
646 } else {
647 mean_us.max(0.0)
648 };
649 let delay = Duration::from_micros(delay_us as u64);
650 let item_tx = tx.clone();
651 hopr_utils::runtime::prelude::spawn(async move {
652 if !delay.is_zero() {
653 futures_timer::Delay::new(delay).await;
654 }
655 let _ = item_tx.unbounded_send(item);
656 });
657 }
658 };
660 let fan_in = async move {
661 let mut rx = rx;
662 while let Some(item) = futures::StreamExt::next(&mut rx).await {
663 if wire_sink.send(item).await.is_err() {
664 tracing::error!(
665 task = %HoprTransportProcess::MixerForwarder,
666 "wire sink dropped — discarding transit-delayed packet"
667 );
668 break;
669 }
670 }
671 };
672 futures::join!(fan_out, fan_in);
673 }
674 #[cfg(not(feature = "runtime-tokio"))]
675 {
676 let _ = lat;
677 while let Some(item) = futures::StreamExt::next(&mut mix_rx).await {
678 if wire_sink.send(item).await.is_err() {
679 tracing::error!(
680 task = %HoprTransportProcess::MixerForwarder,
681 "wire sink dropped — discarding mixed packet"
682 );
683 }
684 }
685 }
686 } else {
687 while let Some(item) = futures::StreamExt::next(&mut mix_rx).await {
688 if wire_sink.send(item).await.is_err() {
689 tracing::error!(
690 task = %HoprTransportProcess::MixerForwarder,
691 "wire sink dropped — discarding mixed packet"
692 );
693 }
694 }
695 }
696
697 tracing::warn!(
698 task = %HoprTransportProcess::MixerForwarder,
699 "long-running background task finished"
700 );
701 }),
702 );
703
704 #[cfg(feature = "runtime-tokio")]
706 processes.insert(
707 HoprTransportProcess::PathRefresh,
708 hopr_utils::spawn_as_abortable!(self.path_planner.run_background_refresh()),
709 );
710
711 processes.insert(
712 HoprTransportProcess::Medium,
713 hopr_utils::spawn_as_abortable!(transport_layer_process().inspect(|_| tracing::warn!(
714 task = %HoprTransportProcess::Medium,
715 "long-running background task finished"
716 ))),
717 );
718
719 let msg_protocol_bidirectional_channel_capacity =
720 std::env::var("HOPR_INTERNAL_PROTOCOL_BIDIRECTIONAL_CHANNEL_CAPACITY")
721 .ok()
722 .and_then(|s| s.trim().parse::<usize>().ok())
723 .filter(|&c| c > 0)
724 .unwrap_or(16_384);
725
726 debug!(
727 capacity = msg_protocol_bidirectional_channel_capacity,
728 "creating protocol bidirectional channel"
729 );
730 let (tx_from_protocol, rx_from_protocol) =
731 bounded_sink_channel::<(HoprPseudonym, ApplicationDataIn)>(msg_protocol_bidirectional_channel_capacity);
732
733 let cover_traffic_allocated_tag = self
737 .session_telemetry_tag_allocator
738 .allocate()
739 .ok_or_else(|| HoprTransportError::Api("failed to allocate cover traffic tag".into()))?;
740 let cover_traffic_tag: Tag = cover_traffic_allocated_tag.value().into();
741
742 let rx_from_protocol = rx_from_protocol.filter_map(move |(pseudonym, data)| {
745 let _keep_alive = &cover_traffic_allocated_tag;
746 async move { (data.data.application_tag != cover_traffic_tag).then_some((pseudonym, data)) }
747 });
748
749 let cover_traffic_stream = CoverTrafficGeneration::build(&cover_traffic).filter_map(move |routing| {
751 let start =
752 hopr_api::types::crypto_random::random_integer(0, Some((RANDOM_DATA.len() - 100) as u64)) as usize;
753 let data = &RANDOM_DATA[start..start + 100];
754
755 futures::future::ready(if let Ok(data) = ApplicationData::new(cover_traffic_tag, data) {
756 Some((routing, ApplicationDataOut::with_no_packet_info(data)))
757 } else {
758 tracing::error!("failed to construct cover traffic packet");
759 None
760 })
761 });
762
763 let merged_unresolved_output_data =
765 select_with_strategy(unresolved_routing_msg_rx, cover_traffic_stream, |_: &mut ()| {
766 futures::stream::PollNext::Left
767 });
768
769 let path_planner = self.path_planner.clone();
776 let distress_threshold = self.cfg.packet.surb_store.distress_threshold;
777 let routing_concurrency = {
778 let avail = std::thread::available_parallelism()
779 .ok()
780 .map(|n| n.get())
781 .unwrap_or(1)
782 .max(1)
783 * 8;
784 self.cfg
785 .packet
786 .pipeline
787 .output_concurrency
788 .filter(|&n| n > 0)
789 .unwrap_or(avail)
790 };
791 let all_resolved_external_msg_rx = crate::path::resolve::resolve_routing_stage(
792 merged_unresolved_output_data,
793 move |size_hint, max_surbs, unresolved| {
794 let path_planner = path_planner.clone();
795 async move { path_planner.resolve_routing(size_hint, max_surbs, unresolved).await }
796 },
797 distress_threshold,
798 routing_concurrency,
799 crate::path::resolve::surb_resolution_wait(self.cfg.packet.pipeline.surb_resolution_wait),
800 );
801
802 let channels_dst = self
803 .chain_api
804 .domain_separators()
805 .await
806 .map_err(HoprTransportError::chain)?
807 .channel;
808
809 validator::Validate::validate(&self.cfg.pix)
822 .map_err(|error| HoprTransportError::Api(format!("invalid PIX configuration: {error}")))?;
823 fn narrow<T: TryFrom<usize>>(value: usize, field: &str) -> errors::Result<T> {
827 T::try_from(value).map_err(|_| HoprTransportError::Api(format!("PIX {field} out of range: {value}")))
828 }
829 let ssa_generator = Arc::new(
832 hopr_protocol_pix::SsaShareGenerator::<HoprPixSpec>::try_new(hopr_protocol_pix::SsaGeneratorConfig {
833 polynomials_per_ssa: narrow(self.cfg.pix.num_ssa_parts, "num_ssa_parts")?,
834 threshold: narrow(self.cfg.pix.ssa_part_size, "ssa_part_size")?,
835 surplus_shares: narrow(self.cfg.pix.surplus_shares(), "additional_shares")?,
836 })
837 .map_err(|error| HoprTransportError::Api(format!("invalid SSA generator configuration: {error}")))?,
838 );
839
840 let surb_round_trips = protocol::surb_telemetry::SurbRoundTripRegistry::default();
841
842 let pipeline_builder = HoprPacketPipelineBuilder::new()
843 .identity((&self.chain_key, &self.packet_key))
844 .transport((mixing_channel_tx, wire_msg_rx))
845 .api((tx_from_protocol, all_resolved_external_msg_rx))
846 .surb_store(self.path_planner.surb_store.clone())
847 .chain_api(self.chain_api.clone())
848 .ticket_factory(ticket_factory)
849 .ssa_generator(ssa_generator.clone())
850 .channels_dst(channels_dst)
851 .with_counters(self.counters.clone())
852 .with_surb_telemetry(
853 surb_round_trips.clone(),
854 protocol::surb_telemetry::path_slots_of(self.graph.clone()),
855 )
856 .with_config(self.cfg.packet);
857
858 let pix_toolbox = match (role, exit_ack_share) {
868 (protocol::NodeType::Exit, Some(ref ssa_events)) => {
869 let (pix_tools, pipeline_builder) = wire_exit_pix(
870 pipeline_builder,
871 ssa_generator.clone(),
872 ssa_reconstructor(&self.cfg.pix)?,
873 ssa_events,
874 self.smgr.clone(),
875 &mut processes,
876 );
877
878 let pipeline_processes = pipeline_builder.build_for_exit();
879 processes.extend_from(pipeline_processes);
880 Some(pix_tools)
881 }
882 (protocol::NodeType::Relay, None) => {
883 let pipeline_processes = pipeline_builder.with_ticket_events(ticket_events).build_for_relay();
887 processes.extend_from(pipeline_processes);
888 None
889 }
890 (protocol::NodeType::Relay, Some(ref ssa_events)) => {
891 let (pix_tools, pipeline_builder) = wire_exit_pix(
895 pipeline_builder,
896 ssa_generator.clone(),
897 ssa_reconstructor(&self.cfg.pix)?,
898 ssa_events,
899 self.smgr.clone(),
900 &mut processes,
901 );
902
903 let pipeline_processes = pipeline_builder.with_ticket_events(ticket_events).build_for_relay();
904 processes.extend_from(pipeline_processes);
905 Some(pix_tools)
906 }
907 (protocol::NodeType::Entry, Some(ref ssa_events)) => {
908 let dummy_reconstructor = ssa_reconstructor(&self.cfg.pix)?;
918 let (pix_tools, session_pix_events) = PixToolbox::new(ssa_generator.clone(), dummy_reconstructor);
919 processes.insert(
920 HoprTransportProcess::PixEvents,
921 hopr_utils::spawn_as_abortable!(
922 session_pix_events
930 .map(session_pix_event_to_pix_event)
931 .map(Ok)
932 .forward(ssa_events.clone().sink_map_err(HoprTransportError::other))
933 ),
934 );
935
936 processes.extend_from(pipeline_builder.build_for_entry());
937 Some(pix_tools)
938 }
939 (_, None) => {
940 let pipeline_processes = match role {
942 protocol::NodeType::Relay => pipeline_builder.with_ticket_events(ticket_events).build_for_relay(),
943 protocol::NodeType::Exit => pipeline_builder.build_for_exit(),
944 protocol::NodeType::Entry => pipeline_builder.build_for_entry(),
945 };
946 processes.extend_from(pipeline_processes);
947 None
948 }
949 };
950
951 let smgr_start_res = if role != protocol::NodeType::Entry {
954 self.smgr.start(
955 unresolved_routing_msg_tx.clone(),
956 on_incoming_session.ok_or_else(|| {
957 HoprTransportError::Api("on_incoming_session channel is required for relay/exit nodes".into())
958 })?,
959 pix_toolbox,
960 )
961 } else {
962 self.smgr
963 .start(unresolved_routing_msg_tx.clone(), futures::sink::drain(), pix_toolbox)
964 };
965
966 smgr_start_res
967 .map_err(|_| HoprTransportError::Api("failed to start session manager".into()))?
968 .into_iter()
969 .enumerate()
970 .map(|(i, jh)| (HoprTransportProcess::SessionsManagement(i + 1), jh))
971 .for_each(|(k, v)| {
972 processes.insert(k, v);
973 });
974 let flush_counters = self.counters.clone();
976 let flush_graph = self.graph.clone();
977 let flush_me = *self.packet_key.public();
978 let flush_interval = self.cfg.counter_flush_interval;
979 processes.insert(
980 HoprTransportProcess::CounterFlush,
981 hopr_utils::spawn_as_abortable!(async move {
982 use hopr_api::graph::traits::{EdgeObservableWrite, EdgeWeightType};
983
984 futures_time::stream::interval(futures_time::time::Duration::from(flush_interval))
985 .for_each(|_| {
986 for (peer, num_packets, num_acks) in flush_counters.drain() {
987 tracing::trace!(
988 %peer,
989 num_packets,
990 num_acks,
991 "flushing protocol conformance counters"
992 );
993 flush_graph.upsert_edge(&flush_me, &peer, |obs| {
994 obs.record(EdgeWeightType::ImmediateProtocolConformance { num_packets, num_acks });
995 });
996 }
997 futures::future::ready(())
998 })
999 .await;
1000 }),
1001 );
1002
1003 tracing::info!(?role, "starting surb round-trip flush task");
1005 const RETURN_PATH_DEGRADED_GRACE: std::time::Duration = std::time::Duration::from_secs(10);
1009 let surb_flush_graph = self.graph.clone();
1010 let surb_flush_interval = self.cfg.surb_flush_interval;
1011 let surb_flush_smgr = self.smgr.clone();
1012 let surb_flush_chain = self.chain_api.clone();
1013 let surb_flush_planner = self.path_planner.clone();
1014 processes.insert(
1015 HoprTransportProcess::SurbFlush,
1016 hopr_utils::spawn_as_abortable!(async move {
1017 let mut episodes = protocol::return_path_recovery::ReturnPathEpisodes::new(RETURN_PATH_DEGRADED_GRACE);
1018 let mut ticks = futures_time::stream::interval(futures_time::time::Duration::from(surb_flush_interval));
1019
1020 while ticks.next().await.is_some() {
1021 let silent = surb_round_trips.degraded_destinations();
1024
1025 protocol::surb_telemetry::flush_into(
1029 &surb_round_trips,
1030 &surb_flush_graph,
1031 hopr_utils::platform::time::native::current_time()
1032 .as_unix_timestamp()
1033 .as_millis(),
1034 );
1035
1036 let (planner, chain, smgr) = (&surb_flush_planner, &surb_flush_chain, &surb_flush_smgr);
1043 for step in episodes
1044 .tick(
1045 silent,
1046 |destination| async move { planner.recompute_paths_from(&destination).await },
1047 |destination| async move {
1048 chain
1058 .packet_key_to_chain_key(&destination)
1059 .ok()
1060 .flatten()
1061 .map(hopr_api::types::internal::prelude::NodeId::Chain)
1062 .map(|n| smgr.mark_return_path_degraded(&n, RETURN_PATH_DEGRADED_GRACE))
1063 .unwrap_or(0)
1064 },
1065 )
1066 .await
1067 {
1068 match step {
1069 protocol::return_path_recovery::RecoveryStep::Replanned { destination, moved } => {
1070 tracing::info!(%destination, entries = moved, "return path silent, re-planned")
1071 }
1072 protocol::return_path_recovery::RecoveryStep::Refilled { destination, sessions } => {
1073 tracing::info!(%destination, sessions, "refilling behind the re-plan")
1074 }
1075 }
1076 }
1077 }
1078 }),
1079 );
1080
1081 let manual_ping_channel_capacity = std::env::var("HOPR_INTERNAL_MANUAL_PING_CHANNEL_CAPACITY")
1083 .ok()
1084 .and_then(|s| s.trim().parse::<usize>().ok())
1085 .filter(|&c| c > 0)
1086 .unwrap_or(128);
1087 debug!(capacity = manual_ping_channel_capacity, "Creating manual ping channel");
1088 let (manual_ping_tx, manual_ping_rx_raw) =
1089 crossfire::mpsc::bounded_async::<(OffchainPublicKey, PingQueryReplier)>(manual_ping_channel_capacity);
1090 let manual_ping_rx = manual_ping_rx_raw.into_stream();
1091
1092 let probe = Probe::new(self.cfg.probe, self.probing_tag_allocator.clone());
1093
1094 let (probing_processes, probe_classifier) = probe
1095 .continuously_scan(
1096 unresolved_routing_msg_tx.clone(),
1097 manual_ping_rx,
1098 cover_traffic,
1099 self.graph.clone(),
1100 )
1101 .await;
1102
1103 processes.flat_map_extend_from(probing_processes, HoprTransportProcess::Probing);
1104
1105 self.ping
1107 .clone()
1108 .set(Pinger::new(
1109 PingConfig {
1110 timeout: self.cfg.probe.timeout,
1111 },
1112 manual_ping_tx,
1113 ))
1114 .map_err(|_| HoprTransportError::Api("must set the ticket aggregation writer only once".into()))?;
1115
1116 let (on_incoming_data_tx, on_incoming_data_rx) =
1125 bounded_sink_channel::<ApplicationDataIn>(msg_protocol_bidirectional_channel_capacity);
1126 let smgr = self.smgr.clone();
1127 let unresolved_routing_msg_tx_for_task = unresolved_routing_msg_tx.clone();
1128 processes.insert(
1129 HoprTransportProcess::SessionsManagement(0),
1130 hopr_utils::spawn_as_abortable!(async move {
1131 probe_classifier
1132 .filter_stream(unresolved_routing_msg_tx_for_task, rx_from_protocol)
1133 .filter_map(move |(pseudonym, data)| {
1134 hopr_transport_session::counters::DISPATCH_MESSAGE_CALLS
1135 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1136 let result = match smgr.dispatch_message(pseudonym, data) {
1148 Ok(DispatchResult::Processed) => {
1149 tracing::trace!("message dispatch completed");
1150 None
1151 }
1152 Ok(DispatchResult::Unrelated(data)) => {
1153 tracing::trace!("unrelated message dispatch completed");
1154 Some(data)
1155 }
1156 Err(error) => {
1157 tracing::error!(%error, "error while dispatching packet in the session manager");
1158 None
1159 }
1160 };
1161 async move {
1162 hopr_utils::runtime::prelude::consume_budget().await;
1163 result
1164 }
1165 })
1166 .fold(on_incoming_data_tx, |mut tx, data| async move {
1167 if tx.send(data).await.is_err() {
1168 tracing::error!(
1169 task = %HoprTransportProcess::SessionsManagement(0),
1170 "incoming-data channel disconnected — dropping unrelated packet; \
1171 HoprSocket must be consumed or drained by the caller"
1172 );
1173 }
1174 tx
1175 })
1176 .await;
1177 tracing::warn!(
1178 task = %HoprTransportProcess::SessionsManagement(0),
1179 "long-running background task finished"
1180 );
1181 }),
1182 );
1183
1184 self.network
1186 .clone()
1187 .set(transport_network)
1188 .map_err(|_| HoprTransportError::Api("transport network viewer already set".into()))?;
1189
1190 Ok((
1191 (on_incoming_data_rx.boxed(), unresolved_routing_msg_tx).into(),
1192 processes,
1193 ))
1194 }
1195
1196 #[tracing::instrument(level = "debug", skip(self))]
1197 pub async fn ping(
1198 &self,
1199 peer: &OffchainPublicKey,
1200 ) -> errors::Result<(std::time::Duration, <Graph as NetworkGraphView>::Observed)> {
1201 let me: &OffchainPublicKey = self.packet_key.public();
1202 if peer == me {
1203 return Err(HoprTransportError::Api("ping to self does not make sense".into()));
1204 }
1205
1206 let pinger = self
1207 .ping
1208 .get()
1209 .ok_or_else(|| HoprTransportError::Api("ping processing is not yet initialized".into()))?;
1210
1211 let latency = (*pinger).ping(peer).await?;
1212
1213 if let Some(observations) = self.graph.edge(me, peer) {
1214 Ok((latency, observations))
1215 } else {
1216 Err(HoprTransportError::Api(format!(
1217 "no observations available for peer {peer}",
1218 )))
1219 }
1220 }
1221
1222 #[tracing::instrument(level = "debug", skip(self))]
1223 pub async fn new_session(
1224 &self,
1225 destination: Address,
1226 target: SessionTarget,
1227 cfg: SessionClientConfig,
1228 ) -> errors::Result<(HoprSession, HoprSessionConfigurator)> {
1229 let session = self.smgr.new_session(destination, target, cfg).await?;
1230 let id = *session.id();
1231 Ok((
1232 session,
1233 HoprSessionConfigurator {
1234 id,
1235 smgr: Arc::downgrade(&self.smgr),
1236 },
1237 ))
1238 }
1239
1240 #[tracing::instrument(level = "debug", skip(self))]
1241 pub async fn listening_multiaddresses(&self) -> Vec<Multiaddr> {
1242 self.network
1243 .get()
1244 .ok_or_else(|| HoprTransportError::Api("transport network is not yet initialized".into()))
1245 .map(|network| network.listening_as().into_iter().collect())
1246 .unwrap_or_default()
1247 }
1248
1249 #[tracing::instrument(level = "debug", skip(self))]
1250 pub fn announceable_multiaddresses(&self) -> Vec<Multiaddr> {
1251 let mut mas = self
1252 .local_multiaddresses()
1253 .into_iter()
1254 .filter(|ma| {
1255 crate::multiaddrs::is_supported(ma)
1256 && (self.cfg.transport.announce_local_addresses || is_public_address(ma))
1257 })
1258 .map(|ma| strip_p2p_protocol(&ma))
1259 .filter(|v| !v.is_empty())
1260 .collect::<Vec<_>>();
1261
1262 mas.sort_by(|l, r| {
1263 let is_left_dns = crate::multiaddrs::is_dns(l);
1264 let is_right_dns = crate::multiaddrs::is_dns(r);
1265
1266 if !(is_left_dns ^ is_right_dns) {
1267 std::cmp::Ordering::Equal
1268 } else if is_left_dns {
1269 std::cmp::Ordering::Less
1270 } else {
1271 std::cmp::Ordering::Greater
1272 }
1273 });
1274
1275 mas
1276 }
1277
1278 pub fn graph(&self) -> &Graph {
1280 &self.graph
1281 }
1282
1283 pub fn surb_store(&self) -> &MemorySurbStore {
1288 &self.path_planner.surb_store
1289 }
1290
1291 #[tracing::instrument(level = "debug", skip(self))]
1292 pub fn local_multiaddresses(&self) -> Vec<Multiaddr> {
1293 self.network
1294 .get()
1295 .map(|network| network.listening_as().into_iter().collect())
1296 .unwrap_or_else(|| {
1297 tracing::error!("transport network is not yet initialized, cannot fetch announced multiaddresses");
1298 self.my_multiaddresses.clone()
1299 })
1300 }
1301
1302 #[tracing::instrument(level = "debug", skip(self))]
1303 pub async fn network_observed_multiaddresses(&self, peer: &OffchainPublicKey) -> Vec<Multiaddr> {
1304 match self
1305 .network
1306 .get()
1307 .ok_or_else(|| HoprTransportError::Api("transport network is not yet initialized".into()))
1308 {
1309 Ok(network) => network
1310 .multiaddress_of(&peer.into())
1311 .unwrap_or_default()
1312 .into_iter()
1313 .collect(),
1314 Err(error) => {
1315 tracing::error!(%error, "failed to get observed multiaddresses");
1316 return vec![];
1317 }
1318 }
1319 }
1320
1321 #[tracing::instrument(level = "debug", skip(self))]
1322 pub async fn network_health(&self) -> Health {
1323 self.network
1324 .get()
1325 .ok_or_else(|| HoprTransportError::Api("transport network is not yet initialized".into()))
1326 .map(|network| network.health())
1327 .unwrap_or(Health::Red)
1328 }
1329
1330 pub async fn network_connected_peers(&self) -> errors::Result<Vec<OffchainPublicKey>> {
1331 Ok(futures::stream::iter(
1332 self.network
1333 .get()
1334 .ok_or_else(|| {
1335 tracing::error!("transport network is not yet initialized");
1336 HoprTransportError::Api("transport network is not yet initialized".into())
1337 })?
1338 .connected_peers(),
1339 )
1340 .filter_map(|peer_id| async move {
1341 match peer_id_to_public_key(&peer_id) {
1342 Ok(key) => Some(key),
1343 Err(error) => {
1344 tracing::warn!(%peer_id, %error, "failed to convert PeerId to OffchainPublicKey");
1345 None
1346 }
1347 }
1348 })
1349 .collect()
1350 .await)
1351 }
1352
1353 #[tracing::instrument(level = "debug", skip(self))]
1354 pub fn network_peer_observations(&self, peer: &OffchainPublicKey) -> Option<<Graph as NetworkGraphView>::Observed> {
1355 self.graph.edge(self.packet_key.public(), peer)
1356 }
1357
1358 #[tracing::instrument(level = "debug", skip(self))]
1360 pub async fn all_network_peers(
1361 &self,
1362 minimum_score: f64,
1363 ) -> errors::Result<Vec<(OffchainPublicKey, <Graph as NetworkGraphView>::Observed)>> {
1364 let me = self.packet_key.public();
1365 Ok(self
1366 .network_connected_peers()
1367 .await?
1368 .into_iter()
1369 .filter_map(|peer| {
1370 let observation = self.graph.edge(me, &peer);
1371 if let Some(info) = observation {
1372 if info.score().is_some_and(|score| score >= minimum_score) {
1374 Some((peer, info))
1375 } else {
1376 None
1377 }
1378 } else {
1379 None
1380 }
1381 })
1382 .collect::<Vec<_>>())
1383 }
1384}
1385
1386pub(crate) fn recovered_ssa_to_pix_event(
1389 rec: &RecoveredSsa<SimplePseudonym, <HoprPixSpec as PixSpec>::AddressPrivateKey>,
1390) -> PixEvent {
1391 PixEvent::PrivateKeyRecovered(PixPrivateKeyRecovered {
1392 id: (*rec.ssa_id.pseudonym(), rec.ssa_id.ssa_index()),
1393 secret: PixDepositSecret(rec.ssa.secret().clone()),
1394 })
1395}
1396
1397const PIX_EVENT_DISPATCH_CONCURRENCY: usize = 64;
1405
1406fn session_pix_event_to_pix_event(event: HoprSessionOutPixEvent) -> PixEvent {
1409 match event {
1410 HoprSessionOutPixEvent::ReadyToDeposit(AgreedSsaQuota {
1411 ssa_id,
1412 deposit_address,
1413 quota_per_ssa,
1414 }) => PixEvent::NewDepositAddress(PixNewDepositAddress {
1415 id: (*ssa_id.pseudonym(), ssa_id.ssa_index()),
1416 address: deposit_address.into(),
1417 quota: quota_per_ssa,
1418 additional_data: None,
1423 }),
1424 HoprSessionOutPixEvent::DepositNeeded(
1425 AgreedSsaQuota {
1426 ssa_id,
1427 deposit_address,
1428 quota_per_ssa,
1429 },
1430 notifier,
1431 ) => PixEvent::DepositAddressReceived(PixDepositAddressReceived {
1432 id: (*ssa_id.pseudonym(), ssa_id.ssa_index()),
1433 address: deposit_address.into(),
1434 quota: quota_per_ssa,
1435 additional_data: None,
1438 deposit_updated: Some(notifier),
1439 }),
1440 }
1441}
1442
1443async fn dispatch_share_resolution(smgr: Arc<HoprSessionManager>, resolution: HoprShareResolution) -> Option<PixEvent> {
1446 match resolution {
1447 ShareResolution::RecoveredSsa(ssa_recovery_event) => {
1448 if let Err(error) = smgr
1449 .dispatch_pix_event(HoprSessionInPixEvent::SsaRecovered(ssa_recovery_event.ssa_id))
1450 .await
1451 {
1452 tracing::error!(%error, "failed to dispatch SSA recovery PIX event to the SessionManager");
1453 }
1454 Some(recovered_ssa_to_pix_event(&ssa_recovery_event))
1455 }
1456 ShareResolution::AlmostRecoveredSsa(ssa_id) => {
1457 if let Err(error) = smgr
1458 .dispatch_pix_event(HoprSessionInPixEvent::SsaAlmostRecovered(ssa_id))
1459 .await
1460 {
1461 tracing::error!(%error, %ssa_id, "failed to dispatch early SSA recovery event to the SessionManager");
1462 }
1463 None
1464 }
1465 ShareResolution::InvalidShares {
1466 peer,
1467 ssa_id,
1468 observed_total,
1469 } => {
1470 tracing::error!(
1471 %peer, %ssa_id, observed_total,
1472 "first RP relayer sent acknowledgement indicating invalid PIX share from Entry"
1473 );
1474 if let Err(error) = smgr
1475 .dispatch_pix_event(HoprSessionInPixEvent::UnverifiableShare(ssa_id))
1476 .await
1477 {
1478 tracing::error!(%error, %ssa_id, "failed to dispatch invalid share PIX event to the SessionManager");
1479 }
1480 None
1481 }
1482 ShareResolution::Progress(progress) => {
1489 tracing::trace!(
1490 ssa_id = %progress.ssa_id,
1491 useful_shares = progress.useful_shares,
1492 target = progress.target_useful_shares,
1493 recovered_polynomials = progress.recovered_polynomials,
1494 "pix recovery progress"
1495 );
1496 None
1497 }
1498 }
1499}
1500
1501fn ssa_reconstructor(cfg: &PixGlobalConfig) -> errors::Result<Arc<hopr_protocol_pix::SsaReconstructor<HoprPixSpec>>> {
1515 hopr_protocol_pix::SsaReconstructor::<HoprPixSpec>::try_new(cfg.reconstructor.into())
1516 .map(Arc::new)
1517 .map_err(|error| HoprTransportError::Api(format!("invalid SSA reconstructor configuration: {error}")))
1518}
1519
1520#[allow(clippy::type_complexity)]
1536fn wire_exit_pix<WIn, WOut, Chain, S, TFact, G, AppOut, AppIn, TEvt, PixEvt>(
1537 pipeline_builder: HoprPacketPipelineBuilder<WIn, WOut, Chain, S, TFact, G, AppOut, AppIn, TEvt>,
1538 ssa_generator: Arc<hopr_protocol_pix::SsaShareGenerator<HoprPixSpec>>,
1539 ssa_reconstructor: Arc<hopr_protocol_pix::SsaReconstructor<HoprPixSpec>>,
1540 ssa_events: &PixEvt,
1541 smgr: Arc<HoprSessionManager>,
1542 processes: &mut AbortableList<HoprTransportProcess>,
1543) -> (
1544 PixToolbox,
1545 HoprPacketPipelineBuilder<
1546 WIn,
1547 WOut,
1548 Chain,
1549 S,
1550 TFact,
1551 G,
1552 AppOut,
1553 AppIn,
1554 TEvt,
1555 Arc<hopr_protocol_pix::SsaReconstructor<HoprPixSpec>>,
1556 CrossfireSink<HoprShareResolution>,
1557 >,
1558)
1559where
1560 PixEvt: futures::Sink<PixEvent> + Clone + Unpin + Send + 'static,
1561 PixEvt::Error: std::error::Error + Clone + Sync + Send + 'static,
1562{
1563 let (pix_tools, session_pix_events) = PixToolbox::new(ssa_generator, ssa_reconstructor.clone());
1564 let (ssa_share_resolution_events_tx, ssa_share_resolution_events_rx) = bounded_sink_channel(1024);
1565 processes.insert(
1566 HoprTransportProcess::PixEvents,
1567 hopr_utils::spawn_as_abortable!(
1568 pix_event_stream(session_pix_events, ssa_share_resolution_events_rx, smgr)
1569 .map(Ok)
1570 .forward(ssa_events.clone().sink_map_err(HoprTransportError::other))
1571 ),
1572 );
1573
1574 (
1575 pix_tools,
1576 pipeline_builder.with_exit_ack_share_processing(ssa_reconstructor, ssa_share_resolution_events_tx),
1577 )
1578}
1579
1580fn pix_event_stream(
1591 session_pix_events: impl futures::Stream<Item = HoprSessionOutPixEvent> + Send + 'static,
1592 ssa_share_resolutions: impl futures::Stream<Item = HoprShareResolution> + Send + 'static,
1593 smgr: Arc<HoprSessionManager>,
1594) -> impl futures::Stream<Item = PixEvent> + Send + 'static {
1595 session_pix_events.map(session_pix_event_to_pix_event).merge(
1596 ssa_share_resolutions
1597 .then_concurrent(
1598 move |resolution| dispatch_share_resolution(smgr.clone(), resolution),
1599 PIX_EVENT_DISPATCH_CONCURRENCY,
1600 )
1601 .filter_map(futures::future::ready),
1602 )
1603}
1604
1605impl<Chain, Graph, Net> NetworkView for HoprTransport<Chain, Graph, Net>
1610where
1611 Net: NetworkView + Send + Sync + 'static,
1612{
1613 fn listening_as(&self) -> std::collections::HashSet<Multiaddr> {
1614 self.network.get().map(|n| n.listening_as()).unwrap_or_default()
1615 }
1616
1617 fn multiaddress_of(&self, peer: &PeerId) -> Option<std::collections::HashSet<Multiaddr>> {
1618 self.network.get()?.multiaddress_of(peer)
1619 }
1620
1621 fn discovered_peers(&self) -> std::collections::HashSet<PeerId> {
1622 self.network.get().map(|n| n.discovered_peers()).unwrap_or_default()
1623 }
1624
1625 fn connected_peers(&self) -> std::collections::HashSet<PeerId> {
1626 self.network.get().map(|n| n.connected_peers()).unwrap_or_default()
1627 }
1628
1629 fn is_connected(&self, peer: &PeerId) -> bool {
1630 self.network.get().map(|n| n.is_connected(peer)).unwrap_or(false)
1631 }
1632
1633 fn health(&self) -> Health {
1634 self.network.get().map(|n| n.health()).unwrap_or(Health::Red)
1635 }
1636
1637 fn subscribe_network_events(
1638 &self,
1639 ) -> impl futures::Stream<Item = hopr_api::network::NetworkEvent> + Send + 'static {
1640 match self.network.get() {
1641 Some(n) => futures::future::Either::Left(n.subscribe_network_events()),
1642 None => futures::future::Either::Right(futures::stream::empty()),
1643 }
1644 }
1645}
1646
1647#[async_trait::async_trait]
1652impl<Chain, Graph, Net> hopr_api::node::TransportOperations for HoprTransport<Chain, Graph, Net>
1653where
1654 Chain: ChainReadChannelOperations
1655 + ChainReadAccountOperations
1656 + hopr_api::chain::ChainWriteTicketOperations
1657 + ChainKeyOperations
1658 + hopr_api::chain::ChainReadTicketOperations
1659 + ChainValues
1660 + Clone
1661 + Send
1662 + Sync
1663 + 'static,
1664 Graph: NetworkGraphView<NodeId = OffchainPublicKey>
1665 + NetworkGraphUpdate
1666 + hopr_api::graph::NetworkGraphWrite<NodeId = OffchainPublicKey>
1667 + hopr_api::graph::NetworkGraphTraverse<NodeId = OffchainPublicKey>
1668 + Clone
1669 + Send
1670 + Sync
1671 + 'static,
1672 <Graph as NetworkGraphView>::Observed: EdgeObservableRead + Send,
1673 <Graph as hopr_api::graph::NetworkGraphTraverse>::Observed: EdgeObservableRead + Send + 'static,
1674 <Graph as hopr_api::graph::NetworkGraphWrite>::Observed: hopr_api::graph::traits::EdgeObservableWrite + Send,
1675 Net: NetworkView + NetworkStreamControl + Clone + Send + Sync + 'static,
1676{
1677 type Error = errors::HoprTransportError;
1678 type Observable = <Graph as NetworkGraphView>::Observed;
1679
1680 async fn ping(&self, key: &OffchainPublicKey) -> Result<(Duration, Self::Observable), Self::Error> {
1681 self.ping(key).await
1682 }
1683
1684 async fn observed_multiaddresses(&self, key: &OffchainPublicKey) -> Vec<Multiaddr> {
1685 self.network_observed_multiaddresses(key).await
1686 }
1687}
1688
1689pub const PACKET_PAYLOAD_SIZE: usize = hopr_crypto_packet::prelude::HoprPacket::PAYLOAD_SIZE;
1691
1692#[cfg(test)]
1693mod pix_recovery_event_tests {
1694 use hopr_api::{
1695 node::PixEvent,
1696 types::{
1697 crypto::{
1698 keypairs::{Keypair, OffchainKeypair},
1699 types::{HalfKey, SimplePseudonym},
1700 },
1701 crypto_random::{Randomizable, random_bytes},
1702 internal::prelude::VerifiedAcknowledgement,
1703 },
1704 };
1705 use hopr_crypto_packet::HoprPixSpec;
1706 use hopr_protocol_pix::{
1707 EntryShareGenerator, ExitAcknowledgementShareProcessor, PixSpec, SsaGeneratorConfig, SsaId, SsaIndex,
1708 SsaReconstructor, SsaReconstructorConfig, SsaShareGenerator, TaggedEncryptedPartialSsaShare,
1709 };
1710
1711 use super::recovered_ssa_to_pix_event;
1712
1713 #[test]
1714 fn recovered_ssa_maps_to_private_key_event_with_correct_secret_and_id() -> anyhow::Result<()> {
1715 let cfg = SsaGeneratorConfig {
1716 polynomials_per_ssa: 2,
1717 threshold: 2,
1718 surplus_shares: 0,
1719 };
1720 let generator = SsaShareGenerator::<HoprPixSpec>::new(cfg);
1721 let reconstructor = SsaReconstructor::<HoprPixSpec>::new(SsaReconstructorConfig {
1722 early_recovery_threshold: 1.0,
1723 ..Default::default()
1724 });
1725
1726 let pseudonym = SimplePseudonym::random();
1727 let peer = OffchainKeypair::random();
1728 let ssa_id = SsaId::new(pseudonym, SsaIndex::MIN);
1729
1730 let client = generator.new_ssa_commitment(&pseudonym, SsaIndex::MIN)?;
1731 let server_commitment = reconstructor.new_exit_commitment(ssa_id, 2, 2)?;
1732 let expected_addr = HoprPixSpec::group_to_deposit_address(client.ssa_commitment + server_commitment)
1733 .ok_or_else(|| anyhow::anyhow!("deposit address"))?;
1734 client.process_into_reconstructor(&reconstructor)?;
1735
1736 let mut acks = Vec::new();
1737 while let Some((msg, share)) = {
1738 let msg = random_bytes::<20>();
1739 generator.next_share(&pseudonym, &msg).map(|v| v.map(|u| (msg, u)))
1740 }? {
1741 let ack = HalfKey::random();
1742 let enc = share.share.encrypt(&share.id, &ack)?;
1743 reconstructor.insert_encrypted_share(
1744 peer.public(),
1745 ack.to_challenge()?,
1746 TaggedEncryptedPartialSsaShare::new(pseudonym, &msg, enc)?,
1747 )?;
1748 acks.push(VerifiedAcknowledgement::new(ack, &peer).leak());
1749 }
1750
1751 let resolutions = reconstructor.acknowledge_shares(*peer.public(), acks)?;
1752 let rec = resolutions
1753 .into_iter()
1754 .find_map(|r| r.try_as_recovered_ssa())
1755 .ok_or_else(|| anyhow::anyhow!("expected a RecoveredSsa resolution"))?;
1756
1757 assert_eq!(<HoprPixSpec as PixSpec>::DepositAddress::from(&rec.ssa), expected_addr);
1758
1759 let PixEvent::PrivateKeyRecovered(pk) = recovered_ssa_to_pix_event(&rec) else {
1760 anyhow::bail!("expected PrivateKeyRecovered");
1761 };
1762
1763 assert_eq!(pk.id, (pseudonym, ssa_id.ssa_index()));
1764 assert_eq!(pk.secret.0.as_ref(), rec.ssa.secret().as_ref());
1765
1766 Ok(())
1767 }
1768}
1769
1770#[cfg(test)]
1771mod tests {
1772 use std::sync::{
1773 Arc,
1774 atomic::{AtomicU64, Ordering},
1775 };
1776
1777 use futures::StreamExt;
1778 use hopr_utils::runtime::prelude::{consume_budget, spawn, yield_now};
1779
1780 #[tokio::test(flavor = "current_thread")]
1811 async fn sessions_management_dispatch_loop_yields_to_scheduler() {
1812 const N: usize = 1_000;
1814
1815 let counter = Arc::new(AtomicU64::new(0));
1817 let counter_clone = counter.clone();
1818 let canary = spawn(async move {
1819 loop {
1820 counter_clone.fetch_add(1, Ordering::Relaxed);
1821 yield_now().await;
1822 }
1823 });
1824
1825 let unrelated_count = futures::stream::iter(0..N)
1828 .filter_map(|_item| {
1829 let result: Option<()> = None; async move {
1832 consume_budget().await;
1833 result
1834 }
1835 })
1836 .fold(0u64, |acc, _| async move { acc + 1 })
1837 .await;
1838
1839 assert_eq!(
1840 unrelated_count, 0,
1841 "all Processed items should be filtered out by filter_map"
1842 );
1843
1844 let progress = counter.load(Ordering::Relaxed);
1848 assert!(
1849 progress > 0,
1850 "canary task made no forward progress during the {N}-item dispatch run (counter = {progress}); the \
1851 dispatch loop is monopolizing the executor thread — ensure filter_map returns `async move {{ \
1852 consume_budget().await; result }}` rather than `future::ready(result)`"
1853 );
1854
1855 canary.abort();
1856 canary.await.ok();
1857 }
1858}