Skip to main content

fedimint_server/net/
p2p_connection.rs

1#[cfg(test)]
2mod tests;
3
4use std::io::Cursor;
5use std::marker::PhantomData;
6use std::pin::Pin;
7use std::sync::Arc;
8use std::time::Duration;
9
10use anyhow::Context;
11use async_trait::async_trait;
12use bytes::{Bytes, BytesMut};
13use fedimint_core::encoding::{Decodable, Encodable};
14use fedimint_core::module::registry::ModuleDecoderRegistry;
15use fedimint_server_core::dashboard_ui::ConnectionType;
16use futures::stream::{SplitSink, SplitStream};
17use futures::{SinkExt, Stream, StreamExt};
18use iroh_next::endpoint::{Connection as IrohV1Connection, RecvStream as IrohV1RecvStream};
19use serde::Serialize;
20use serde::de::DeserializeOwned;
21use tokio::net::TcpStream;
22use tokio_rustls::TlsStream;
23use tokio_util::codec::{Framed, LengthDelimitedCodec};
24
25/// Maximum size of a p2p message in bytes. The largest message we expect to
26/// receive is a signed session outcome.
27pub const MAX_P2P_MESSAGE_SIZE: usize = 10_000_000;
28
29pub type DynP2PConnection<M> = Arc<dyn IP2PConnection<M>>;
30
31pub type DynIP2PFrame<M> = Box<dyn IP2PFrame<M>>;
32
33/// Type-erased stream notifying the P2P state machine that connection metadata
34/// may have changed.
35pub type DynConnectionStatusUpdates = Pin<Box<dyn Stream<Item = ()> + Send + 'static>>;
36
37#[async_trait]
38pub trait IP2PFrame<M>: Send + 'static {
39    /// Read the entire frame from the connection and deserialize it into a
40    /// message. This is *not* required to be cancel-safe.
41    async fn read_to_end(&mut self) -> anyhow::Result<M>;
42
43    fn into_dyn(self) -> DynIP2PFrame<M>
44    where
45        Self: Sized,
46    {
47        Box::new(self)
48    }
49}
50
51#[async_trait]
52pub trait IP2PConnection<M>: Send + Sync + 'static {
53    /// Send a message over the connection. This is *not* required to be
54    /// cancel-safe.
55    ///
56    /// Takes `&self` so that the send and receive halves can be driven
57    /// concurrently. A send parked on transport flow control must never stop
58    /// the peer's stream from being drained, or two peers that are both
59    /// sending an oversized message deadlock permanently.
60    ///
61    /// While `send` and `receive` may run concurrently with each other, each
62    /// of them must have at most one caller at a time: a second concurrent
63    /// `receive` would steal frames from the first and silently reorder the
64    /// message stream.
65    async fn send(&self, message: M) -> anyhow::Result<()>;
66
67    /// Receive a p2p frame from the connection. This is *required* to be
68    /// cancel-safe. See [`Self::send`] for the concurrency contract.
69    async fn receive(&self) -> anyhow::Result<DynIP2PFrame<M>>;
70
71    /// Get the round-trip time of the connection.
72    fn rtt(&self) -> Option<Duration>;
73
74    /// Get the transport type currently backing this live connection.
75    fn connection_type(&self) -> Option<ConnectionType> {
76        None
77    }
78
79    /// Subscribe to notifications that the live connection metadata may have
80    /// changed.
81    ///
82    /// Implementations should treat these as wake-ups only. The state machine
83    /// reads a fresh [`Self::connection_type`] and [`Self::rtt`] snapshot after
84    /// each notification. `None` means notifications are unsupported, while the
85    /// end of a returned stream means there will be no more notifications.
86    fn connection_status_updates(&self) -> Option<DynConnectionStatusUpdates> {
87        None
88    }
89
90    fn into_dyn(self) -> DynP2PConnection<M>
91    where
92        Self: Sized,
93    {
94        Arc::new(self)
95    }
96}
97
98/// Implementations of the IP2PFrame and IP2PConnection traits for TLS
99
100#[async_trait]
101impl<M> IP2PFrame<M> for BytesMut
102where
103    M: Decodable + DeserializeOwned + Send + 'static,
104{
105    async fn read_to_end(&mut self) -> anyhow::Result<M> {
106        if let Ok(message) = M::consensus_decode_whole(self, &ModuleDecoderRegistry::default()) {
107            return Ok(message);
108        }
109
110        Ok(bincode::deserialize_from(Cursor::new(&**self))?)
111    }
112}
113
114type TlsFramed = Framed<TlsStream<TcpStream>, LengthDelimitedCodec>;
115
116/// A TLS p2p connection.
117///
118/// A single `Framed` cannot serve both directions at once, so the sink and
119/// stream halves are split and guarded separately. That is what lets a send
120/// parked on socket buffers coexist with a concurrent receive.
121pub struct TlsP2PConnection<M> {
122    sink: tokio::sync::Mutex<SplitSink<TlsFramed, Bytes>>,
123    stream: tokio::sync::Mutex<SplitStream<TlsFramed>>,
124    // `fn(M) -> M` keeps the struct `Send + Sync` regardless of `M`, which is
125    // never stored, only sent.
126    _message: PhantomData<fn(M) -> M>,
127}
128
129impl<M> TlsP2PConnection<M> {
130    pub fn new(framed: TlsFramed) -> Self {
131        let (sink, stream) = framed.split();
132
133        Self {
134            sink: tokio::sync::Mutex::new(sink),
135            stream: tokio::sync::Mutex::new(stream),
136            _message: PhantomData,
137        }
138    }
139}
140
141#[async_trait]
142impl<M> IP2PConnection<M> for TlsP2PConnection<M>
143where
144    M: Encodable + Decodable + Serialize + DeserializeOwned + Send + 'static,
145{
146    async fn send(&self, message: M) -> anyhow::Result<()> {
147        let mut bytes = Vec::new();
148
149        bincode::serialize_into(&mut bytes, &message)?;
150
151        // The locks are never contended by contract — see the trait doc — so
152        // a second concurrent caller fails loudly instead of queuing behind
153        // the lock and interleaving with the first.
154        let mut sink = self
155            .sink
156            .try_lock()
157            .expect("send has at most one caller at a time");
158
159        SinkExt::send(&mut *sink, Bytes::from_owner(bytes)).await?;
160
161        Ok(())
162    }
163
164    async fn receive(&self) -> anyhow::Result<DynIP2PFrame<M>> {
165        let mut stream = self
166            .stream
167            .try_lock()
168            .expect("receive has at most one caller at a time");
169
170        let message = stream
171            .next()
172            .await
173            .context("Framed stream is closed")??
174            .into_dyn();
175
176        Ok(message)
177    }
178
179    fn rtt(&self) -> Option<Duration> {
180        None
181    }
182
183    fn connection_type(&self) -> Option<ConnectionType> {
184        Some(ConnectionType::Direct)
185    }
186}
187
188/// Compatibility implementations for the public Iroh 0.35 connection types.
189
190#[async_trait]
191impl<M> IP2PFrame<M> for iroh::endpoint::RecvStream
192where
193    M: Decodable + DeserializeOwned + Send + 'static,
194{
195    async fn read_to_end(&mut self) -> anyhow::Result<M> {
196        let bytes = self.read_to_end(MAX_P2P_MESSAGE_SIZE).await?;
197
198        if let Ok(message) = M::consensus_decode_whole(&bytes, &ModuleDecoderRegistry::default()) {
199            return Ok(message);
200        }
201
202        Ok(bincode::deserialize_from(Cursor::new(&bytes))?)
203    }
204}
205
206#[async_trait]
207impl<M> IP2PConnection<M> for iroh::endpoint::Connection
208where
209    M: Encodable + Decodable + Serialize + DeserializeOwned + Send + 'static,
210{
211    async fn send(&self, message: M) -> anyhow::Result<()> {
212        let mut bytes = Vec::new();
213
214        bincode::serialize_into(&mut bytes, &message)?;
215
216        let mut sink = self.open_uni().await?;
217
218        sink.write_all(&bytes).await?;
219        sink.finish()?;
220
221        Ok(())
222    }
223
224    async fn receive(&self) -> anyhow::Result<DynIP2PFrame<M>> {
225        Ok(self.accept_uni().await?.into_dyn())
226    }
227
228    fn rtt(&self) -> Option<Duration> {
229        Some(iroh::endpoint::Connection::rtt(self))
230    }
231}
232
233/// Implementations of the P2P traits for Iroh 1.0.
234
235#[async_trait]
236impl<M> IP2PFrame<M> for IrohV1RecvStream
237where
238    M: Decodable + DeserializeOwned + Send + 'static,
239{
240    async fn read_to_end(&mut self) -> anyhow::Result<M> {
241        let bytes = self.read_to_end(MAX_P2P_MESSAGE_SIZE).await?;
242
243        if let Ok(message) = M::consensus_decode_whole(&bytes, &ModuleDecoderRegistry::default()) {
244            return Ok(message);
245        }
246
247        Ok(bincode::deserialize_from(Cursor::new(&bytes))?)
248    }
249}
250
251#[async_trait]
252impl<M> IP2PConnection<M> for IrohV1Connection
253where
254    M: Encodable + Decodable + Serialize + DeserializeOwned + Send + 'static,
255{
256    async fn send(&self, message: M) -> anyhow::Result<()> {
257        let mut bytes = Vec::new();
258
259        bincode::serialize_into(&mut bytes, &message)?;
260
261        let mut sink = self.open_uni().await?;
262
263        sink.write_all(&bytes).await?;
264
265        sink.finish()?;
266
267        Ok(())
268    }
269
270    async fn receive(&self) -> anyhow::Result<DynIP2PFrame<M>> {
271        Ok(self.accept_uni().await?.into_dyn())
272    }
273
274    fn rtt(&self) -> Option<Duration> {
275        self.paths()
276            .iter()
277            .find(iroh_next::endpoint::Path::is_selected)
278            .and_then(|path| IrohV1Connection::rtt(self, path.id()))
279    }
280
281    fn connection_type(&self) -> Option<ConnectionType> {
282        connection_type_from_paths(self.paths().iter().map(|path| IrohPath {
283            selected: path.is_selected(),
284            kind: if path.is_ip() {
285                IrohPathKind::Direct
286            } else if path.is_relay() {
287                IrohPathKind::Relay
288            } else {
289                IrohPathKind::Unknown
290            },
291        }))
292    }
293
294    fn connection_status_updates(&self) -> Option<DynConnectionStatusUpdates> {
295        Some(Box::pin(self.path_events().map(|_| ())))
296    }
297}
298
299#[derive(Clone, Copy)]
300enum IrohPathKind {
301    Direct,
302    Relay,
303    Unknown,
304}
305
306#[derive(Clone, Copy)]
307struct IrohPath {
308    selected: bool,
309    kind: IrohPathKind,
310}
311
312fn connection_type_from_paths(paths: impl IntoIterator<Item = IrohPath>) -> Option<ConnectionType> {
313    let mut direct = false;
314    let mut relay = false;
315    for path in paths {
316        if path.selected {
317            direct |= matches!(path.kind, IrohPathKind::Direct);
318            relay |= matches!(path.kind, IrohPathKind::Relay);
319        }
320    }
321
322    match (direct, relay) {
323        (true, true) => Some(ConnectionType::Mixed),
324        (true, false) => Some(ConnectionType::Direct),
325        (false, true) => Some(ConnectionType::Relay),
326        (false, false) => None,
327    }
328}