Skip to main content

hopr_transport/
lib.rs

1//! The crate aggregates and composes individual transport level objects and functionality
2//! into a unified [`crate::HoprTransport`] object with the goal of isolating the transport layer
3//! and defining a fully specified transport API.
4//!
5//! See also the `hopr_protocol_start` crate for details on Start sub-protocol which initiates a Session.
6//!
7//! As such, the transport layer components should be only those that are directly necessary to:
8//!
9//! 1. send and receive a packet, acknowledgement or ticket aggregation request
10//! 2. send and receive a network telemetry request
11//! 3. automate transport level processes
12//! 4. algorithms associated with the transport layer operational management
13//! 5. interface specifications to allow modular behavioral extensions
14
15/// Configuration of the [crate::HoprTransport].
16pub mod config;
17/// Constants used and exposed by the crate.
18pub mod constants;
19/// Errors used by the crate.
20pub mod errors;
21/// Graph-based path planning for the HOPR transport layer.
22pub mod path;
23/// Transport binary protocol layer (codec, pipeline, heartbeat, stream).
24pub mod protocol;
25
26mod multiaddrs;
27
28#[cfg(feature = "capture")]
29mod capture;
30mod pipeline;
31pub mod socket;
32
33use std::{
34    sync::{Arc, OnceLock},
35    time::Duration,
36};
37
38use constants::MAXIMUM_MSG_OUTGOING_BUFFER_SIZE;
39use futures::{FutureExt, SinkExt, StreamExt, channel::mpsc::Sender, stream::select_with_strategy};
40pub use hopr_api::{
41    Multiaddr, PeerId,
42    network::{Health, traits::NetworkView},
43    types::{
44        crypto::{
45            keypairs::{ChainKeypair, Keypair, OffchainKeypair},
46            types::{HalfKeyChallenge, Hash, OffchainPublicKey},
47        },
48        internal::{prelude::HoprPseudonym, routing::RoutingOptions},
49    },
50};
51use hopr_api::{
52    chain::{ChainKeyOperations, ChainReadAccountOperations, ChainReadChannelOperations, ChainValues},
53    ct::{CoverTrafficGeneration, ProbingTrafficGeneration},
54    graph::{NetworkGraphUpdate, NetworkGraphView, traits::EdgeObservableRead},
55    network::{BoxedProcessFn, NetworkStreamControl},
56    types::primitive::prelude::*,
57};
58use hopr_crypto_packet::prelude::PacketSignal;
59pub use hopr_protocol_app::prelude::{ApplicationData, ApplicationDataIn, ApplicationDataOut, Tag};
60use hopr_protocol_hopr::MemorySurbStore;
61pub use hopr_transport_probe::{NeighborTelemetry, PathTelemetry, errors::ProbeError, ping::PingQueryReplier};
62use hopr_transport_probe::{
63    Probe,
64    ping::{PingConfig, Pinger},
65};
66pub use hopr_transport_session as session;
67#[cfg(feature = "runtime-tokio")]
68pub use hopr_transport_session::transfer_session;
69pub use hopr_transport_session::{
70    Capabilities as SessionCapabilities, Capability as SessionCapability, HoprSession, IncomingSession, SESSION_MTU,
71    SURB_SIZE, ServiceId, SessionClientConfig, SessionId, SessionTarget, SurbBalancerConfig,
72    errors::{SessionManagerError, TransportSessionError},
73};
74use hopr_transport_session::{DispatchResult, SessionManager, SessionManagerConfig};
75#[cfg(feature = "telemetry")]
76pub use hopr_transport_session::{SessionAckMode, SessionLifecycleState};
77pub use hopr_transport_tag_allocator::TagAllocatorConfig;
78use hopr_utils::{
79    network_types::{
80        crossfire_sink::{CrossfireSink, bounded_sink_channel},
81        prelude::*,
82    },
83    runtime::AbortableList,
84};
85pub use multiaddr::Protocol;
86use rust_stream_ext_concurrent::then_concurrent::StreamThenConcurrentExt;
87use tracing::{Instrument, debug, error, trace, warn};
88
89#[cfg(feature = "runtime-tokio")]
90use crate::path::BackgroundPathCacheRefreshable;
91pub use crate::{config::HoprProtocolConfig, protocol::PeerProtocolCounterRegistry};
92use crate::{
93    constants::SESSION_INITIATION_TIMEOUT_BASE,
94    errors::HoprTransportError,
95    multiaddrs::strip_p2p_protocol,
96    path::{HoprGraphPathSelector, PathPlanner},
97    pipeline::HoprPacketPipelineBuilder,
98    socket::HoprSocket,
99};
100
101pub const APPLICATION_TAG_RANGE: std::ops::Range<Tag> = Tag::APPLICATION_TAG_RANGE;
102
103pub use hopr_api as api;
104use hopr_api::{
105    chain::{ChainReadTicketOperations, ChainWriteTicketOperations},
106    tickets::TicketFactory,
107    types::internal::routing::DestinationRouting,
108};
109
110// Needs lazy-static, since Duration multiplication by a constant is yet not a const-operation.
111lazy_static::lazy_static! {
112    static ref SESSION_INITIATION_TIMEOUT_MAX: Duration = 2 * SESSION_INITIATION_TIMEOUT_BASE * RoutingOptions::MAX_INTERMEDIATE_HOPS as u32;
113
114    static ref PEER_ID_CACHE: moka::sync::Cache<PeerId, OffchainPublicKey> = moka::sync::Cache::builder()
115        .time_to_idle(Duration::from_mins(15))
116        .max_capacity(10_000)
117        .build();
118
119    static ref RANDOM_DATA: [u8; 400] = hopr_api::types::crypto_random::random_bytes();
120}
121
122/// PeerId -> OffchainPublicKey is a CPU-intensive blocking operation.
123///
124/// This helper uses a cached static object to speed up the lookup and avoid blocking the async
125/// runtime on repeated conversions for the same [`PeerId`]s.
126pub fn peer_id_to_public_key(peer_id: &PeerId) -> crate::errors::Result<OffchainPublicKey> {
127    PEER_ID_CACHE
128        .try_get_with_by_ref(peer_id, move || {
129            OffchainPublicKey::from_peerid(peer_id).map_err(|e| e.into())
130        })
131        .map_err(|e: Arc<HoprTransportError>| {
132            crate::errors::HoprTransportError::Other(anyhow::anyhow!(
133                "failed to convert peer_id ({:?}) to an offchain public key: {e}",
134                peer_id
135            ))
136        })
137}
138
139#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, strum::Display)]
140pub enum HoprTransportProcess {
141    #[strum(to_string = "component responsible for the transport medium (libp2p swarm)")]
142    Medium,
143    #[strum(to_string = "HOPR packet pipeline ({0})")]
144    Pipeline(protocol::PacketPipelineProcesses),
145    #[strum(to_string = "session manager sub-process #{0}")]
146    SessionsManagement(usize),
147    #[strum(to_string = "network probing sub-process: {0}")]
148    Probing(hopr_transport_probe::HoprProbeProcess),
149    #[cfg(feature = "runtime-tokio")]
150    #[strum(to_string = "path cache refresh")]
151    PathRefresh,
152    #[strum(to_string = "sync of outgoing ticket indices")]
153    OutgoingIndexSync,
154    #[strum(to_string = "periodic protocol counter flush")]
155    CounterFlush,
156    #[strum(to_string = "mixer→wire forwarder")]
157    MixerForwarder,
158    #[cfg(feature = "capture")]
159    #[strum(to_string = "packet capture")]
160    Capture,
161}
162
163/// HOPR protocol specific instantiation of the SessionManager.
164type HoprSessionManager = SessionManager<CrossfireSink<(DestinationRouting, ApplicationDataOut)>>;
165
166/// Allows configuration of one specific [`HoprSession`].
167///
168/// The configurator does not prevent the Session from being closed
169/// or the Session manager from being dropped.
170#[derive(Debug, Clone)]
171pub struct HoprSessionConfigurator {
172    id: SessionId,
173    // Makes sure configurator does not extend lifetime of the SessionManager.
174    smgr: std::sync::Weak<HoprSessionManager>,
175}
176
177impl HoprSessionConfigurator {
178    /// [`SessionId`] of the session this object can configure.
179    pub fn id(&self) -> &SessionId {
180        &self.id
181    }
182
183    /// Sends a Session Keep-Alive packet over the Session.
184    ///
185    /// NOTE: This usually carries at least 2 SURBs on the HOPR protocol level and can be
186    /// used for manual SURB balancing.
187    ///
188    /// NOTE: This operation only sends the Session Keep-Alive packet and **DOES NOT** guarantee the other side
189    /// has received it.
190    pub async fn ping(&self) -> errors::Result<()> {
191        Ok(self
192            .smgr
193            .upgrade()
194            .ok_or(HoprTransportError::Other(anyhow::anyhow!("session manager is dropped")))?
195            .ping_session(&self.id)
196            .await?)
197    }
198
199    /// Gets the configuration of the SURB balancer.
200    ///
201    /// Returns an error if the Session is closed, the Session manager is gone.
202    ///
203    /// Returns `Ok(None)` if the Session has been created without a SURB balancer.
204    pub fn get_surb_balancer_config(&self) -> errors::Result<Option<SurbBalancerConfig>> {
205        Ok(self
206            .smgr
207            .upgrade()
208            .ok_or(HoprTransportError::Other(anyhow::anyhow!("session manager is dropped")))?
209            .get_surb_balancer_config(&self.id)?)
210    }
211
212    /// Updates the configuration of the SURB balancer.
213    ///
214    /// Returns an error if the Session is closed, the Session manager is gone, or the
215    /// Session has been created without a SURB balancer.
216    pub fn update_surb_balancer_config(&self, config: SurbBalancerConfig) -> errors::Result<()> {
217        Ok(self
218            .smgr
219            .upgrade()
220            .ok_or(HoprTransportError::Other(anyhow::anyhow!("session manager is dropped")))?
221            .update_surb_balancer_config(&self.id, config)?)
222    }
223
224    /// Explicitly closes the underlying Session in the [`SessionManager`].
225    ///
226    /// Returns `true` if the session was found and closed, `false` if it was
227    /// already gone (or the manager is dropped). Frees the per-session state
228    /// (frame reassembly buffers, control channels, …) immediately rather than
229    /// waiting for the manager's idle-timeout eviction.
230    pub fn close(&self) -> bool {
231        match self.smgr.upgrade() {
232            Some(smgr) => smgr.close_session(&self.id),
233            None => false,
234        }
235    }
236}
237
238/// Interface into the physical transport mechanism allowing all off-chain HOPR-related tasks on
239/// the transport.
240pub struct HoprTransport<Chain, Graph, Net> {
241    packet_key: OffchainKeypair,
242    chain_key: ChainKeypair,
243    chain_api: Chain,
244    ping: Arc<OnceLock<Pinger>>,
245    network: Arc<OnceLock<Net>>,
246    graph: Graph,
247    path_planner: PathPlanner<MemorySurbStore, Chain, HoprGraphPathSelector<Graph>>,
248    my_multiaddresses: Vec<Multiaddr>,
249    smgr: Arc<HoprSessionManager>,
250    session_telemetry_tag_allocator: Arc<dyn hopr_transport_tag_allocator::TagAllocator + Send + Sync>,
251    probing_tag_allocator: Arc<dyn hopr_transport_tag_allocator::TagAllocator + Send + Sync>,
252    counters: PeerProtocolCounterRegistry,
253    cfg: HoprProtocolConfig,
254}
255
256impl<Chain, Graph, Net> HoprTransport<Chain, Graph, Net>
257where
258    Chain: ChainReadChannelOperations
259        + ChainReadAccountOperations
260        + ChainWriteTicketOperations
261        + ChainKeyOperations
262        + ChainReadTicketOperations
263        + ChainValues
264        + Clone
265        + Send
266        + Sync
267        + 'static,
268    Graph: NetworkGraphView<NodeId = OffchainPublicKey>
269        + NetworkGraphUpdate
270        + hopr_api::graph::NetworkGraphWrite<NodeId = OffchainPublicKey>
271        + hopr_api::graph::NetworkGraphTraverse<NodeId = OffchainPublicKey>
272        + Clone
273        + Send
274        + Sync
275        + 'static,
276    <Graph as NetworkGraphView>::Observed: hopr_api::graph::traits::EdgeObservableRead + Send,
277    <Graph as hopr_api::graph::NetworkGraphTraverse>::Observed:
278        hopr_api::graph::traits::EdgeObservableRead + Send + 'static,
279    <Graph as hopr_api::graph::NetworkGraphWrite>::Observed: hopr_api::graph::traits::EdgeObservableWrite + Send,
280    Net: NetworkView + NetworkStreamControl + Clone + Send + Sync + 'static,
281{
282    pub fn new(
283        identity: (&ChainKeypair, &OffchainKeypair),
284        resolver: Chain,
285        graph: Graph,
286        my_multiaddresses: Vec<Multiaddr>,
287        cfg: HoprProtocolConfig,
288    ) -> errors::Result<Self> {
289        let me_offchain = *identity.1.public();
290        let planner_config = cfg.path_planner;
291        let selector = HoprGraphPathSelector::new(
292            me_offchain,
293            graph.clone(),
294            planner_config.max_cached_paths,
295            planner_config.edge_penalty,
296            planner_config.min_ack_rate,
297            planner_config.min_paths_anonymity_floor,
298        );
299
300        let tag_allocators = hopr_transport_tag_allocator::create_allocators_from_config(&cfg.session.tag_allocator)?;
301
302        let mut session_telemetry_tag_allocator = None;
303        let mut probing_tag_allocator = None;
304        for (usage, alloc) in tag_allocators {
305            match usage {
306                // TODO: cleanup of Session tag allocators needed * (#8199)
307                hopr_transport_tag_allocator::Usage::Session => {}
308                hopr_transport_tag_allocator::Usage::SessionTerminalTelemetry => {
309                    session_telemetry_tag_allocator = Some(alloc)
310                }
311                hopr_transport_tag_allocator::Usage::ProvingTelemetry => probing_tag_allocator = Some(alloc),
312            }
313        }
314        let session_telemetry_tag_allocator = session_telemetry_tag_allocator
315            .ok_or_else(|| HoprTransportError::Api("session telemetry tag allocator missing".into()))?;
316        let probing_tag_allocator =
317            probing_tag_allocator.ok_or_else(|| HoprTransportError::Api("probing tag allocator missing".into()))?;
318
319        Ok(Self {
320            packet_key: identity.1.clone(),
321            chain_key: identity.0.clone(),
322            ping: Arc::new(OnceLock::new()),
323            network: Arc::new(OnceLock::new()),
324            graph,
325            path_planner: PathPlanner::new(
326                me_offchain,
327                MemorySurbStore::new(cfg.packet.surb_store),
328                resolver.clone(),
329                selector,
330                planner_config,
331            ),
332            my_multiaddresses,
333            smgr: Arc::new(SessionManager::new(SessionManagerConfig {
334                frame_mtu: std::env::var("HOPR_SESSION_FRAME_SIZE")
335                    .ok()
336                    .and_then(|s| s.parse::<usize>().ok())
337                    .unwrap_or_else(|| SessionManagerConfig::default().frame_mtu)
338                    .max(ApplicationData::PAYLOAD_SIZE),
339                max_frame_timeout: std::env::var("HOPR_SESSION_FRAME_TIMEOUT_MS")
340                    .ok()
341                    .and_then(|s| s.parse::<u64>().ok().map(Duration::from_millis))
342                    .unwrap_or_else(|| SessionManagerConfig::default().max_frame_timeout)
343                    .max(Duration::from_millis(100)),
344                max_buffered_segments: std::env::var("HOPR_SESSION_MAX_BUFFERED_SEGMENTS")
345                    .ok()
346                    .and_then(|s| s.parse::<usize>().ok())
347                    .unwrap_or_else(|| SessionManagerConfig::default().max_buffered_segments),
348                initiation_timeout_base: SESSION_INITIATION_TIMEOUT_BASE,
349                idle_timeout: cfg.session.idle_timeout,
350                balancer_sampling_interval: cfg.session.balancer_sampling_interval,
351                initial_return_session_egress_rate: 10,
352                minimum_surb_buffer_duration: cfg.session.balancer_minimum_surb_buffer_duration,
353                maximum_surb_buffer_size: cfg.packet.surb_store.rb_capacity,
354                surb_balance_notify_period: cfg.session.surb_balance_notify_period,
355                surb_target_notify: true,
356                maximum_sessions: cfg.session.maximum_managed_sessions,
357                ..Default::default()
358            })),
359            chain_api: resolver,
360            session_telemetry_tag_allocator,
361            probing_tag_allocator,
362            counters: PeerProtocolCounterRegistry::default(),
363            cfg,
364        })
365    }
366
367    /// Execute all processes of the [`HoprTransport`] object as a **Relay** node.
368    ///
369    /// Relay nodes run the full packet pipeline including incoming ticket/acknowledgement
370    /// processing and require a [`futures::Sink`] for ticket events as well as an
371    /// `on_incoming_session` channel from the SessionManager (they can accept incoming sessions).
372    pub async fn run_relay<T, TFact, Ct>(
373        &self,
374        cover_traffic: Ct,
375        network: Net,
376        network_process: BoxedProcessFn,
377        ticket_events: T,
378        ticket_factory: TFact,
379        on_incoming_session: Sender<IncomingSession>,
380    ) -> errors::Result<(
381        HoprSocket<
382            futures::stream::BoxStream<'static, ApplicationDataIn>,
383            CrossfireSink<(DestinationRouting, ApplicationDataOut)>,
384        >,
385        AbortableList<HoprTransportProcess>,
386    )>
387    where
388        T: futures::Sink<hopr_api::node::TicketEvent> + Clone + Send + Unpin + 'static,
389        T::Error: std::error::Error + Clone + Send,
390        Ct: ProbingTrafficGeneration + CoverTrafficGeneration + Send + Sync + 'static,
391        TFact: TicketFactory + Clone + Send + Sync + 'static,
392    {
393        self.run_inner(
394            protocol::NodeType::Relay,
395            cover_traffic,
396            network,
397            network_process,
398            ticket_events,
399            ticket_factory,
400            Some(on_incoming_session),
401        )
402        .await
403    }
404
405    /// Execute all processes of the [`HoprTransport`] object as an **Exit** (destination) node.
406    ///
407    /// Exit nodes do not process tickets but keep the incoming acknowledgement
408    /// pipeline running and can accept incoming sessions via SessionManager.
409    pub async fn run_exit<TFact, Ct>(
410        &self,
411        cover_traffic: Ct,
412        network: Net,
413        network_process: BoxedProcessFn,
414        ticket_factory: TFact,
415        on_incoming_session: Sender<IncomingSession>,
416    ) -> errors::Result<(
417        HoprSocket<
418            futures::stream::BoxStream<'static, ApplicationDataIn>,
419            CrossfireSink<(DestinationRouting, ApplicationDataOut)>,
420        >,
421        AbortableList<HoprTransportProcess>,
422    )>
423    where
424        Ct: ProbingTrafficGeneration + CoverTrafficGeneration + Send + Sync + 'static,
425        TFact: TicketFactory + Clone + Send + Sync + 'static,
426    {
427        self.run_inner(
428            protocol::NodeType::Exit,
429            cover_traffic,
430            network,
431            network_process,
432            futures::sink::drain(),
433            ticket_factory,
434            Some(on_incoming_session),
435        )
436        .await
437    }
438
439    /// Execute all processes of the [`HoprTransport`] object as an **Entry** (source) node.
440    ///
441    /// Entry nodes do not process tickets, do not start the incoming acknowledgement
442    /// pipeline, and do not accept incoming sessions — therefore, they require neither a
443    /// `ticket_events` sink nor an `on_incoming_session` channel.
444    pub async fn run_entry<TFact, Ct>(
445        &self,
446        cover_traffic: Ct,
447        network: Net,
448        network_process: BoxedProcessFn,
449        ticket_factory: TFact,
450    ) -> errors::Result<(
451        HoprSocket<
452            futures::stream::BoxStream<'static, ApplicationDataIn>,
453            CrossfireSink<(DestinationRouting, ApplicationDataOut)>,
454        >,
455        AbortableList<HoprTransportProcess>,
456    )>
457    where
458        Ct: ProbingTrafficGeneration + CoverTrafficGeneration + Send + Sync + 'static,
459        TFact: TicketFactory + Clone + Send + Sync + 'static,
460    {
461        self.run_inner(
462            protocol::NodeType::Entry,
463            cover_traffic,
464            network,
465            network_process,
466            futures::sink::drain(),
467            ticket_factory,
468            None,
469        )
470        .await
471    }
472
473    /// Internal worker driving all node-type variants of `HoprTransport::run_*`.
474    ///
475    /// Branches on `role`:
476    /// - [`protocol::NodeType::Relay`]: full packet pipeline + SessionManager.
477    /// - [`protocol::NodeType::Exit`]: ack-drain pipeline + incoming Sessions.
478    /// - [`protocol::NodeType::Entry`]: no ack pipeline, no incoming Sessions.
479    #[allow(clippy::too_many_arguments)]
480    async fn run_inner<T, TFact, Ct>(
481        &self,
482        role: protocol::NodeType,
483        cover_traffic: Ct,
484        network: Net,
485        network_process: BoxedProcessFn,
486        ticket_events: T,
487        ticket_factory: TFact,
488        on_incoming_session: Option<Sender<IncomingSession>>,
489    ) -> errors::Result<(
490        HoprSocket<
491            futures::stream::BoxStream<'static, ApplicationDataIn>,
492            CrossfireSink<(DestinationRouting, ApplicationDataOut)>,
493        >,
494        AbortableList<HoprTransportProcess>,
495    )>
496    where
497        T: futures::Sink<hopr_api::node::TicketEvent> + Clone + Send + Unpin + 'static,
498        T::Error: std::error::Error + Clone + Send,
499        Ct: ProbingTrafficGeneration + CoverTrafficGeneration + Send + Sync + 'static,
500        TFact: TicketFactory + Clone + Send + Sync + 'static,
501    {
502        let mut processes = AbortableList::<HoprTransportProcess>::default();
503
504        let (unresolved_routing_msg_tx, unresolved_routing_msg_rx) =
505            bounded_sink_channel::<(DestinationRouting, ApplicationDataOut)>(MAXIMUM_MSG_OUTGOING_BUFFER_SIZE);
506
507        // -- transport medium
508
509        let transport_network = network;
510        let transport_layer_process = network_process;
511
512        let msg_codec = crate::protocol::HoprBinaryCodec {};
513        let (wire_msg_tx, wire_msg_rx) =
514            protocol::stream::process_stream_protocol(msg_codec, transport_network.clone(), self.cfg.stream).await?;
515
516        // Shared mixing channel: all per-destination clones of `mixing_channel_tx` push into one
517        // heap, so cross-destination packets are mixed together rather than each destination
518        // getting its own independent delay queue. The single forwarder task owns the receiver
519        // (and therefore the heap timer) — no per-clone waker coordination is needed.
520        let mut mixer_cfg = self.cfg.mixer;
521        mixer_cfg.metric_delay_window = u64::try_from(5 * mixer_cfg.delay_range.as_millis())
522            .unwrap_or(u64::MAX)
523            .max(1);
524        let (mixing_channel_tx, mix_rx) = hopr_transport_mixer::channel(mixer_cfg);
525        processes.insert(
526            HoprTransportProcess::MixerForwarder,
527            hopr_utils::spawn_as_abortable!(async move {
528                mix_rx
529                    .fold(wire_msg_tx, |mut sink, item| async move {
530                        if sink.send(item).await.is_err() {
531                            tracing::error!(
532                                task = %HoprTransportProcess::MixerForwarder,
533                                "wire sink dropped — discarding mixed packet"
534                            );
535                        }
536                        sink
537                    })
538                    .await;
539                tracing::warn!(
540                    task = %HoprTransportProcess::MixerForwarder,
541                    "long-running background task finished"
542                );
543            }),
544        );
545
546        // -- path cache background refresh (only when tokio runtime is available)
547        #[cfg(feature = "runtime-tokio")]
548        processes.insert(
549            HoprTransportProcess::PathRefresh,
550            hopr_utils::spawn_as_abortable!(self.path_planner.run_background_refresh()),
551        );
552
553        processes.insert(
554            HoprTransportProcess::Medium,
555            hopr_utils::spawn_as_abortable!(transport_layer_process().inspect(|_| tracing::warn!(
556                task = %HoprTransportProcess::Medium,
557                "long-running background task finished"
558            ))),
559        );
560
561        let msg_protocol_bidirectional_channel_capacity =
562            std::env::var("HOPR_INTERNAL_PROTOCOL_BIDIRECTIONAL_CHANNEL_CAPACITY")
563                .ok()
564                .and_then(|s| s.trim().parse::<usize>().ok())
565                .filter(|&c| c > 0)
566                .unwrap_or(16_384);
567
568        debug!(
569            capacity = msg_protocol_bidirectional_channel_capacity,
570            "creating protocol bidirectional channel"
571        );
572        let (tx_from_protocol, rx_from_protocol) =
573            bounded_sink_channel::<(HoprPseudonym, ApplicationDataIn)>(msg_protocol_bidirectional_channel_capacity);
574
575        // === START === cover traffic control
576        // Allocate a cover traffic tag from the session telemetry partition to avoid
577        // collisions with session and probing tags.
578        let cover_traffic_allocated_tag = self
579            .session_telemetry_tag_allocator
580            .allocate()
581            .ok_or_else(|| HoprTransportError::Api("failed to allocate cover traffic tag".into()))?;
582        let cover_traffic_tag: Tag = cover_traffic_allocated_tag.value().into();
583
584        // filter out the known cover traffic not to lose processing time with it
585        // The allocated tag is moved into the closure to keep it alive for the transport lifetime.
586        let rx_from_protocol = rx_from_protocol.filter_map(move |(pseudonym, data)| {
587            let _keep_alive = &cover_traffic_allocated_tag;
588            async move { (data.data.application_tag != cover_traffic_tag).then_some((pseudonym, data)) }
589        });
590
591        // prepare a cover traffic stream
592        let cover_traffic_stream = CoverTrafficGeneration::build(&cover_traffic).filter_map(move |routing| {
593            let start =
594                hopr_api::types::crypto_random::random_integer(0, Some((RANDOM_DATA.len() - 100) as u64)) as usize;
595            let data = &RANDOM_DATA[start..start + 100];
596
597            futures::future::ready(if let Ok(data) = ApplicationData::new(cover_traffic_tag, data) {
598                Some((routing, ApplicationDataOut::with_no_packet_info(data)))
599            } else {
600                tracing::error!("failed to construct cover traffic packet");
601                None
602            })
603        });
604
605        // merge cover traffic with other outgoing data
606        let merged_unresolved_output_data =
607            select_with_strategy(unresolved_routing_msg_rx, cover_traffic_stream, |_: &mut ()| {
608                futures::stream::PollNext::Left
609            });
610
611        // === END === cover traffic control
612
613        // We have to resolve DestinationRouting -> ResolvedTransportRouting before
614        // sending the external packets to the transport pipeline. Concurrency matches
615        // the encoder stage (output_concurrency) to avoid head-of-line blocking on
616        // cache-miss path lookups.
617        let path_planner = self.path_planner.clone();
618        let distress_threshold = self.cfg.packet.surb_store.distress_threshold;
619        let routing_concurrency = {
620            let avail = std::thread::available_parallelism()
621                .ok()
622                .map(|n| n.get())
623                .unwrap_or(1)
624                .max(1)
625                * 8;
626            self.cfg
627                .packet
628                .pipeline
629                .output_concurrency
630                .filter(|&n| n > 0)
631                .unwrap_or(avail)
632        };
633        let all_resolved_external_msg_rx = merged_unresolved_output_data
634            .then_concurrent(
635                move |(unresolved, mut data)| {
636                    let path_planner = path_planner.clone();
637                    async move {
638                        trace!(?unresolved, "resolving routing for packet");
639                        match path_planner
640                            .resolve_routing(data.data.total_len(), data.estimate_surbs_with_msg(), unresolved)
641                            .await
642                        {
643                            Ok((resolved, rem_surbs)) => {
644                                // Set the SURB distress/out-of-SURBs flag if applicable.
645                                // These flags are translated into HOPR protocol packet signals and are
646                                // applicable only on the return path.
647                                let mut signals_to_dst = data
648                                    .packet_info
649                                    .as_ref()
650                                    .map(|info| info.signals_to_destination)
651                                    .unwrap_or_default();
652
653                                if resolved.is_return() {
654                                    signals_to_dst = match rem_surbs {
655                                        Some(rem) if (1..distress_threshold.max(2)).contains(&rem) => {
656                                            signals_to_dst | PacketSignal::SurbDistress
657                                        }
658                                        Some(0) => signals_to_dst | PacketSignal::OutOfSurbs,
659                                        _ => signals_to_dst - (PacketSignal::OutOfSurbs | PacketSignal::SurbDistress),
660                                    };
661                                } else {
662                                    // Unset these flags as they make no sense on the forward path.
663                                    signals_to_dst -= PacketSignal::SurbDistress | PacketSignal::OutOfSurbs;
664                                }
665
666                                data.packet_info.get_or_insert_default().signals_to_destination = signals_to_dst;
667                                trace!(?resolved, "resolved routing for packet");
668                                Some((resolved, data))
669                            }
670                            Err(error) => {
671                                error!(%error, "failed to resolve routing");
672                                None
673                            }
674                        }
675                    }
676                    .in_current_span()
677                },
678                routing_concurrency,
679            )
680            .filter_map(futures::future::ready);
681
682        let channels_dst = self
683            .chain_api
684            .domain_separators()
685            .await
686            .map_err(HoprTransportError::chain)?
687            .channel;
688
689        let pipeline_builder = HoprPacketPipelineBuilder::new()
690            .identity((&self.chain_key, &self.packet_key))
691            .transport((mixing_channel_tx, wire_msg_rx))
692            .api((tx_from_protocol, all_resolved_external_msg_rx))
693            .surb_store(self.path_planner.surb_store.clone())
694            .chain_api(self.chain_api.clone())
695            .ticket_factory(ticket_factory)
696            .channels_dst(channels_dst)
697            .with_counters(self.counters.clone())
698            .with_config(self.cfg.packet);
699
700        let pipeline_processes = match role {
701            protocol::NodeType::Relay => pipeline_builder.with_ticket_events(ticket_events).build_for_relay(),
702            protocol::NodeType::Exit => pipeline_builder.build_for_exit(),
703            protocol::NodeType::Entry => pipeline_builder.build_for_entry(),
704        };
705        processes.extend_from(pipeline_processes);
706
707        // -- periodic counter flush
708        let flush_counters = self.counters.clone();
709        let flush_graph = self.graph.clone();
710        let flush_me = *self.packet_key.public();
711        let flush_interval = self.cfg.counter_flush_interval;
712        processes.insert(
713            HoprTransportProcess::CounterFlush,
714            hopr_utils::spawn_as_abortable!(async move {
715                use hopr_api::graph::traits::{EdgeObservableWrite, EdgeWeightType};
716
717                futures_time::stream::interval(futures_time::time::Duration::from(flush_interval))
718                    .for_each(|_| {
719                        for (peer, num_packets, num_acks) in flush_counters.drain() {
720                            tracing::trace!(
721                                %peer,
722                                num_packets,
723                                num_acks,
724                                "flushing protocol conformance counters"
725                            );
726                            flush_graph.upsert_edge(&flush_me, &peer, |obs| {
727                                obs.record(EdgeWeightType::ImmediateProtocolConformance { num_packets, num_acks });
728                            });
729                        }
730                        futures::future::ready(())
731                    })
732                    .await;
733            }),
734        );
735
736        // -- network probing
737        let manual_ping_channel_capacity = std::env::var("HOPR_INTERNAL_MANUAL_PING_CHANNEL_CAPACITY")
738            .ok()
739            .and_then(|s| s.trim().parse::<usize>().ok())
740            .filter(|&c| c > 0)
741            .unwrap_or(128);
742        debug!(capacity = manual_ping_channel_capacity, "Creating manual ping channel");
743        let (manual_ping_tx, manual_ping_rx_raw) =
744            crossfire::mpsc::bounded_async::<(OffchainPublicKey, PingQueryReplier)>(manual_ping_channel_capacity);
745        let manual_ping_rx = manual_ping_rx_raw.into_stream();
746
747        let probe = Probe::new(self.cfg.probe, self.probing_tag_allocator.clone());
748
749        let (probing_processes, probe_classifier) = probe
750            .continuously_scan(
751                unresolved_routing_msg_tx.clone(),
752                manual_ping_rx,
753                cover_traffic,
754                self.graph.clone(),
755            )
756            .await;
757
758        processes.flat_map_extend_from(probing_processes, HoprTransportProcess::Probing);
759
760        // manual ping
761        self.ping
762            .clone()
763            .set(Pinger::new(
764                PingConfig {
765                    timeout: self.cfg.probe.timeout,
766                },
767                manual_ping_tx,
768            ))
769            .map_err(|_| HoprTransportError::Api("must set the ticket aggregation writer only once".into()))?;
770
771        // -- session management
772        let smgr_start_res = if role != protocol::NodeType::Entry {
773            // Relays and Exits can accept incoming Sessions
774            self.smgr.start(
775                unresolved_routing_msg_tx.clone(),
776                on_incoming_session.ok_or_else(|| {
777                    HoprTransportError::Api("on_incoming_session channel is required for relay/exit nodes".into())
778                })?,
779            )
780        } else {
781            // Entry nodes cannot accept incoming Sessions
782            self.smgr
783                .start(unresolved_routing_msg_tx.clone(), futures::sink::drain())
784        };
785
786        smgr_start_res
787            .map_err(|_| HoprTransportError::Api("failed to start session manager".into()))?
788            .into_iter()
789            .enumerate()
790            .map(|(i, jh)| (HoprTransportProcess::SessionsManagement(i + 1), jh))
791            .for_each(|(k, v)| {
792                processes.insert(k, v);
793            });
794
795        // Wire incoming: cover-traffic-filtered stream → probe classify → (session dispatch).
796        // This stage must run in a background task, so the pipeline drains even when the
797        // caller discards the returned HoprSocket (e.g. edge-node builder).
798        //
799        // The channel uses a resilient for_each rather than .forward() so that a disconnected
800        // receiver (HoprSocket dropped without consuming) logs an error and continues rather
801        // than collapsing the entire ingress pipeline. Callers should use HoprSocket::reader()
802        // and actively drain the stream; see hopr-lib builder for the reference drain.
803        let (on_incoming_data_tx, on_incoming_data_rx) =
804            bounded_sink_channel::<ApplicationDataIn>(msg_protocol_bidirectional_channel_capacity);
805        let smgr = self.smgr.clone();
806        let unresolved_routing_msg_tx_for_task = unresolved_routing_msg_tx.clone();
807        processes.insert(
808            HoprTransportProcess::SessionsManagement(0),
809            hopr_utils::spawn_as_abortable!(async move {
810                probe_classifier
811                    .filter_stream(unresolved_routing_msg_tx_for_task, rx_from_protocol)
812                    .filter_map(move |(pseudonym, data)| {
813                        futures::future::ready(match smgr.dispatch_message(pseudonym, data) {
814                            Ok(DispatchResult::Processed) => {
815                                tracing::trace!("message dispatch completed");
816                                None
817                            }
818                            Ok(DispatchResult::Unrelated(data)) => {
819                                tracing::trace!("unrelated message dispatch completed");
820                                Some(data)
821                            }
822                            Err(error) => {
823                                tracing::error!(%error, "error while dispatching packet in the session manager");
824                                None
825                            }
826                        })
827                    })
828                    .fold(on_incoming_data_tx, |mut tx, data| async move {
829                        if tx.send(data).await.is_err() {
830                            tracing::error!(
831                                task = %HoprTransportProcess::SessionsManagement(0),
832                                "incoming-data channel disconnected — dropping unrelated packet; \
833                                 HoprSocket must be consumed or drained by the caller"
834                            );
835                        }
836                        tx
837                    })
838                    .await;
839                tracing::warn!(
840                    task = %HoprTransportProcess::SessionsManagement(0),
841                    "long-running background task finished"
842                );
843            }),
844        );
845
846        // Populate the OnceLock at the end, making sure everything before didn't fail.
847        self.network
848            .clone()
849            .set(transport_network)
850            .map_err(|_| HoprTransportError::Api("transport network viewer already set".into()))?;
851
852        Ok((
853            (on_incoming_data_rx.boxed(), unresolved_routing_msg_tx).into(),
854            processes,
855        ))
856    }
857
858    #[tracing::instrument(level = "debug", skip(self))]
859    pub async fn ping(
860        &self,
861        peer: &OffchainPublicKey,
862    ) -> errors::Result<(std::time::Duration, <Graph as NetworkGraphView>::Observed)> {
863        let me: &OffchainPublicKey = self.packet_key.public();
864        if peer == me {
865            return Err(HoprTransportError::Api("ping to self does not make sense".into()));
866        }
867
868        let pinger = self
869            .ping
870            .get()
871            .ok_or_else(|| HoprTransportError::Api("ping processing is not yet initialized".into()))?;
872
873        let latency = (*pinger).ping(peer).await?;
874
875        if let Some(observations) = self.graph.edge(me, peer) {
876            Ok((latency, observations))
877        } else {
878            Err(HoprTransportError::Api(format!(
879                "no observations available for peer {peer}",
880            )))
881        }
882    }
883
884    #[tracing::instrument(level = "debug", skip(self))]
885    pub async fn new_session(
886        &self,
887        destination: Address,
888        target: SessionTarget,
889        cfg: SessionClientConfig,
890    ) -> errors::Result<(HoprSession, HoprSessionConfigurator)> {
891        let session = self.smgr.new_session(destination, target, cfg).await?;
892        let id = *session.id();
893        Ok((
894            session,
895            HoprSessionConfigurator {
896                id,
897                smgr: Arc::downgrade(&self.smgr),
898            },
899        ))
900    }
901
902    #[tracing::instrument(level = "debug", skip(self))]
903    pub async fn listening_multiaddresses(&self) -> Vec<Multiaddr> {
904        self.network
905            .get()
906            .ok_or_else(|| HoprTransportError::Api("transport network is not yet initialized".into()))
907            .map(|network| network.listening_as().into_iter().collect())
908            .unwrap_or_default()
909    }
910
911    #[tracing::instrument(level = "debug", skip(self))]
912    pub fn announceable_multiaddresses(&self) -> Vec<Multiaddr> {
913        let mut mas = self
914            .local_multiaddresses()
915            .into_iter()
916            .filter(|ma| {
917                crate::multiaddrs::is_supported(ma)
918                    && (self.cfg.transport.announce_local_addresses || is_public_address(ma))
919            })
920            .map(|ma| strip_p2p_protocol(&ma))
921            .filter(|v| !v.is_empty())
922            .collect::<Vec<_>>();
923
924        mas.sort_by(|l, r| {
925            let is_left_dns = crate::multiaddrs::is_dns(l);
926            let is_right_dns = crate::multiaddrs::is_dns(r);
927
928            if !(is_left_dns ^ is_right_dns) {
929                std::cmp::Ordering::Equal
930            } else if is_left_dns {
931                std::cmp::Ordering::Less
932            } else {
933                std::cmp::Ordering::Greater
934            }
935        });
936
937        mas
938    }
939
940    /// Returns a reference to the network graph.
941    pub fn graph(&self) -> &Graph {
942        &self.graph
943    }
944
945    #[tracing::instrument(level = "debug", skip(self))]
946    pub fn local_multiaddresses(&self) -> Vec<Multiaddr> {
947        self.network
948            .get()
949            .map(|network| network.listening_as().into_iter().collect())
950            .unwrap_or_else(|| {
951                tracing::error!("transport network is not yet initialized, cannot fetch announced multiaddresses");
952                self.my_multiaddresses.clone()
953            })
954    }
955
956    #[tracing::instrument(level = "debug", skip(self))]
957    pub async fn network_observed_multiaddresses(&self, peer: &OffchainPublicKey) -> Vec<Multiaddr> {
958        match self
959            .network
960            .get()
961            .ok_or_else(|| HoprTransportError::Api("transport network is not yet initialized".into()))
962        {
963            Ok(network) => network
964                .multiaddress_of(&peer.into())
965                .unwrap_or_default()
966                .into_iter()
967                .collect(),
968            Err(error) => {
969                tracing::error!(%error, "failed to get observed multiaddresses");
970                return vec![];
971            }
972        }
973    }
974
975    #[tracing::instrument(level = "debug", skip(self))]
976    pub async fn network_health(&self) -> Health {
977        self.network
978            .get()
979            .ok_or_else(|| HoprTransportError::Api("transport network is not yet initialized".into()))
980            .map(|network| network.health())
981            .unwrap_or(Health::Red)
982    }
983
984    pub async fn network_connected_peers(&self) -> errors::Result<Vec<OffchainPublicKey>> {
985        Ok(futures::stream::iter(
986            self.network
987                .get()
988                .ok_or_else(|| {
989                    tracing::error!("transport network is not yet initialized");
990                    HoprTransportError::Api("transport network is not yet initialized".into())
991                })?
992                .connected_peers(),
993        )
994        .filter_map(|peer_id| async move {
995            match peer_id_to_public_key(&peer_id) {
996                Ok(key) => Some(key),
997                Err(error) => {
998                    tracing::warn!(%peer_id, %error, "failed to convert PeerId to OffchainPublicKey");
999                    None
1000                }
1001            }
1002        })
1003        .collect()
1004        .await)
1005    }
1006
1007    #[tracing::instrument(level = "debug", skip(self))]
1008    pub fn network_peer_observations(&self, peer: &OffchainPublicKey) -> Option<<Graph as NetworkGraphView>::Observed> {
1009        self.graph.edge(self.packet_key.public(), peer)
1010    }
1011
1012    /// Get connected peers with quality higher than some value.
1013    #[tracing::instrument(level = "debug", skip(self))]
1014    pub async fn all_network_peers(
1015        &self,
1016        minimum_score: f64,
1017    ) -> errors::Result<Vec<(OffchainPublicKey, <Graph as NetworkGraphView>::Observed)>> {
1018        let me = self.packet_key.public();
1019        Ok(self
1020            .network_connected_peers()
1021            .await?
1022            .into_iter()
1023            .filter_map(|peer| {
1024                let observation = self.graph.edge(me, &peer);
1025                if let Some(info) = observation {
1026                    if info.score() >= minimum_score {
1027                        Some((peer, info))
1028                    } else {
1029                        None
1030                    }
1031                } else {
1032                    None
1033                }
1034            })
1035            .collect::<Vec<_>>())
1036    }
1037}
1038
1039// ---------------------------------------------------------------------------
1040// NetworkView impl for HoprTransport — wraps OnceLock<Net> access
1041// ---------------------------------------------------------------------------
1042
1043impl<Chain, Graph, Net> NetworkView for HoprTransport<Chain, Graph, Net>
1044where
1045    Net: NetworkView + Send + Sync + 'static,
1046{
1047    fn listening_as(&self) -> std::collections::HashSet<Multiaddr> {
1048        self.network.get().map(|n| n.listening_as()).unwrap_or_default()
1049    }
1050
1051    fn multiaddress_of(&self, peer: &PeerId) -> Option<std::collections::HashSet<Multiaddr>> {
1052        self.network.get()?.multiaddress_of(peer)
1053    }
1054
1055    fn discovered_peers(&self) -> std::collections::HashSet<PeerId> {
1056        self.network.get().map(|n| n.discovered_peers()).unwrap_or_default()
1057    }
1058
1059    fn connected_peers(&self) -> std::collections::HashSet<PeerId> {
1060        self.network.get().map(|n| n.connected_peers()).unwrap_or_default()
1061    }
1062
1063    fn is_connected(&self, peer: &PeerId) -> bool {
1064        self.network.get().map(|n| n.is_connected(peer)).unwrap_or(false)
1065    }
1066
1067    fn health(&self) -> Health {
1068        self.network.get().map(|n| n.health()).unwrap_or(Health::Red)
1069    }
1070
1071    fn subscribe_network_events(
1072        &self,
1073    ) -> impl futures::Stream<Item = hopr_api::network::NetworkEvent> + Send + 'static {
1074        match self.network.get() {
1075            Some(n) => futures::future::Either::Left(n.subscribe_network_events()),
1076            None => futures::future::Either::Right(futures::stream::empty()),
1077        }
1078    }
1079}
1080
1081// ---------------------------------------------------------------------------
1082// TransportOperations impl for HoprTransport
1083// ---------------------------------------------------------------------------
1084
1085#[async_trait::async_trait]
1086impl<Chain, Graph, Net> hopr_api::node::TransportOperations for HoprTransport<Chain, Graph, Net>
1087where
1088    Chain: ChainReadChannelOperations
1089        + ChainReadAccountOperations
1090        + hopr_api::chain::ChainWriteTicketOperations
1091        + ChainKeyOperations
1092        + hopr_api::chain::ChainReadTicketOperations
1093        + ChainValues
1094        + Clone
1095        + Send
1096        + Sync
1097        + 'static,
1098    Graph: NetworkGraphView<NodeId = OffchainPublicKey>
1099        + NetworkGraphUpdate
1100        + hopr_api::graph::NetworkGraphWrite<NodeId = OffchainPublicKey>
1101        + hopr_api::graph::NetworkGraphTraverse<NodeId = OffchainPublicKey>
1102        + Clone
1103        + Send
1104        + Sync
1105        + 'static,
1106    <Graph as NetworkGraphView>::Observed: EdgeObservableRead + Send,
1107    <Graph as hopr_api::graph::NetworkGraphTraverse>::Observed: EdgeObservableRead + Send + 'static,
1108    <Graph as hopr_api::graph::NetworkGraphWrite>::Observed: hopr_api::graph::traits::EdgeObservableWrite + Send,
1109    Net: NetworkView + NetworkStreamControl + Clone + Send + Sync + 'static,
1110{
1111    type Error = errors::HoprTransportError;
1112    type Observable = <Graph as NetworkGraphView>::Observed;
1113
1114    async fn ping(&self, key: &OffchainPublicKey) -> Result<(Duration, Self::Observable), Self::Error> {
1115        self.ping(key).await
1116    }
1117
1118    async fn observed_multiaddresses(&self, key: &OffchainPublicKey) -> Vec<Multiaddr> {
1119        self.network_observed_multiaddresses(key).await
1120    }
1121}