Skip to main content

hopr_transport_session/
types.rs

1use std::{
2    convert::Into,
3    fmt::{Debug, Formatter},
4    pin::Pin,
5    sync::Arc,
6    task::{Context, Poll},
7    time::Duration,
8};
9
10use futures::{SinkExt, StreamExt, TryStreamExt};
11use hopr_api::types::{internal::routing::DestinationRouting, primitive::errors::GeneralError};
12use hopr_protocol_app::prelude::{ApplicationData, ApplicationDataIn, ApplicationDataOut, ReservedTag, Tag};
13#[cfg(feature = "telemetry")]
14use hopr_protocol_session::NoopTracker;
15use hopr_protocol_session::{
16    AcknowledgementMode, AcknowledgementState, AcknowledgementStateConfig, ReliableSocket, SessionSocketConfig,
17    UnreliableSocket,
18    flow_control::{DeliveryClock, DeliveryMeter, DeliveryTap, FlowControlConfig},
19};
20use hopr_protocol_start::StartProtocol;
21use hopr_utils::network_types::utils::{AsyncWriteSink, DuplexIO};
22use tracing::{debug, instrument};
23
24use crate::{
25    Capabilities, Capability,
26    balancer::BalancerStateValues,
27    errors::TransportSessionError,
28    flow_control::{PacedWriter, SurbSupply},
29};
30
31/// Wrapper for [`Capabilities`] that makes conversion to/from `u8` possible.
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub struct ByteCapabilities(pub Capabilities);
34
35impl TryFrom<u8> for ByteCapabilities {
36    type Error = GeneralError;
37
38    fn try_from(value: u8) -> Result<Self, Self::Error> {
39        Capabilities::new(value)
40            .map(Self)
41            .map_err(|_| GeneralError::ParseError("capabilities".into()))
42    }
43}
44
45impl From<ByteCapabilities> for u8 {
46    fn from(value: ByteCapabilities) -> Self {
47        *value.0.as_ref()
48    }
49}
50
51impl From<ByteCapabilities> for Capabilities {
52    fn from(value: ByteCapabilities) -> Self {
53        value.0
54    }
55}
56
57impl From<Capabilities> for ByteCapabilities {
58    fn from(value: Capabilities) -> Self {
59        Self(value)
60    }
61}
62
63impl AsRef<Capabilities> for ByteCapabilities {
64    fn as_ref(&self) -> &Capabilities {
65        &self.0
66    }
67}
68
69/// Start protocol instantiation for HOPR.
70pub type HoprStartProtocol = StartProtocol<SessionId, SessionTarget, ByteCapabilities>;
71
72/// Constant application tag used for all sessions.
73/// Previously tags were dynamically allocated per session.
74pub const SESSION_APPLICATION_TAG: Tag = Tag::Reserved(ReservedTag::Session as u64);
75
76/// [`SessionId`], [`ServiceId`], and [`SessionTarget`] are provided by `hopr-types` and
77/// re-exported here via `hopr-utils`, so they match the published `hopr-api` session types.
78///
79/// - `SessionId` is a type alias for `HoprPseudonym` (a constant application tag is used for all sessions instead
80///   of dynamically allocating tags).
81/// - `ServiceId` identifies a service local to the Exit node (e.g. Cover Traffic).
82/// - `SessionTarget` describes where data received over the session is forwarded.
83pub use hopr_utils::network_types::types::{ServiceId, SessionId, SessionTarget};
84
85pub(crate) fn caps_to_ack_mode(caps: Capabilities) -> AcknowledgementMode {
86    if caps.contains(Capability::RetransmissionAck | Capability::RetransmissionNack) {
87        AcknowledgementMode::Both
88    } else if caps.contains(Capability::RetransmissionAck) {
89        AcknowledgementMode::Full
90    } else {
91        AcknowledgementMode::Partial
92    }
93}
94
95/// Indicates the closure reason of a [`HoprSession`].
96#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)]
97pub enum ClosureReason {
98    /// Write-half of the Session has been closed.
99    WriteClosed,
100    /// Read-part of the Session has been closed (encountered empty read).
101    EmptyRead,
102    /// Session has been evicted from the cache due to inactivity or capacity reasons.
103    Eviction,
104}
105
106/// Helper trait to allow Box aliasing
107trait AsyncReadWrite: futures::AsyncWrite + futures::AsyncRead + Send + Unpin {}
108impl<T: futures::AsyncWrite + futures::AsyncRead + Send + Unpin> AsyncReadWrite for T {}
109
110/// Wrapper for an incoming [`HoprSession`] carrying the [`SessionId`] and [`SessionTarget`]
111/// extracted from the Start protocol during session establishment.
112///
113/// This is the published generic [`hopr_api::node::IncomingSession`] specialized to the
114/// concrete [`HoprSession`] byte-stream.
115pub type IncomingSession = hopr_api::node::IncomingSession<HoprSession>;
116
117/// Configures the Session protocol socket over HOPR.
118#[derive(Copy, Clone, Debug, PartialEq, Eq, smart_default::SmartDefault, serde::Serialize)]
119pub struct HoprSessionConfig {
120    /// Capabilities of the Session protocol socket.
121    ///
122    /// Default is no capabilities.
123    #[default(Capabilities::empty())]
124    pub capabilities: Capabilities,
125    /// Expected frame size of the Session protocol socket.
126    ///
127    /// Default is 1500.
128    #[default(1500)]
129    pub frame_mtu: usize,
130    /// Maximum amount of time an incomplete frame can be kept in the buffer.
131    ///
132    /// Default is 800 ms
133    #[default(Duration::from_millis(800))]
134    #[serde(with = "humantime_serde")]
135    pub frame_timeout: Duration,
136    /// Maximum number of segments to buffer in the downstream transport.
137    /// If 0 is given, the transport is unbuffered.
138    ///
139    /// Default is 0.
140    #[default(0)]
141    pub max_buffered_segments: usize,
142    /// Abandon the frame due next once this many later frames are waiting behind it.
143    ///
144    /// Head-of-line bound, distinct from [`Self::frame_timeout`]: that one waits for a frame that
145    /// may still arrive, this bounds how much already-received data is held while it waits. `None`
146    /// keeps the timeout as the only rule.
147    pub max_frames_behind_gap: Option<usize>,
148}
149
150/// Represents the Session protocol socket over HOPR.
151///
152/// This is essentially a HOPR-specific wrapper for [`ReliableSocket`] and [`UnreliableSocket`]
153/// Session protocol sockets.
154#[pin_project::pin_project]
155pub struct HoprSession {
156    id: SessionId,
157    #[pin]
158    inner: Box<dyn AsyncReadWrite>,
159    routing: DestinationRouting,
160    cfg: HoprSessionConfig,
161    on_close: Option<Box<dyn FnOnce(SessionId, ClosureReason) + Send + Sync>>,
162}
163
164pub(crate) const SESSION_SOCKET_CAPACITY: usize = 16384;
165
166impl HoprSession {
167    /// Creates a new HOPR Session.
168    ///
169    /// It builds an [`futures::io::AsyncRead`] + [`futures::io::AsyncWrite`] transport
170    /// from the given `hopr` interface and passing it to the appropriate [`UnreliableSocket`] or [`ReliableSocket`]
171    /// based on the given `capabilities`.
172    ///
173    /// The `on_close` closure can be optionally called when the Session has been closed via `poll_close`.
174    #[tracing::instrument(skip_all, fields(id, routing, cfg, session_id = %id))]
175    pub fn new<Tx, Rx>(
176        id: SessionId,
177        routing: DestinationRouting,
178        cfg: HoprSessionConfig,
179        hopr: (Tx, Rx),
180        on_close: Option<Box<dyn FnOnce(SessionId, ClosureReason) + Send + Sync>>,
181    ) -> Result<Self, TransportSessionError>
182    where
183        Tx: futures::Sink<(DestinationRouting, ApplicationDataOut)> + Send + Unpin + 'static,
184        Rx: futures::Stream<Item = ApplicationDataIn> + Send + Unpin + 'static,
185        Tx::Error: std::error::Error + Send + Sync,
186    {
187        Self::new_with_surb_state(id, routing, cfg, hopr, on_close, None, None)
188    }
189
190    /// Like [`new`](Self::new) but threads the SURB balancer state and the opt-in client-side
191    /// flow-control config. `flow_control` = the client's [`FlowControlConfig`] for this session
192    /// (`None` leaves it unpaced); `surb_mgmt` gives the window its anti-grief down-only SURB ceiling.
193    /// The entry (sending) side passes `Some(..)`; sites without them pass `None`.
194    #[allow(clippy::too_many_arguments)]
195    #[tracing::instrument(skip_all, fields(id, routing, cfg, session_id = %id))]
196    pub fn new_with_surb_state<Tx, Rx>(
197        id: SessionId,
198        routing: DestinationRouting,
199        cfg: HoprSessionConfig,
200        hopr: (Tx, Rx),
201        on_close: Option<Box<dyn FnOnce(SessionId, ClosureReason) + Send + Sync>>,
202        surb_mgmt: Option<Arc<BalancerStateValues>>,
203        flow_control: Option<FlowControlConfig>,
204    ) -> Result<Self, TransportSessionError>
205    where
206        Tx: futures::Sink<(DestinationRouting, ApplicationDataOut)> + Send + Unpin + 'static,
207        Rx: futures::Stream<Item = ApplicationDataIn> + Send + Unpin + 'static,
208        Tx::Error: std::error::Error + Send + Sync,
209    {
210        let routing_clone = routing.clone();
211
212        #[cfg(feature = "telemetry")]
213        let (session_id_write, session_id_read) = (id, id);
214
215        // Wrap the HOPR transport so that it appears as regular transport to the SessionSocket
216        let transport = DuplexIO(
217            AsyncWriteSink::<{ ApplicationData::PAYLOAD_SIZE }, _>(hopr.0.sink_map_err(std::io::Error::other).with(
218                move |buf: Box<[u8]>| {
219                    #[cfg(feature = "telemetry")]
220                    crate::telemetry::record_session_write(&session_id_write, buf.len());
221                    // The Session protocol does not set any packet info on outgoing packets.
222                    // However, the SessionManager on top usually overrides this.
223                    futures::future::ready(
224                        ApplicationData::new(SESSION_APPLICATION_TAG, buf.into_vec())
225                            .map(|data| (routing_clone.clone(), ApplicationDataOut::with_no_packet_info(data)))
226                            .map_err(std::io::Error::other),
227                    )
228                },
229            )),
230            // The Session protocol ignores the packet info on incoming packets.
231            // It is typically SessionManager's job to interpret those.
232            hopr.1
233                .map(move |data| {
234                    #[cfg(feature = "telemetry")]
235                    crate::telemetry::record_session_read(&session_id_read, data.data.plain_text.len());
236                    Ok::<_, std::io::Error>(data.data.plain_text)
237                })
238                .into_async_read(),
239        );
240
241        // Based on the requested capabilities, see if we should use the Session protocol
242        let inner: Box<dyn AsyncReadWrite> = if cfg.capabilities.contains(Capability::Segmentation) {
243            let socket_cfg = SessionSocketConfig {
244                frame_size: cfg.frame_mtu,
245                frame_timeout: cfg.frame_timeout,
246                capacity: SESSION_SOCKET_CAPACITY,
247                flush_immediately: cfg.capabilities.contains(Capability::NoDelay),
248                // Datagram-boundary preservation is stateless-only: the stateless branch below turns
249                // it on for `NoDelay` sessions. The reliable socket must never run in datagram mode
250                // (its NACK missing-segment bitmap cannot address the segments of an oversized
251                // datagram frame), so it keeps `NoDelay`'s buffering behavior only.
252                datagram: false,
253                max_buffered_segments: cfg.max_buffered_segments,
254                // Anti-bufferbloat bound; only meaningful when flow control is enabled, which is
255                // also where the honest clock that observes the resulting loss lives.
256                max_frame_age: flow_control.and_then(|c| c.max_frame_age),
257                // Head-of-line bound, and deliberately *not* gated on flow control the way
258                // `max_frame_age` is. The reasoning there runs backwards for this one: a session
259                // that can retransmit may still recover a missing frame, so waiting is
260                // productive, while a session without retransmission is waiting for something
261                // that is never coming and holds everything already received behind it. The
262                // sessions that need this bound most are exactly the ones flow control excludes.
263                max_frames_behind_gap: cfg.max_frames_behind_gap,
264                ..Default::default()
265            };
266
267            // Need to test the capabilities separately, because any Retransmission capability
268            // implies Segmentation, and therefore `is_disjoint` would fail
269            if cfg.capabilities.contains(Capability::RetransmissionAck)
270                || cfg.capabilities.contains(Capability::RetransmissionNack)
271            {
272                let fc = flow_control;
273
274                // TODO: update config values
275                let ack_cfg = AcknowledgementStateConfig {
276                    // This is a very coarse assumption, that a single 3-hop packet
277                    // takes on average 200 ms to deliver.
278                    // We can no longer base this timeout on the number of hops because
279                    // it is not known for SURB-based routing.
280                    expected_packet_latency: Duration::from_millis(200),
281                    mode: caps_to_ack_mode(cfg.capabilities),
282                    backoff_base: 0.2,
283                    max_incoming_frame_retries: 1,
284                    // Under flow control the sender is paced to the SURB drain rate, so an un-acked
285                    // frame is usually just a *delayed* ack on a temporarily-starved return path, not
286                    // a genuine loss. The retry budget is a flow-control config knob (`frame_retries`,
287                    // default 2 = original); a robust profile raises it so delayed frames recover
288                    // instead of being abandoned (an abandoned frame leaves a gap → stream corruption).
289                    // `.max(1)`: never drop the retry budget to 0 — an abandoned frame under
290                    // reliable-mode flow control leaves a gap and corrupts the stream.
291                    max_outgoing_frame_retries: fc.map(|c| c.frame_retries.max(1) as usize).unwrap_or(2),
292                    // Retire an outgoing frame that is already too stale to be worth delivering,
293                    // rather than spending the remaining retry budget on it.
294                    max_frame_age: fc.and_then(|c| c.max_frame_age),
295                    ..Default::default()
296                };
297
298                debug!(
299                    ?socket_cfg,
300                    ?ack_cfg,
301                    flow_control = fc.is_some(),
302                    "opening new stateful session socket"
303                );
304
305                // Opt-in client-side flow control: when enabled, install the honest-clock tap on the
306                // ack state and keep the paired clock to drive the paced writer.
307                let (ack_state, flow_control) = match fc {
308                    Some(fc_cfg) => {
309                        let meter = DeliveryMeter::default();
310                        let ack_state = AcknowledgementState::<{ ApplicationData::PAYLOAD_SIZE }>::new(id, ack_cfg)
311                            .with_delivery_tap(DeliveryTap::new(meter.clone(), cfg.frame_mtu));
312                        let clock = DeliveryClock::new(meter, Some(ack_cfg.expected_packet_latency));
313                        (ack_state, Some((fc_cfg, clock)))
314                    }
315                    None => (
316                        AcknowledgementState::<{ ApplicationData::PAYLOAD_SIZE }>::new(id, ack_cfg),
317                        None,
318                    ),
319                };
320
321                let socket = ReliableSocket::new(
322                    transport,
323                    ack_state,
324                    socket_cfg,
325                    #[cfg(feature = "telemetry")]
326                    NoopTracker,
327                )?;
328
329                match flow_control {
330                    Some((fc_cfg, clock)) => {
331                        let surb_state = surb_mgmt
332                            .clone()
333                            .unwrap_or_else(|| Arc::new(BalancerStateValues::default()));
334                        let supply = SurbSupply::new(surb_state, cfg.frame_mtu);
335                        debug!(?fc_cfg, "wrapping session socket with paced flow-control writer");
336                        Box::new(PacedWriter::new(socket, fc_cfg, clock, supply))
337                    }
338                    None => Box::new(socket),
339                }
340            } else {
341                // Stateless (unreliable) sockets support datagram-boundary preservation, driven by
342                // `NoDelay` (UDP-like framing). See `Capability::NoDelay`.
343                let socket_cfg = SessionSocketConfig {
344                    datagram: cfg.capabilities.contains(Capability::NoDelay),
345                    ..socket_cfg
346                };
347                debug!(?socket_cfg, "opening new stateless session socket");
348
349                Box::new(UnreliableSocket::<{ ApplicationData::PAYLOAD_SIZE }>::new_stateless(
350                    id,
351                    transport,
352                    socket_cfg,
353                    #[cfg(feature = "telemetry")]
354                    NoopTracker,
355                )?)
356            }
357        } else {
358            debug!("opening raw session socket");
359            Box::new(transport)
360        };
361
362        Ok(Self {
363            id,
364            inner,
365            routing,
366            cfg,
367            on_close,
368        })
369    }
370
371    /// ID of this Session.
372    pub fn id(&self) -> &SessionId {
373        &self.id
374    }
375
376    /// Routing options used to deliver data.
377    pub fn routing(&self) -> &DestinationRouting {
378        &self.routing
379    }
380
381    /// Configuration of this Session.
382    pub fn config(&self) -> &HoprSessionConfig {
383        &self.cfg
384    }
385}
386
387impl std::fmt::Debug for HoprSession {
388    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
389        f.debug_struct("Session")
390            .field("id", &self.id)
391            .field("routing", &self.routing)
392            .finish_non_exhaustive()
393    }
394}
395
396impl futures::AsyncRead for HoprSession {
397    #[instrument(name = "Session::poll_read", level = "trace", skip_all, fields(session_id = %self.id), ret)]
398    fn poll_read(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll<std::io::Result<usize>> {
399        let this = self.project();
400        let read = futures::ready!(this.inner.poll_read(cx, buf))?;
401        if read == 0 {
402            tracing::trace!("hopr session empty read");
403            // Empty read signals end of the socket, notify if needed
404            if let Some(notifier) = this.on_close.take() {
405                tracing::trace!("notifying read half closure of session");
406                notifier(*this.id, ClosureReason::EmptyRead);
407            }
408        }
409        Poll::Ready(Ok(read))
410    }
411}
412
413impl futures::AsyncWrite for HoprSession {
414    #[instrument(name = "Session::poll_write", level = "trace", skip_all, fields(session_id = %self.id), ret)]
415    fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll<std::io::Result<usize>> {
416        self.project().inner.poll_write(cx, buf)
417    }
418
419    #[instrument(name = "Session::poll_flush", level = "trace", skip_all, fields(session_id = %self.id), ret)]
420    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
421        self.project().inner.poll_flush(cx)
422    }
423
424    #[instrument(name = "Session::poll_close", level = "trace", skip_all, fields(session_id = %self.id), ret)]
425    fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
426        let this = self.project();
427        futures::ready!(this.inner.poll_close(cx))?;
428        tracing::trace!("hopr session closed");
429
430        #[cfg(feature = "telemetry")]
431        crate::telemetry::set_session_state(this.id, crate::telemetry::SessionLifecycleState::Closing);
432
433        if let Some(notifier) = this.on_close.take() {
434            tracing::trace!("notifying write half closure of session");
435            notifier(*this.id, ClosureReason::WriteClosed);
436        }
437
438        Poll::Ready(Ok(()))
439    }
440}
441
442#[cfg(feature = "runtime-tokio")]
443impl tokio::io::AsyncRead for HoprSession {
444    fn poll_read(
445        mut self: Pin<&mut Self>,
446        cx: &mut Context<'_>,
447        buf: &mut tokio::io::ReadBuf<'_>,
448    ) -> Poll<std::io::Result<()>> {
449        let slice = buf.initialize_unfilled();
450        let n = std::task::ready!(futures::AsyncRead::poll_read(self.as_mut(), cx, slice))?;
451        buf.advance(n);
452        Poll::Ready(Ok(()))
453    }
454}
455
456#[cfg(feature = "runtime-tokio")]
457impl tokio::io::AsyncWrite for HoprSession {
458    fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll<Result<usize, std::io::Error>> {
459        futures::AsyncWrite::poll_write(self.as_mut(), cx, buf)
460    }
461
462    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>> {
463        futures::AsyncWrite::poll_flush(self.as_mut(), cx)
464    }
465
466    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>> {
467        futures::AsyncWrite::poll_close(self.as_mut(), cx)
468    }
469}
470
471#[cfg(test)]
472mod tests {
473    use anyhow::Context;
474    use futures::{AsyncReadExt, AsyncWriteExt};
475    use hopr_api::types::{
476        crypto::prelude::*,
477        crypto_random::Randomizable,
478        internal::{prelude::HoprPseudonym, routing::RoutingOptions},
479        primitive::prelude::*,
480    };
481    use hopr_utils::network_types::prelude::SealedHost;
482
483    use super::*;
484
485    // --- ByteCapabilities tests ---
486
487    #[test]
488    fn byte_capabilities_roundtrip_via_u8() -> anyhow::Result<()> {
489        let flags: Capabilities = Capability::Segmentation.into();
490        let caps = ByteCapabilities::from(flags);
491        let byte_val: u8 = caps.into();
492        let restored = ByteCapabilities::try_from(byte_val)?;
493        assert_eq!(caps, restored);
494        Ok(())
495    }
496
497    #[test]
498    fn byte_capabilities_invalid_bits_are_rejected() {
499        // 0xFF has bits set that don't correspond to any Capability
500        assert!(ByteCapabilities::try_from(0xFF_u8).is_err());
501    }
502
503    #[test]
504    fn byte_capabilities_empty_is_zero() {
505        let caps = ByteCapabilities::from(Capabilities::empty());
506        let byte_val: u8 = caps.into();
507        assert_eq!(byte_val, 0);
508    }
509
510    #[test]
511    fn byte_capabilities_combined_flags() -> anyhow::Result<()> {
512        let caps: Capabilities = Capability::Segmentation | Capability::NoRateControl;
513        let byte_caps = ByteCapabilities::from(caps);
514        let byte_val: u8 = byte_caps.into();
515        let restored = ByteCapabilities::try_from(byte_val)?;
516        assert_eq!(*restored.as_ref(), caps);
517        Ok(())
518    }
519
520    // --- caps_to_ack_mode tests ---
521
522    #[test]
523    fn caps_to_ack_mode_both_when_ack_and_nack() {
524        let caps: Capabilities = Capability::RetransmissionAck | Capability::RetransmissionNack;
525        assert_eq!(caps_to_ack_mode(caps), AcknowledgementMode::Both);
526    }
527
528    #[test]
529    fn caps_to_ack_mode_full_when_only_ack() {
530        let caps: Capabilities = Capability::RetransmissionAck.into();
531        assert_eq!(caps_to_ack_mode(caps), AcknowledgementMode::Full);
532    }
533
534    #[test]
535    fn caps_to_ack_mode_partial_when_no_retransmission() {
536        let caps: Capabilities = Capability::Segmentation.into();
537        assert_eq!(caps_to_ack_mode(caps), AcknowledgementMode::Partial);
538    }
539
540    #[test]
541    fn caps_to_ack_mode_partial_when_empty() {
542        assert_eq!(caps_to_ack_mode(Capabilities::empty()), AcknowledgementMode::Partial);
543    }
544
545    #[test]
546    fn caps_to_ack_mode_should_be_partial_when_only_nack() {
547        let caps: Capabilities = Capability::RetransmissionNack.into();
548        assert_eq!(caps_to_ack_mode(caps), AcknowledgementMode::Partial);
549    }
550
551    // --- ClosureReason tests ---
552
553    #[test]
554    fn closure_reason_display_values_are_stable() {
555        let reasons = [
556            ClosureReason::WriteClosed,
557            ClosureReason::EmptyRead,
558            ClosureReason::Eviction,
559        ];
560        insta::assert_debug_snapshot!(reasons);
561    }
562
563    // --- HoprSessionConfig tests ---
564
565    #[test]
566    fn hopr_session_config_default_snapshot() {
567        let cfg = HoprSessionConfig::default();
568        insta::assert_yaml_snapshot!(cfg);
569    }
570
571    // --- SessionTarget tests ---
572
573    #[test]
574    fn session_target_variants_debug_snapshot() -> anyhow::Result<()> {
575        let targets: Vec<SessionTarget> = vec![
576            SessionTarget::UdpStream(SealedHost::Plain(
577                "127.0.0.1:8080".parse().context("parsing UDP target")?,
578            )),
579            SessionTarget::TcpStream(SealedHost::Plain("10.0.0.1:443".parse().context("parsing TCP target")?)),
580            SessionTarget::ExitNode(42),
581        ];
582        insta::assert_debug_snapshot!(targets);
583        Ok(())
584    }
585
586    // --- SessionId edge cases ---
587
588    #[test]
589    fn session_id_display_and_debug_should_be_identical() {
590        let id = HoprPseudonym::random();
591        assert_eq!(format!("{id}"), format!("{id:?}"));
592    }
593
594    #[test]
595    fn session_id_hash_eq_consistency() {
596        use std::collections::HashSet;
597        let pseudonym = HoprPseudonym::random();
598        let id1: SessionId = pseudonym;
599        let id2: SessionId = pseudonym;
600        let id3: SessionId = HoprPseudonym::random();
601
602        let mut set = HashSet::new();
603        set.insert(id1);
604        assert!(set.contains(&id2));
605        assert!(!set.contains(&id3), "different pseudonym should not be in the set");
606    }
607
608    // --- Existing tests ---
609
610    #[test_log::test(tokio::test)]
611    async fn test_session_bidirectional_flow_without_segmentation() -> anyhow::Result<()> {
612        let dst: Address = (&ChainKeypair::random()).into();
613        let id: SessionId = HoprPseudonym::random();
614        const DATA_LEN: usize = 5000;
615
616        let (alice_tx, bob_rx) = futures::channel::mpsc::unbounded::<(DestinationRouting, ApplicationDataOut)>();
617        let (bob_tx, alice_rx) = futures::channel::mpsc::unbounded::<(DestinationRouting, ApplicationDataOut)>();
618
619        let mut alice_session = HoprSession::new(
620            id,
621            DestinationRouting::forward_only(dst, RoutingOptions::Hops(0.try_into()?)),
622            Default::default(),
623            (
624                alice_tx,
625                alice_rx
626                    .map(|(_, data)| ApplicationDataIn {
627                        data: data.data,
628                        packet_info: Default::default(),
629                    })
630                    .inspect(|d| debug!("alice rcvd: {}", d.data.total_len())),
631            ),
632            None,
633        )?;
634
635        let mut bob_session = HoprSession::new(
636            id,
637            DestinationRouting::Return(id.into()),
638            Default::default(),
639            (
640                bob_tx,
641                bob_rx
642                    .map(|(_, data)| ApplicationDataIn {
643                        data: data.data,
644                        packet_info: Default::default(),
645                    })
646                    .inspect(|d| debug!("bob rcvd: {}", d.data.total_len())),
647            ),
648            None,
649        )?;
650
651        let alice_sent = hopr_api::types::crypto_random::random_bytes::<DATA_LEN>();
652        let bob_sent = hopr_api::types::crypto_random::random_bytes::<DATA_LEN>();
653
654        let mut bob_recv = [0u8; DATA_LEN];
655        let mut alice_recv = [0u8; DATA_LEN];
656
657        tokio::time::timeout(Duration::from_secs(1), alice_session.write_all(&alice_sent))
658            .await
659            .context("alice write failed")?
660            .context("alice write timed out")?;
661        alice_session.flush().await?;
662
663        tokio::time::timeout(Duration::from_secs(1), bob_session.write_all(&bob_sent))
664            .await
665            .context("bob write failed")?
666            .context("bob write timed out")?;
667        bob_session.flush().await?;
668
669        tokio::time::timeout(Duration::from_secs(1), bob_session.read_exact(&mut bob_recv))
670            .await
671            .context("bob read failed")?
672            .context("bob read timed out")?;
673
674        tokio::time::timeout(Duration::from_secs(1), alice_session.read_exact(&mut alice_recv))
675            .await
676            .context("alice read failed")?
677            .context("alice read timed out")?;
678
679        assert_eq!(&alice_sent, bob_recv.as_slice());
680        assert_eq!(bob_sent, alice_recv);
681
682        Ok(())
683    }
684
685    #[test_log::test(tokio::test)]
686    async fn test_session_bidirectional_flow_with_segmentation() -> anyhow::Result<()> {
687        let dst: Address = (&ChainKeypair::random()).into();
688        let id: SessionId = HoprPseudonym::random();
689        const DATA_LEN: usize = 5000;
690
691        let (alice_tx, bob_rx) = futures::channel::mpsc::unbounded::<(DestinationRouting, ApplicationDataOut)>();
692        let (bob_tx, alice_rx) = futures::channel::mpsc::unbounded::<(DestinationRouting, ApplicationDataOut)>();
693
694        let mut alice_session = HoprSession::new(
695            id,
696            DestinationRouting::forward_only(dst, RoutingOptions::Hops(0.try_into()?)),
697            HoprSessionConfig {
698                capabilities: Capability::Segmentation.into(),
699                ..Default::default()
700            },
701            (
702                alice_tx,
703                alice_rx
704                    .map(|(_, data)| ApplicationDataIn {
705                        data: data.data,
706                        packet_info: Default::default(),
707                    })
708                    .inspect(|d| debug!("alice rcvd: {}", d.data.total_len())),
709            ),
710            None,
711        )?;
712
713        let mut bob_session = HoprSession::new(
714            id,
715            DestinationRouting::Return(id.into()),
716            HoprSessionConfig {
717                capabilities: Capability::Segmentation.into(),
718                ..Default::default()
719            },
720            (
721                bob_tx,
722                bob_rx
723                    .map(|(_, data)| ApplicationDataIn {
724                        data: data.data,
725                        packet_info: Default::default(),
726                    })
727                    .inspect(|d| debug!("bob rcvd: {}", d.data.total_len())),
728            ),
729            None,
730        )?;
731
732        let alice_sent = hopr_api::types::crypto_random::random_bytes::<DATA_LEN>();
733        let bob_sent = hopr_api::types::crypto_random::random_bytes::<DATA_LEN>();
734
735        let mut bob_recv = [0u8; DATA_LEN];
736        let mut alice_recv = [0u8; DATA_LEN];
737
738        tokio::time::timeout(Duration::from_secs(1), alice_session.write_all(&alice_sent))
739            .await
740            .context("alice write failed")?
741            .context("alice write timed out")?;
742        alice_session.flush().await?;
743
744        tokio::time::timeout(Duration::from_secs(1), bob_session.write_all(&bob_sent))
745            .await
746            .context("bob write failed")?
747            .context("bob write timed out")?;
748        bob_session.flush().await?;
749
750        tokio::time::timeout(Duration::from_secs(1), bob_session.read_exact(&mut bob_recv))
751            .await
752            .context("bob read failed")?
753            .context("bob read timed out")?;
754
755        tokio::time::timeout(Duration::from_secs(1), alice_session.read_exact(&mut alice_recv))
756            .await
757            .context("alice read failed")?
758            .context("alice read timed out")?;
759
760        assert_eq!(alice_sent, bob_recv);
761        assert_eq!(bob_sent, alice_recv);
762
763        Ok(())
764    }
765}