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