Skip to main content

stratum_apps/network_helpers/
mod.rs

1//! High-level networking utilities for SV2 connections
2//!
3//! This module provides connection management, encrypted streams, and protocol handling
4//! for Stratum V2 applications. It includes support for:
5//!
6//! - Noise-encrypted connections ([`noise_connection`], [`noise_stream`])
7//! - SV1 protocol connections ([`sv1_connection`]) - when `sv1` feature is enabled
8//! - Hostname resolution ([`resolve_hostname`])
9//!
10//! Originally from the `network_helpers_sv2` crate.
11
12pub mod noise_connection;
13pub mod noise_stream;
14pub mod resolve_hostname;
15
16#[cfg(feature = "sv1")]
17pub mod sv1_connection;
18
19pub use resolve_hostname::{resolve_host, resolve_host_port, ResolveError};
20
21use async_channel::{RecvError, SendError};
22use std::{fmt, time::Duration};
23use stratum_core::{
24    binary_sv2::{Deserialize, GetSize, Serialize},
25    codec_sv2::{Error as CodecError, HandshakeRole},
26    noise_sv2::{Initiator, Responder},
27};
28use tokio::net::TcpStream;
29
30use crate::{
31    key_utils::{Secp256k1PublicKey, Secp256k1SecretKey},
32    network_helpers::noise_stream::NoiseTcpStream,
33};
34
35/// Networking errors that can occur in SV2 connections
36#[derive(Debug)]
37pub enum Error {
38    /// Invalid handshake message received from remote peer
39    HandshakeRemoteInvalidMessage,
40    /// Error from the codec layer
41    CodecError(CodecError),
42    /// Error receiving from async channel
43    RecvError,
44    /// Error sending to async channel
45    SendError,
46    /// Socket was closed, likely by the peer
47    SocketClosed,
48    /// Handshake timeout
49    HandshakeTimeout,
50    /// Invalid key provided to construct an Initiator or Responder
51    InvalidKey,
52    /// DNS resolution failed for a hostname
53    DnsResolutionFailed(String),
54}
55
56impl fmt::Display for Error {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        match self {
59            Error::HandshakeRemoteInvalidMessage => {
60                write!(f, "Invalid handshake message received from remote peer")
61            }
62
63            Error::CodecError(e) => write!(f, "{}", e),
64
65            Error::RecvError => write!(f, "Error receiving from async channel"),
66
67            Error::SendError => write!(f, "Error sending to async channel"),
68
69            Error::SocketClosed => write!(f, "Socket was closed (likely by the peer)"),
70
71            Error::HandshakeTimeout => write!(f, "Handshake timeout"),
72
73            Error::InvalidKey => write!(f, "Invalid key provided for handshake"),
74
75            Error::DnsResolutionFailed(msg) => write!(f, "DNS resolution failed: {msg}"),
76        }
77    }
78}
79
80impl From<CodecError> for Error {
81    fn from(e: CodecError) -> Self {
82        Error::CodecError(e)
83    }
84}
85
86impl From<RecvError> for Error {
87    fn from(_: RecvError) -> Self {
88        Error::RecvError
89    }
90}
91
92impl<T> From<SendError<T>> for Error {
93    fn from(_: SendError<T>) -> Self {
94        Error::SendError
95    }
96}
97
98impl From<ResolveError> for Error {
99    fn from(e: ResolveError) -> Self {
100        Error::DnsResolutionFailed(e.to_string())
101    }
102}
103
104/// Default handshake timeout used by [`connect_with_noise`] and [`accept_noise_connection`].
105/// Use [`noise_stream::NoiseTcpStream::new`] directly to override.
106const NOISE_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
107
108/// Default timeout for establishing outbound TCP connections to SV2 peers.
109/// This keeps fallback attempts bounded when a remote endpoint is unreachable or filtered.
110pub const TCP_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
111
112/// Connects to an upstream server as a Noise initiator, returning the split read/write halves.
113///
114/// The handshake timeout is opinionated and fixed at [`NOISE_HANDSHAKE_TIMEOUT`]. If you need a
115/// custom timeout, use [`noise_stream::NoiseTcpStream::new`] directly.
116///
117/// Pass `Some(key)` to verify the server's authority public key, or `None` to skip
118/// verification (encrypted but unauthenticated — use only on trusted networks).
119pub async fn connect_with_noise<Message>(
120    stream: TcpStream,
121    authority_pub_key: Option<Secp256k1PublicKey>,
122) -> Result<NoiseTcpStream<Message>, Error>
123where
124    Message: Serialize + Deserialize<'static> + GetSize + Send + 'static,
125{
126    let initiator = match authority_pub_key {
127        Some(key) => Initiator::from_raw_k(key.into_bytes()).map_err(|_| Error::InvalidKey)?,
128        None => Initiator::without_pk().map_err(|_| Error::InvalidKey)?,
129    };
130    let stream = noise_stream::NoiseTcpStream::new(
131        stream,
132        HandshakeRole::Initiator(initiator),
133        NOISE_HANDSHAKE_TIMEOUT,
134    )
135    .await?;
136    Ok(stream)
137}
138
139/// Accepts a downstream connection as a Noise responder, returning the split read/write halves.
140///
141/// The handshake timeout is opinionated and fixed at [`NOISE_HANDSHAKE_TIMEOUT`]. If you need a
142/// custom timeout, use [`noise_stream::NoiseTcpStream::new`] directly.
143///
144/// `cert_validity` controls how long the generated Noise certificate is valid,
145/// which is independent of the handshake timeout.
146pub async fn accept_noise_connection<Message>(
147    stream: TcpStream,
148    pub_key: Secp256k1PublicKey,
149    prv_key: Secp256k1SecretKey,
150    cert_validity: u64,
151) -> Result<NoiseTcpStream<Message>, Error>
152where
153    Message: Serialize + Deserialize<'static> + GetSize + Send + 'static,
154{
155    let responder = Responder::from_authority_kp(
156        &pub_key.into_bytes(),
157        &prv_key.into_bytes(),
158        Duration::from_secs(cert_validity),
159    )
160    .map_err(|_| Error::InvalidKey)?;
161    let stream = noise_stream::NoiseTcpStream::new(
162        stream,
163        HandshakeRole::Responder(responder),
164        NOISE_HANDSHAKE_TIMEOUT,
165    )
166    .await?;
167    Ok(stream)
168}