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};
44pub use hopr_api::{
45 Multiaddr, PeerId,
46 network::{Health, traits::NetworkView},
47 types::{
48 crypto::{
49 keypairs::{ChainKeypair, Keypair, OffchainKeypair},
50 types::{HalfKeyChallenge, Hash, OffchainPublicKey},
51 },
52 internal::{prelude::HoprPseudonym, routing::RoutingOptions},
53 },
54};
55use hopr_api::{
56 chain::{ChainKeyOperations, ChainReadAccountOperations, ChainReadChannelOperations, ChainValues},
57 ct::{CoverTrafficGeneration, ProbingTrafficGeneration},
58 graph::{NetworkGraphUpdate, NetworkGraphView, traits::EdgeObservableRead},
59 network::{BoxedProcessFn, NetworkStreamControl},
60 types::primitive::prelude::*,
61};
62pub use hopr_protocol_app::prelude::{ApplicationData, ApplicationDataIn, ApplicationDataOut, Tag};
63pub use hopr_protocol_hopr::{MemorySurbStore, SurbStore};
64pub use hopr_transport_probe::{NeighborTelemetry, PathTelemetry, errors::ProbeError, ping::PingQueryReplier};
65use hopr_transport_probe::{
66 Probe,
67 ping::{PingConfig, Pinger},
68};
69pub use hopr_transport_session as session;
70pub use hopr_transport_session::{
71 Capabilities as SessionCapabilities, Capability as SessionCapability, FlowControlConfig, HoprSession,
72 IncomingSession, SESSION_MTU, SURB_SIZE, ServiceId, SessionClientConfig, SessionId, SessionTarget,
73 SurbBalancerConfig,
74 errors::{SessionManagerError, TransportSessionError},
75};
76use hopr_transport_session::{DispatchResult, SessionManager, SessionManagerConfig};
77#[cfg(feature = "telemetry")]
78pub use hopr_transport_session::{SessionAckMode, SessionLifecycleState};
79#[cfg(feature = "runtime-tokio")]
80pub use hopr_transport_session::{transfer_session, transfer_session_datagram};
81pub use hopr_transport_tag_allocator::TagAllocatorConfig;
82use hopr_utils::{
83 network_types::{
84 crossfire_sink::{CrossfireSink, bounded_sink_channel},
85 prelude::*,
86 },
87 runtime::AbortableList,
88};
89pub use multiaddr::Protocol;
90use tracing::{debug, warn};
91
92#[cfg(feature = "runtime-tokio")]
93use crate::path::BackgroundPathCacheRefreshable;
94pub use crate::{config::HoprProtocolConfig, protocol::PeerProtocolCounterRegistry};
95use crate::{
96 constants::SESSION_INITIATION_TIMEOUT_BASE,
97 errors::HoprTransportError,
98 multiaddrs::strip_p2p_protocol,
99 path::{HoprGraphPathSelector, PathPlanner},
100 pipeline::HoprPacketPipelineBuilder,
101 socket::HoprSocket,
102};
103
104pub const APPLICATION_TAG_RANGE: std::ops::Range<Tag> = Tag::APPLICATION_TAG_RANGE;
105
106pub use hopr_api as api;
107use hopr_api::{
108 chain::{ChainReadTicketOperations, ChainWriteTicketOperations},
109 tickets::TicketFactory,
110 types::internal::routing::DestinationRouting,
111};
112
113lazy_static::lazy_static! {
115 static ref SESSION_INITIATION_TIMEOUT_MAX: Duration = 2 * SESSION_INITIATION_TIMEOUT_BASE * RoutingOptions::MAX_INTERMEDIATE_HOPS as u32;
116
117 static ref PEER_ID_CACHE: moka::sync::Cache<PeerId, OffchainPublicKey> = moka::sync::Cache::builder()
118 .time_to_idle(Duration::from_mins(15))
119 .max_capacity(10_000)
120 .build();
121
122 static ref RANDOM_DATA: [u8; 400] = hopr_api::types::crypto_random::random_bytes();
123}
124
125pub fn peer_id_to_public_key(peer_id: &PeerId) -> crate::errors::Result<OffchainPublicKey> {
130 PEER_ID_CACHE
131 .try_get_with_by_ref(peer_id, move || {
132 OffchainPublicKey::from_peerid(peer_id).map_err(|e| e.into())
133 })
134 .map_err(|e: Arc<HoprTransportError>| {
135 crate::errors::HoprTransportError::Other(anyhow::anyhow!(
136 "failed to convert peer_id ({:?}) to an offchain public key: {e}",
137 peer_id
138 ))
139 })
140}
141
142#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, strum::Display)]
143pub enum HoprTransportProcess {
144 #[strum(to_string = "component responsible for the transport medium (libp2p swarm)")]
145 Medium,
146 #[strum(to_string = "HOPR packet pipeline ({0})")]
147 Pipeline(protocol::PacketPipelineProcesses),
148 #[strum(to_string = "session manager sub-process #{0}")]
149 SessionsManagement(usize),
150 #[strum(to_string = "network probing sub-process: {0}")]
151 Probing(hopr_transport_probe::HoprProbeProcess),
152 #[cfg(feature = "runtime-tokio")]
153 #[strum(to_string = "path cache refresh")]
154 PathRefresh,
155 #[strum(to_string = "sync of outgoing ticket indices")]
156 OutgoingIndexSync,
157 #[strum(to_string = "periodic protocol counter flush")]
158 CounterFlush,
159 SurbFlush,
161 #[strum(to_string = "mixer→wire forwarder")]
162 MixerForwarder,
163 #[cfg(feature = "capture")]
164 #[strum(to_string = "packet capture")]
165 Capture,
166}
167
168type HoprSessionManager = SessionManager<CrossfireSink<(DestinationRouting, ApplicationDataOut)>>;
170
171#[derive(Debug, Clone)]
176pub struct HoprSessionConfigurator {
177 id: SessionId,
178 smgr: std::sync::Weak<HoprSessionManager>,
180}
181
182impl HoprSessionConfigurator {
183 pub fn id(&self) -> &SessionId {
185 &self.id
186 }
187
188 pub async fn ping(&self) -> errors::Result<()> {
196 Ok(self
197 .smgr
198 .upgrade()
199 .ok_or(HoprTransportError::Other(anyhow::anyhow!("session manager is dropped")))?
200 .ping_session(&self.id)
201 .await?)
202 }
203
204 pub fn get_surb_balancer_config(&self) -> errors::Result<Option<SurbBalancerConfig>> {
210 Ok(self
211 .smgr
212 .upgrade()
213 .ok_or(HoprTransportError::Other(anyhow::anyhow!("session manager is dropped")))?
214 .get_surb_balancer_config(&self.id)?)
215 }
216
217 pub fn update_surb_balancer_config(&self, config: SurbBalancerConfig) -> errors::Result<()> {
222 Ok(self
223 .smgr
224 .upgrade()
225 .ok_or(HoprTransportError::Other(anyhow::anyhow!("session manager is dropped")))?
226 .update_surb_balancer_config(&self.id, config)?)
227 }
228
229 pub fn close(&self) -> bool {
236 match self.smgr.upgrade() {
237 Some(smgr) => smgr.close_session(&self.id),
238 None => false,
239 }
240 }
241}
242
243pub struct HoprTransport<Chain, Graph, Net> {
246 packet_key: OffchainKeypair,
247 chain_key: ChainKeypair,
248 chain_api: Chain,
249 ping: Arc<OnceLock<Pinger>>,
250 network: Arc<OnceLock<Net>>,
251 graph: Graph,
252 path_planner: PathPlanner<MemorySurbStore, Chain, HoprGraphPathSelector<Graph>>,
253 my_multiaddresses: Vec<Multiaddr>,
254 smgr: Arc<HoprSessionManager>,
255 session_telemetry_tag_allocator: Arc<dyn hopr_transport_tag_allocator::TagAllocator + Send + Sync>,
256 probing_tag_allocator: Arc<dyn hopr_transport_tag_allocator::TagAllocator + Send + Sync>,
257 counters: PeerProtocolCounterRegistry,
258 cfg: HoprProtocolConfig,
259}
260
261impl<Chain, Graph, Net> HoprTransport<Chain, Graph, Net>
262where
263 Chain: ChainReadChannelOperations
264 + ChainReadAccountOperations
265 + ChainWriteTicketOperations
266 + ChainKeyOperations
267 + ChainReadTicketOperations
268 + ChainValues
269 + Clone
270 + Send
271 + Sync
272 + 'static,
273 Graph: NetworkGraphView<NodeId = OffchainPublicKey>
274 + NetworkGraphUpdate
275 + hopr_api::graph::NetworkGraphWrite<NodeId = OffchainPublicKey>
276 + hopr_api::graph::NetworkGraphTraverse<NodeId = OffchainPublicKey>
277 + Clone
278 + Send
279 + Sync
280 + 'static,
281 <Graph as NetworkGraphView>::Observed: hopr_api::graph::traits::EdgeObservableRead + Send,
282 <Graph as hopr_api::graph::NetworkGraphTraverse>::Observed:
283 hopr_api::graph::traits::EdgeObservableRead + Send + 'static,
284 <Graph as hopr_api::graph::NetworkGraphWrite>::Observed: hopr_api::graph::traits::EdgeObservableWrite + Send,
285 Net: NetworkView + NetworkStreamControl + Clone + Send + Sync + 'static,
286{
287 pub fn new(
288 identity: (&ChainKeypair, &OffchainKeypair),
289 resolver: Chain,
290 graph: Graph,
291 my_multiaddresses: Vec<Multiaddr>,
292 cfg: HoprProtocolConfig,
293 ) -> errors::Result<Self> {
294 let me_offchain = *identity.1.public();
295 let planner_config = cfg.path_planner;
296 let selector = HoprGraphPathSelector::new(
297 me_offchain,
298 graph.clone(),
299 planner_config.max_cached_paths,
300 planner_config.edge_penalty,
301 planner_config.min_ack_rate,
302 planner_config.min_paths_anonymity_floor,
303 );
304
305 let tag_allocators = hopr_transport_tag_allocator::create_allocators_from_config(&cfg.session.tag_allocator)?;
306
307 let mut session_telemetry_tag_allocator = None;
308 let mut probing_tag_allocator = None;
309 for (usage, alloc) in tag_allocators {
310 match usage {
311 hopr_transport_tag_allocator::Usage::Session => {}
313 hopr_transport_tag_allocator::Usage::SessionTerminalTelemetry => {
314 session_telemetry_tag_allocator = Some(alloc)
315 }
316 hopr_transport_tag_allocator::Usage::ProvingTelemetry => probing_tag_allocator = Some(alloc),
317 }
318 }
319 let session_telemetry_tag_allocator = session_telemetry_tag_allocator
320 .ok_or_else(|| HoprTransportError::Api("session telemetry tag allocator missing".into()))?;
321 let probing_tag_allocator =
322 probing_tag_allocator.ok_or_else(|| HoprTransportError::Api("probing tag allocator missing".into()))?;
323
324 let surb_store_cfg = hopr_protocol_hopr::SurbStoreConfig {
332 pseudonyms_lifetime: std::env::var("HOPR_INTERNAL_SURB_PSEUDONYM_LIFETIME_MS")
333 .ok()
334 .and_then(|s| s.trim().parse::<u64>().ok())
335 .map(Duration::from_millis)
336 .map(|d| d.max(hopr_protocol_hopr::MINIMUM_SURB_LIFETIME))
337 .unwrap_or(cfg.packet.surb_store.pseudonyms_lifetime),
338 ..cfg.packet.surb_store
339 };
340
341 let path_planner = PathPlanner::new(
344 me_offchain,
345 MemorySurbStore::new(surb_store_cfg),
346 resolver.clone(),
347 selector,
348 planner_config,
349 );
350
351 Ok(Self {
352 packet_key: identity.1.clone(),
353 chain_key: identity.0.clone(),
354 ping: Arc::new(OnceLock::new()),
355 network: Arc::new(OnceLock::new()),
356 graph,
357 path_planner,
358 my_multiaddresses,
359 smgr: Arc::new(SessionManager::new(SessionManagerConfig {
360 frame_mtu: std::env::var("HOPR_SESSION_FRAME_SIZE")
361 .ok()
362 .and_then(|s| s.parse::<usize>().ok())
363 .unwrap_or_else(|| SessionManagerConfig::default().frame_mtu)
364 .max(SESSION_MTU),
365 max_frame_timeout: std::env::var("HOPR_SESSION_FRAME_TIMEOUT_MS")
366 .ok()
367 .and_then(|s| s.parse::<u64>().ok().map(Duration::from_millis))
368 .unwrap_or_else(|| SessionManagerConfig::default().max_frame_timeout)
369 .max(Duration::from_millis(100)),
370 max_frames_behind_gap: cfg.session.max_frames_behind_gap,
371 max_buffered_segments: std::env::var("HOPR_SESSION_MAX_BUFFERED_SEGMENTS")
372 .ok()
373 .and_then(|s| s.parse::<usize>().ok())
374 .unwrap_or_else(|| SessionManagerConfig::default().max_buffered_segments),
375 initiation_timeout_base: SESSION_INITIATION_TIMEOUT_BASE,
376 idle_timeout: cfg.session.idle_timeout,
377 balancer_sampling_interval: cfg.session.balancer_sampling_interval,
378 initial_return_session_egress_rate: 10,
379 minimum_surb_buffer_duration: cfg.session.balancer_minimum_surb_buffer_duration,
380 maximum_surb_buffer_size: cfg.packet.surb_store.rb_capacity,
381 surb_balance_notify_period: std::env::var("HOPR_SESSION_SURB_BALANCE_NOTIFY_PERIOD_MS")
384 .ok()
385 .and_then(|s| s.parse::<u64>().ok())
386 .map(|ms| Some(Duration::from_millis(ms)))
387 .unwrap_or(cfg.session.surb_balance_notify_period),
388 surb_target_notify: true,
389 maximum_sessions: cfg.session.maximum_managed_sessions,
390 ..Default::default()
391 })),
392 chain_api: resolver,
393 session_telemetry_tag_allocator,
394 probing_tag_allocator,
395 counters: PeerProtocolCounterRegistry::default(),
396 cfg,
397 })
398 }
399
400 pub async fn run_relay<T, TFact, Ct>(
406 &self,
407 cover_traffic: Ct,
408 network: Net,
409 network_process: BoxedProcessFn,
410 ticket_events: T,
411 ticket_factory: TFact,
412 on_incoming_session: Sender<IncomingSession>,
413 ) -> errors::Result<(
414 HoprSocket<
415 futures::stream::BoxStream<'static, ApplicationDataIn>,
416 CrossfireSink<(DestinationRouting, ApplicationDataOut)>,
417 >,
418 AbortableList<HoprTransportProcess>,
419 )>
420 where
421 T: futures::Sink<hopr_api::node::TicketEvent> + Clone + Send + Unpin + 'static,
422 T::Error: std::error::Error + Clone + Send,
423 Ct: ProbingTrafficGeneration + CoverTrafficGeneration + Send + Sync + 'static,
424 TFact: TicketFactory + Clone + Send + Sync + 'static,
425 {
426 self.run_inner(
427 protocol::NodeType::Relay,
428 cover_traffic,
429 network,
430 network_process,
431 ticket_events,
432 ticket_factory,
433 Some(on_incoming_session),
434 )
435 .await
436 }
437
438 pub async fn run_exit<TFact, Ct>(
443 &self,
444 cover_traffic: Ct,
445 network: Net,
446 network_process: BoxedProcessFn,
447 ticket_factory: TFact,
448 on_incoming_session: Sender<IncomingSession>,
449 ) -> errors::Result<(
450 HoprSocket<
451 futures::stream::BoxStream<'static, ApplicationDataIn>,
452 CrossfireSink<(DestinationRouting, ApplicationDataOut)>,
453 >,
454 AbortableList<HoprTransportProcess>,
455 )>
456 where
457 Ct: ProbingTrafficGeneration + CoverTrafficGeneration + Send + Sync + 'static,
458 TFact: TicketFactory + Clone + Send + Sync + 'static,
459 {
460 self.run_inner(
461 protocol::NodeType::Exit,
462 cover_traffic,
463 network,
464 network_process,
465 futures::sink::drain(),
466 ticket_factory,
467 Some(on_incoming_session),
468 )
469 .await
470 }
471
472 pub async fn run_entry<TFact, Ct>(
478 &self,
479 cover_traffic: Ct,
480 network: Net,
481 network_process: BoxedProcessFn,
482 ticket_factory: TFact,
483 ) -> errors::Result<(
484 HoprSocket<
485 futures::stream::BoxStream<'static, ApplicationDataIn>,
486 CrossfireSink<(DestinationRouting, ApplicationDataOut)>,
487 >,
488 AbortableList<HoprTransportProcess>,
489 )>
490 where
491 Ct: ProbingTrafficGeneration + CoverTrafficGeneration + Send + Sync + 'static,
492 TFact: TicketFactory + Clone + Send + Sync + 'static,
493 {
494 self.run_inner(
495 protocol::NodeType::Entry,
496 cover_traffic,
497 network,
498 network_process,
499 futures::sink::drain(),
500 ticket_factory,
501 None,
502 )
503 .await
504 }
505
506 #[allow(clippy::too_many_arguments)]
513 async fn run_inner<T, TFact, Ct>(
514 &self,
515 role: protocol::NodeType,
516 cover_traffic: Ct,
517 network: Net,
518 network_process: BoxedProcessFn,
519 ticket_events: T,
520 ticket_factory: TFact,
521 on_incoming_session: Option<Sender<IncomingSession>>,
522 ) -> errors::Result<(
523 HoprSocket<
524 futures::stream::BoxStream<'static, ApplicationDataIn>,
525 CrossfireSink<(DestinationRouting, ApplicationDataOut)>,
526 >,
527 AbortableList<HoprTransportProcess>,
528 )>
529 where
530 T: futures::Sink<hopr_api::node::TicketEvent> + Clone + Send + Unpin + 'static,
531 T::Error: std::error::Error + Clone + Send,
532 Ct: ProbingTrafficGeneration + CoverTrafficGeneration + Send + Sync + 'static,
533 TFact: TicketFactory + Clone + Send + Sync + 'static,
534 {
535 let mut processes = AbortableList::<HoprTransportProcess>::default();
536
537 let (unresolved_routing_msg_tx, unresolved_routing_msg_rx) =
538 bounded_sink_channel::<(DestinationRouting, ApplicationDataOut)>(MAXIMUM_MSG_OUTGOING_BUFFER_SIZE);
539
540 let transport_network = network;
543 let transport_layer_process = network_process;
544
545 let msg_codec = crate::protocol::HoprBinaryCodec {};
546 let (wire_msg_tx, wire_msg_rx) =
547 protocol::stream::process_stream_protocol(msg_codec, transport_network.clone(), self.cfg.stream).await?;
548
549 let mut mixer_cfg = self.cfg.mixer;
554 mixer_cfg.metric_delay_window = u64::try_from(5 * mixer_cfg.delay_range.as_millis())
555 .unwrap_or(u64::MAX)
556 .max(1);
557 let (mixing_channel_tx, mix_rx) = hopr_transport_mixer::channel(mixer_cfg);
558 let transit_latency_cfg = self.cfg.transit_latency;
559 processes.insert(
560 HoprTransportProcess::MixerForwarder,
561 hopr_utils::spawn_as_abortable!(async move {
562 let mut mix_rx = mix_rx;
563 let mut wire_sink = wire_msg_tx;
564
565 if let Some(lat) = transit_latency_cfg {
566 #[cfg(feature = "runtime-tokio")]
572 {
573 let (tx, rx) = futures::channel::mpsc::unbounded();
574 let fan_out = async move {
575 while let Some(item) = futures::StreamExt::next(&mut mix_rx).await {
576 let mean_us = lat.mean.as_micros() as f64;
577 let std_us = lat.std_dev.as_micros() as f64;
578 let delay_us = if std_us > 0.0 {
579 use rand_distr::{Distribution, Normal};
580 Normal::new(mean_us, std_us)
581 .expect("transit latency Normal params are valid")
582 .sample(&mut rand::rng())
583 .max(0.0_f64)
584 } else {
585 mean_us.max(0.0)
586 };
587 let delay = Duration::from_micros(delay_us as u64);
588 let item_tx = tx.clone();
589 hopr_utils::runtime::prelude::spawn(async move {
590 if !delay.is_zero() {
591 futures_timer::Delay::new(delay).await;
592 }
593 let _ = item_tx.unbounded_send(item);
594 });
595 }
596 };
598 let fan_in = async move {
599 let mut rx = rx;
600 while let Some(item) = futures::StreamExt::next(&mut rx).await {
601 if wire_sink.send(item).await.is_err() {
602 tracing::error!(
603 task = %HoprTransportProcess::MixerForwarder,
604 "wire sink dropped — discarding transit-delayed packet"
605 );
606 break;
607 }
608 }
609 };
610 futures::join!(fan_out, fan_in);
611 }
612 #[cfg(not(feature = "runtime-tokio"))]
613 {
614 let _ = lat;
615 while let Some(item) = futures::StreamExt::next(&mut mix_rx).await {
616 if wire_sink.send(item).await.is_err() {
617 tracing::error!(
618 task = %HoprTransportProcess::MixerForwarder,
619 "wire sink dropped — discarding mixed packet"
620 );
621 }
622 }
623 }
624 } else {
625 while let Some(item) = futures::StreamExt::next(&mut mix_rx).await {
626 if wire_sink.send(item).await.is_err() {
627 tracing::error!(
628 task = %HoprTransportProcess::MixerForwarder,
629 "wire sink dropped — discarding mixed packet"
630 );
631 }
632 }
633 }
634
635 tracing::warn!(
636 task = %HoprTransportProcess::MixerForwarder,
637 "long-running background task finished"
638 );
639 }),
640 );
641
642 #[cfg(feature = "runtime-tokio")]
644 processes.insert(
645 HoprTransportProcess::PathRefresh,
646 hopr_utils::spawn_as_abortable!(self.path_planner.run_background_refresh()),
647 );
648
649 processes.insert(
650 HoprTransportProcess::Medium,
651 hopr_utils::spawn_as_abortable!(transport_layer_process().inspect(|_| tracing::warn!(
652 task = %HoprTransportProcess::Medium,
653 "long-running background task finished"
654 ))),
655 );
656
657 let msg_protocol_bidirectional_channel_capacity =
658 std::env::var("HOPR_INTERNAL_PROTOCOL_BIDIRECTIONAL_CHANNEL_CAPACITY")
659 .ok()
660 .and_then(|s| s.trim().parse::<usize>().ok())
661 .filter(|&c| c > 0)
662 .unwrap_or(16_384);
663
664 debug!(
665 capacity = msg_protocol_bidirectional_channel_capacity,
666 "creating protocol bidirectional channel"
667 );
668 let (tx_from_protocol, rx_from_protocol) =
669 bounded_sink_channel::<(HoprPseudonym, ApplicationDataIn)>(msg_protocol_bidirectional_channel_capacity);
670
671 let cover_traffic_allocated_tag = self
675 .session_telemetry_tag_allocator
676 .allocate()
677 .ok_or_else(|| HoprTransportError::Api("failed to allocate cover traffic tag".into()))?;
678 let cover_traffic_tag: Tag = cover_traffic_allocated_tag.value().into();
679
680 let rx_from_protocol = rx_from_protocol.filter_map(move |(pseudonym, data)| {
683 let _keep_alive = &cover_traffic_allocated_tag;
684 async move { (data.data.application_tag != cover_traffic_tag).then_some((pseudonym, data)) }
685 });
686
687 let cover_traffic_stream = CoverTrafficGeneration::build(&cover_traffic).filter_map(move |routing| {
689 let start =
690 hopr_api::types::crypto_random::random_integer(0, Some((RANDOM_DATA.len() - 100) as u64)) as usize;
691 let data = &RANDOM_DATA[start..start + 100];
692
693 futures::future::ready(if let Ok(data) = ApplicationData::new(cover_traffic_tag, data) {
694 Some((routing, ApplicationDataOut::with_no_packet_info(data)))
695 } else {
696 tracing::error!("failed to construct cover traffic packet");
697 None
698 })
699 });
700
701 let merged_unresolved_output_data =
703 select_with_strategy(unresolved_routing_msg_rx, cover_traffic_stream, |_: &mut ()| {
704 futures::stream::PollNext::Left
705 });
706
707 let path_planner = self.path_planner.clone();
714 let distress_threshold = self.cfg.packet.surb_store.distress_threshold;
715 let routing_concurrency = {
716 let avail = std::thread::available_parallelism()
717 .ok()
718 .map(|n| n.get())
719 .unwrap_or(1)
720 .max(1)
721 * 8;
722 self.cfg
723 .packet
724 .pipeline
725 .output_concurrency
726 .filter(|&n| n > 0)
727 .unwrap_or(avail)
728 };
729 let all_resolved_external_msg_rx = crate::path::resolve::resolve_routing_stage(
730 merged_unresolved_output_data,
731 move |size_hint, max_surbs, unresolved| {
732 let path_planner = path_planner.clone();
733 async move { path_planner.resolve_routing(size_hint, max_surbs, unresolved).await }
734 },
735 distress_threshold,
736 routing_concurrency,
737 crate::path::resolve::surb_resolution_wait(self.cfg.packet.pipeline.surb_resolution_wait),
738 );
739
740 let channels_dst = self
741 .chain_api
742 .domain_separators()
743 .await
744 .map_err(HoprTransportError::chain)?
745 .channel;
746
747 let surb_round_trips = protocol::surb_telemetry::SurbRoundTripRegistry::default();
748
749 let pipeline_builder = HoprPacketPipelineBuilder::new()
750 .identity((&self.chain_key, &self.packet_key))
751 .transport((mixing_channel_tx, wire_msg_rx))
752 .api((tx_from_protocol, all_resolved_external_msg_rx))
753 .surb_store(self.path_planner.surb_store.clone())
754 .chain_api(self.chain_api.clone())
755 .ticket_factory(ticket_factory)
756 .channels_dst(channels_dst)
757 .with_counters(self.counters.clone())
758 .with_surb_telemetry(
759 surb_round_trips.clone(),
760 protocol::surb_telemetry::path_slots_of(self.graph.clone()),
761 )
762 .with_config(self.cfg.packet);
763
764 let pipeline_processes = match role {
765 protocol::NodeType::Relay => pipeline_builder.with_ticket_events(ticket_events).build_for_relay(),
766 protocol::NodeType::Exit => pipeline_builder.build_for_exit(),
767 protocol::NodeType::Entry => pipeline_builder.build_for_entry(),
768 };
769 processes.extend_from(pipeline_processes);
770
771 let flush_counters = self.counters.clone();
773 let flush_graph = self.graph.clone();
774 let flush_me = *self.packet_key.public();
775 let flush_interval = self.cfg.counter_flush_interval;
776 processes.insert(
777 HoprTransportProcess::CounterFlush,
778 hopr_utils::spawn_as_abortable!(async move {
779 use hopr_api::graph::traits::{EdgeObservableWrite, EdgeWeightType};
780
781 futures_time::stream::interval(futures_time::time::Duration::from(flush_interval))
782 .for_each(|_| {
783 for (peer, num_packets, num_acks) in flush_counters.drain() {
784 tracing::trace!(
785 %peer,
786 num_packets,
787 num_acks,
788 "flushing protocol conformance counters"
789 );
790 flush_graph.upsert_edge(&flush_me, &peer, |obs| {
791 obs.record(EdgeWeightType::ImmediateProtocolConformance { num_packets, num_acks });
792 });
793 }
794 futures::future::ready(())
795 })
796 .await;
797 }),
798 );
799
800 tracing::info!(?role, "starting surb round-trip flush task");
802 const RETURN_PATH_DEGRADED_GRACE: std::time::Duration = std::time::Duration::from_secs(10);
806 let surb_flush_graph = self.graph.clone();
807 let surb_flush_interval = self.cfg.surb_flush_interval;
808 let surb_flush_smgr = self.smgr.clone();
809 let surb_flush_chain = self.chain_api.clone();
810 let surb_flush_planner = self.path_planner.clone();
811 processes.insert(
812 HoprTransportProcess::SurbFlush,
813 hopr_utils::spawn_as_abortable!(async move {
814 let mut episodes = protocol::return_path_recovery::ReturnPathEpisodes::new(RETURN_PATH_DEGRADED_GRACE);
815 let mut ticks = futures_time::stream::interval(futures_time::time::Duration::from(surb_flush_interval));
816
817 while ticks.next().await.is_some() {
818 let (planner, chain, smgr) = (&surb_flush_planner, &surb_flush_chain, &surb_flush_smgr);
826 let now_ms = hopr_utils::platform::time::native::current_time()
827 .as_unix_timestamp()
828 .as_millis();
829 for step in protocol::return_path_recovery::run_flush_tick(
830 &surb_round_trips,
831 &surb_flush_graph,
832 now_ms,
833 &mut episodes,
834 |destination| async move { planner.recompute_paths_from(&destination).await },
835 |destination| async move {
836 match chain.packet_key_to_chain_key(&destination) {
846 Ok(Some(address)) => smgr.mark_return_path_degraded(
847 &hopr_api::types::internal::prelude::NodeId::Chain(address),
848 RETURN_PATH_DEGRADED_GRACE,
849 ),
850 Err(error) => {
854 tracing::warn!(%destination, %error, "could not resolve a silent destination's chain key to mark it degraded");
855 0
856 }
857 Ok(None) => 0,
858 }
859 },
860 )
861 .await
862 {
863 match step {
864 protocol::return_path_recovery::RecoveryStep::Replanned { destination, moved } => {
865 tracing::info!(%destination, entries = moved, "return path silent, re-planned")
866 }
867 protocol::return_path_recovery::RecoveryStep::Refilled { destination, sessions } => {
868 tracing::info!(%destination, sessions, "refilling behind the re-plan")
869 }
870 }
871 }
872 }
873 }),
874 );
875
876 let manual_ping_channel_capacity = std::env::var("HOPR_INTERNAL_MANUAL_PING_CHANNEL_CAPACITY")
878 .ok()
879 .and_then(|s| s.trim().parse::<usize>().ok())
880 .filter(|&c| c > 0)
881 .unwrap_or(128);
882 debug!(capacity = manual_ping_channel_capacity, "Creating manual ping channel");
883 let (manual_ping_tx, manual_ping_rx_raw) =
884 crossfire::mpsc::bounded_async::<(OffchainPublicKey, PingQueryReplier)>(manual_ping_channel_capacity);
885 let manual_ping_rx = manual_ping_rx_raw.into_stream();
886
887 let probe = Probe::new(self.cfg.probe, self.probing_tag_allocator.clone());
888
889 let (probing_processes, probe_classifier) = probe
890 .continuously_scan(
891 unresolved_routing_msg_tx.clone(),
892 manual_ping_rx,
893 cover_traffic,
894 self.graph.clone(),
895 )
896 .await;
897
898 processes.flat_map_extend_from(probing_processes, HoprTransportProcess::Probing);
899
900 self.ping
902 .clone()
903 .set(Pinger::new(
904 PingConfig {
905 timeout: self.cfg.probe.timeout,
906 },
907 manual_ping_tx,
908 ))
909 .map_err(|_| HoprTransportError::Api("must set the ticket aggregation writer only once".into()))?;
910
911 let smgr_start_res = if role != protocol::NodeType::Entry {
913 self.smgr.start(
915 unresolved_routing_msg_tx.clone(),
916 on_incoming_session.ok_or_else(|| {
917 HoprTransportError::Api("on_incoming_session channel is required for relay/exit nodes".into())
918 })?,
919 )
920 } else {
921 self.smgr
923 .start(unresolved_routing_msg_tx.clone(), futures::sink::drain())
924 };
925
926 smgr_start_res
927 .map_err(|_| HoprTransportError::Api("failed to start session manager".into()))?
928 .into_iter()
929 .enumerate()
930 .map(|(i, jh)| (HoprTransportProcess::SessionsManagement(i + 1), jh))
931 .for_each(|(k, v)| {
932 processes.insert(k, v);
933 });
934
935 let (on_incoming_data_tx, on_incoming_data_rx) =
944 bounded_sink_channel::<ApplicationDataIn>(msg_protocol_bidirectional_channel_capacity);
945 let smgr = self.smgr.clone();
946 let unresolved_routing_msg_tx_for_task = unresolved_routing_msg_tx.clone();
947 processes.insert(
948 HoprTransportProcess::SessionsManagement(0),
949 hopr_utils::spawn_as_abortable!(async move {
950 probe_classifier
951 .filter_stream(unresolved_routing_msg_tx_for_task, rx_from_protocol)
952 .filter_map(move |(pseudonym, data)| {
953 hopr_transport_session::counters::DISPATCH_MESSAGE_CALLS
954 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
955 let result = match smgr.dispatch_message(pseudonym, data) {
967 Ok(DispatchResult::Processed) => {
968 tracing::trace!("message dispatch completed");
969 None
970 }
971 Ok(DispatchResult::Unrelated(data)) => {
972 tracing::trace!("unrelated message dispatch completed");
973 Some(data)
974 }
975 Ok(DispatchResult::Dropped(reason)) => {
980 tracing::trace!(?reason, "dropped session packet");
981 None
982 }
983 Err(error) => {
984 tracing::error!(%error, "error while dispatching packet in the session manager");
985 None
986 }
987 };
988 async move {
989 hopr_utils::runtime::prelude::consume_budget().await;
990 result
991 }
992 })
993 .fold(on_incoming_data_tx, |mut tx, data| async move {
994 if tx.send(data).await.is_err() {
995 tracing::error!(
996 task = %HoprTransportProcess::SessionsManagement(0),
997 "incoming-data channel disconnected — dropping unrelated packet; \
998 HoprSocket must be consumed or drained by the caller"
999 );
1000 }
1001 tx
1002 })
1003 .await;
1004 tracing::warn!(
1005 task = %HoprTransportProcess::SessionsManagement(0),
1006 "long-running background task finished"
1007 );
1008 }),
1009 );
1010
1011 self.network
1013 .clone()
1014 .set(transport_network)
1015 .map_err(|_| HoprTransportError::Api("transport network viewer already set".into()))?;
1016
1017 Ok((
1018 (on_incoming_data_rx.boxed(), unresolved_routing_msg_tx).into(),
1019 processes,
1020 ))
1021 }
1022
1023 #[tracing::instrument(level = "debug", skip(self))]
1024 pub async fn ping(
1025 &self,
1026 peer: &OffchainPublicKey,
1027 ) -> errors::Result<(std::time::Duration, <Graph as NetworkGraphView>::Observed)> {
1028 let me: &OffchainPublicKey = self.packet_key.public();
1029 if peer == me {
1030 return Err(HoprTransportError::Api("ping to self does not make sense".into()));
1031 }
1032
1033 let pinger = self
1034 .ping
1035 .get()
1036 .ok_or_else(|| HoprTransportError::Api("ping processing is not yet initialized".into()))?;
1037
1038 let latency = (*pinger).ping(peer).await?;
1039
1040 if let Some(observations) = self.graph.edge(me, peer) {
1041 Ok((latency, observations))
1042 } else {
1043 Err(HoprTransportError::Api(format!(
1044 "no observations available for peer {peer}",
1045 )))
1046 }
1047 }
1048
1049 #[tracing::instrument(level = "debug", skip(self))]
1050 pub async fn new_session(
1051 &self,
1052 destination: Address,
1053 target: SessionTarget,
1054 cfg: SessionClientConfig,
1055 ) -> errors::Result<(HoprSession, HoprSessionConfigurator)> {
1056 let session = self.smgr.new_session(destination, target, cfg).await?;
1057 let id = *session.id();
1058 Ok((
1059 session,
1060 HoprSessionConfigurator {
1061 id,
1062 smgr: Arc::downgrade(&self.smgr),
1063 },
1064 ))
1065 }
1066
1067 #[tracing::instrument(level = "debug", skip(self))]
1068 pub async fn listening_multiaddresses(&self) -> Vec<Multiaddr> {
1069 self.network
1070 .get()
1071 .ok_or_else(|| HoprTransportError::Api("transport network is not yet initialized".into()))
1072 .map(|network| network.listening_as().into_iter().collect())
1073 .unwrap_or_default()
1074 }
1075
1076 #[tracing::instrument(level = "debug", skip(self))]
1077 pub fn announceable_multiaddresses(&self) -> Vec<Multiaddr> {
1078 let mut mas = self
1079 .local_multiaddresses()
1080 .into_iter()
1081 .filter(|ma| {
1082 crate::multiaddrs::is_supported(ma)
1083 && (self.cfg.transport.announce_local_addresses || is_public_address(ma))
1084 })
1085 .map(|ma| strip_p2p_protocol(&ma))
1086 .filter(|v| !v.is_empty())
1087 .collect::<Vec<_>>();
1088
1089 mas.sort_by(|l, r| {
1090 let is_left_dns = crate::multiaddrs::is_dns(l);
1091 let is_right_dns = crate::multiaddrs::is_dns(r);
1092
1093 if !(is_left_dns ^ is_right_dns) {
1094 std::cmp::Ordering::Equal
1095 } else if is_left_dns {
1096 std::cmp::Ordering::Less
1097 } else {
1098 std::cmp::Ordering::Greater
1099 }
1100 });
1101
1102 mas
1103 }
1104
1105 pub fn graph(&self) -> &Graph {
1107 &self.graph
1108 }
1109
1110 pub fn surb_store(&self) -> &MemorySurbStore {
1115 &self.path_planner.surb_store
1116 }
1117
1118 #[tracing::instrument(level = "debug", skip(self))]
1119 pub fn local_multiaddresses(&self) -> Vec<Multiaddr> {
1120 self.network
1121 .get()
1122 .map(|network| network.listening_as().into_iter().collect())
1123 .unwrap_or_else(|| {
1124 tracing::error!("transport network is not yet initialized, cannot fetch announced multiaddresses");
1125 self.my_multiaddresses.clone()
1126 })
1127 }
1128
1129 #[tracing::instrument(level = "debug", skip(self))]
1130 pub async fn network_observed_multiaddresses(&self, peer: &OffchainPublicKey) -> Vec<Multiaddr> {
1131 match self
1132 .network
1133 .get()
1134 .ok_or_else(|| HoprTransportError::Api("transport network is not yet initialized".into()))
1135 {
1136 Ok(network) => network
1137 .multiaddress_of(&peer.into())
1138 .unwrap_or_default()
1139 .into_iter()
1140 .collect(),
1141 Err(error) => {
1142 tracing::error!(%error, "failed to get observed multiaddresses");
1143 return vec![];
1144 }
1145 }
1146 }
1147
1148 #[tracing::instrument(level = "debug", skip(self))]
1149 pub async fn network_health(&self) -> Health {
1150 self.network
1151 .get()
1152 .ok_or_else(|| HoprTransportError::Api("transport network is not yet initialized".into()))
1153 .map(|network| network.health())
1154 .unwrap_or(Health::Red)
1155 }
1156
1157 pub async fn network_connected_peers(&self) -> errors::Result<Vec<OffchainPublicKey>> {
1158 Ok(futures::stream::iter(
1159 self.network
1160 .get()
1161 .ok_or_else(|| {
1162 tracing::error!("transport network is not yet initialized");
1163 HoprTransportError::Api("transport network is not yet initialized".into())
1164 })?
1165 .connected_peers(),
1166 )
1167 .filter_map(|peer_id| async move {
1168 match peer_id_to_public_key(&peer_id) {
1169 Ok(key) => Some(key),
1170 Err(error) => {
1171 tracing::warn!(%peer_id, %error, "failed to convert PeerId to OffchainPublicKey");
1172 None
1173 }
1174 }
1175 })
1176 .collect()
1177 .await)
1178 }
1179
1180 #[tracing::instrument(level = "debug", skip(self))]
1181 pub fn network_peer_observations(&self, peer: &OffchainPublicKey) -> Option<<Graph as NetworkGraphView>::Observed> {
1182 self.graph.edge(self.packet_key.public(), peer)
1183 }
1184
1185 #[tracing::instrument(level = "debug", skip(self))]
1187 pub async fn all_network_peers(
1188 &self,
1189 minimum_score: f64,
1190 ) -> errors::Result<Vec<(OffchainPublicKey, <Graph as NetworkGraphView>::Observed)>> {
1191 let me = self.packet_key.public();
1192 Ok(self
1193 .network_connected_peers()
1194 .await?
1195 .into_iter()
1196 .filter_map(|peer| {
1197 let observation = self.graph.edge(me, &peer);
1198 if let Some(info) = observation {
1199 if info.score().is_some_and(|score| score >= minimum_score) {
1201 Some((peer, info))
1202 } else {
1203 None
1204 }
1205 } else {
1206 None
1207 }
1208 })
1209 .collect::<Vec<_>>())
1210 }
1211}
1212
1213impl<Chain, Graph, Net> NetworkView for HoprTransport<Chain, Graph, Net>
1218where
1219 Net: NetworkView + Send + Sync + 'static,
1220{
1221 fn listening_as(&self) -> std::collections::HashSet<Multiaddr> {
1222 self.network.get().map(|n| n.listening_as()).unwrap_or_default()
1223 }
1224
1225 fn multiaddress_of(&self, peer: &PeerId) -> Option<std::collections::HashSet<Multiaddr>> {
1226 self.network.get()?.multiaddress_of(peer)
1227 }
1228
1229 fn discovered_peers(&self) -> std::collections::HashSet<PeerId> {
1230 self.network.get().map(|n| n.discovered_peers()).unwrap_or_default()
1231 }
1232
1233 fn connected_peers(&self) -> std::collections::HashSet<PeerId> {
1234 self.network.get().map(|n| n.connected_peers()).unwrap_or_default()
1235 }
1236
1237 fn is_connected(&self, peer: &PeerId) -> bool {
1238 self.network.get().map(|n| n.is_connected(peer)).unwrap_or(false)
1239 }
1240
1241 fn health(&self) -> Health {
1242 self.network.get().map(|n| n.health()).unwrap_or(Health::Red)
1243 }
1244
1245 fn subscribe_network_events(
1246 &self,
1247 ) -> impl futures::Stream<Item = hopr_api::network::NetworkEvent> + Send + 'static {
1248 match self.network.get() {
1249 Some(n) => futures::future::Either::Left(n.subscribe_network_events()),
1250 None => futures::future::Either::Right(futures::stream::empty()),
1251 }
1252 }
1253}
1254
1255#[async_trait::async_trait]
1260impl<Chain, Graph, Net> hopr_api::node::TransportOperations for HoprTransport<Chain, Graph, Net>
1261where
1262 Chain: ChainReadChannelOperations
1263 + ChainReadAccountOperations
1264 + hopr_api::chain::ChainWriteTicketOperations
1265 + ChainKeyOperations
1266 + hopr_api::chain::ChainReadTicketOperations
1267 + ChainValues
1268 + Clone
1269 + Send
1270 + Sync
1271 + 'static,
1272 Graph: NetworkGraphView<NodeId = OffchainPublicKey>
1273 + NetworkGraphUpdate
1274 + hopr_api::graph::NetworkGraphWrite<NodeId = OffchainPublicKey>
1275 + hopr_api::graph::NetworkGraphTraverse<NodeId = OffchainPublicKey>
1276 + Clone
1277 + Send
1278 + Sync
1279 + 'static,
1280 <Graph as NetworkGraphView>::Observed: EdgeObservableRead + Send,
1281 <Graph as hopr_api::graph::NetworkGraphTraverse>::Observed: EdgeObservableRead + Send + 'static,
1282 <Graph as hopr_api::graph::NetworkGraphWrite>::Observed: hopr_api::graph::traits::EdgeObservableWrite + Send,
1283 Net: NetworkView + NetworkStreamControl + Clone + Send + Sync + 'static,
1284{
1285 type Error = errors::HoprTransportError;
1286 type Observable = <Graph as NetworkGraphView>::Observed;
1287
1288 async fn ping(&self, key: &OffchainPublicKey) -> Result<(Duration, Self::Observable), Self::Error> {
1289 self.ping(key).await
1290 }
1291
1292 async fn observed_multiaddresses(&self, key: &OffchainPublicKey) -> Vec<Multiaddr> {
1293 self.network_observed_multiaddresses(key).await
1294 }
1295}
1296
1297pub const PACKET_PAYLOAD_SIZE: usize = hopr_crypto_packet::prelude::HoprPacket::PAYLOAD_SIZE;
1299
1300#[cfg(test)]
1301mod tests {
1302 use std::sync::{
1303 Arc,
1304 atomic::{AtomicU64, Ordering},
1305 };
1306
1307 use futures::StreamExt;
1308 use hopr_utils::runtime::prelude::{consume_budget, spawn, yield_now};
1309
1310 #[tokio::test(flavor = "current_thread")]
1341 async fn sessions_management_dispatch_loop_yields_to_scheduler() {
1342 const N: usize = 1_000;
1344
1345 let counter = Arc::new(AtomicU64::new(0));
1347 let counter_clone = counter.clone();
1348 let canary = spawn(async move {
1349 loop {
1350 counter_clone.fetch_add(1, Ordering::Relaxed);
1351 yield_now().await;
1352 }
1353 });
1354
1355 let unrelated_count = futures::stream::iter(0..N)
1358 .filter_map(|_item| {
1359 let result: Option<()> = None; async move {
1362 consume_budget().await;
1363 result
1364 }
1365 })
1366 .fold(0u64, |acc, _| async move { acc + 1 })
1367 .await;
1368
1369 assert_eq!(
1370 unrelated_count, 0,
1371 "all Processed items should be filtered out by filter_map"
1372 );
1373
1374 let progress = counter.load(Ordering::Relaxed);
1378 assert!(
1379 progress > 0,
1380 "canary task made no forward progress during the {N}-item dispatch run (counter = {progress}); the \
1381 dispatch loop is monopolizing the executor thread — ensure filter_map returns `async move {{ \
1382 consume_budget().await; result }}` rather than `future::ready(result)`"
1383 );
1384
1385 canary.abort();
1386 canary.await.ok();
1387 }
1388}