Skip to main content

fedimint_server/net/
p2p.rs

1//! Implements a connection manager for communication with other federation
2//! members
3//!
4//! The main interface is [`fedimint_core::net::IP2PConnections`] and
5//! its main implementation is [`ReconnectP2PConnections`], see these for
6//! details.
7
8#[cfg(test)]
9mod tests;
10
11use std::collections::BTreeMap;
12use std::time::Duration;
13
14use anyhow::anyhow;
15use async_channel::{Receiver, Sender, bounded};
16use async_trait::async_trait;
17use fedimint_core::PeerId;
18use fedimint_core::net::{IP2PConnections, Recipient};
19use fedimint_core::task::{TaskGroup, sleep};
20use fedimint_core::util::FmtCompact;
21use fedimint_core::util::backoff_util::{FibonacciBackoff, api_networking_backoff};
22use fedimint_logging::{LOG_CONSENSUS, LOG_NET_PEER};
23use fedimint_server_core::dashboard_ui::P2PConnectionStatus;
24use futures::future::select_all;
25use futures::{FutureExt, StreamExt};
26use tokio::sync::{Notify, watch};
27use tokio::time::{Instant, sleep_until, timeout};
28use tracing::{Instrument, debug, info, info_span, warn};
29
30use crate::metrics::{PEER_CONNECT_COUNT, PEER_DISCONNECT_COUNT, PEER_MESSAGES_COUNT};
31use crate::net::p2p_connection::{DynConnectionStatusUpdates, DynP2PConnection};
32use crate::net::p2p_connector::DynP2PConnector;
33
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct P2PConnectionState {
36    /// Current connection metadata, or `None` if disconnected.
37    pub connected: Option<P2PConnectionStatus>,
38    /// Last disconnect/connect error observed while disconnected.
39    pub last_error: Option<String>,
40}
41
42pub type P2PStatusSenders = BTreeMap<PeerId, watch::Sender<P2PConnectionState>>;
43pub type P2PStatusReceivers = BTreeMap<PeerId, watch::Receiver<P2PConnectionState>>;
44
45pub fn p2p_status_channels(peers: Vec<PeerId>) -> (P2PStatusSenders, P2PStatusReceivers) {
46    let mut senders = BTreeMap::new();
47    let mut receivers = BTreeMap::new();
48
49    for peer in peers {
50        let (sender, receiver) = watch::channel(P2PConnectionState {
51            connected: None,
52            last_error: None,
53        });
54
55        senders.insert(peer, sender);
56        receivers.insert(peer, receiver);
57    }
58
59    (senders, receivers)
60}
61
62#[derive(Clone)]
63pub struct ReconnectP2PConnections<M> {
64    connections: BTreeMap<PeerId, P2PConnection<M>>,
65}
66
67impl<M: Send + 'static> ReconnectP2PConnections<M> {
68    pub fn new(
69        identity: PeerId,
70        connector: DynP2PConnector<M>,
71        task_group: &TaskGroup,
72        status_senders: P2PStatusSenders,
73        max_connection_age: Option<Duration>,
74    ) -> Self {
75        let mut connection_senders = BTreeMap::new();
76        let mut connections = BTreeMap::new();
77
78        for peer_id in connector.peers() {
79            assert_ne!(peer_id, identity);
80
81            let (connection_sender, connection_receiver) = bounded(4);
82
83            let connection = P2PConnection::new(
84                identity,
85                peer_id,
86                connector.clone(),
87                connection_receiver,
88                status_senders
89                    .get(&peer_id)
90                    .expect("No p2p status sender for peer")
91                    .clone(),
92                max_connection_age,
93                task_group,
94            );
95
96            connection_senders.insert(peer_id, connection_sender);
97            connections.insert(peer_id, connection);
98        }
99
100        task_group.spawn_cancellable("handle-incoming-p2p-connections", async move {
101            info!(target: LOG_NET_PEER, "Starting listening task for p2p connections");
102
103            loop {
104                match connector.accept().await {
105                    Ok((peer, connection)) => {
106                        if connection_senders
107                            .get_mut(&peer)
108                            .expect("Authenticating connectors dont return unknown peers")
109                            .send(connection)
110                            .await
111                            .is_err()
112                        {
113                            break;
114                        }
115                    },
116                    Err(err) => {
117                        warn!(target: LOG_NET_PEER, our_id = %identity, err = %err.fmt_compact(), "Error while opening incoming connection");
118                    }
119                }
120            }
121
122            info!(target: LOG_NET_PEER, "Shutting down listening task for p2p connections");
123        });
124
125        ReconnectP2PConnections { connections }
126    }
127}
128
129#[async_trait]
130impl<M: Clone + Send + 'static> IP2PConnections<M> for ReconnectP2PConnections<M> {
131    fn send(&self, recipient: Recipient, message: M) {
132        match recipient {
133            Recipient::Everyone => {
134                for connection in self.connections.values() {
135                    connection.try_send(message.clone());
136                }
137            }
138            Recipient::Peer(peer) => match self.connections.get(&peer) {
139                Some(connection) => {
140                    connection.try_send(message);
141                }
142                _ => {
143                    warn!(target: LOG_NET_PEER, "No connection for peer {peer}");
144                }
145            },
146        }
147    }
148
149    async fn receive(&self) -> Option<(PeerId, M)> {
150        select_all(self.connections.iter().map(|(&peer, connection)| {
151            Box::pin(connection.receive().map(move |m| m.map(|m| (peer, m))))
152        }))
153        .await
154        .0
155    }
156
157    async fn receive_from_peer(&self, peer: PeerId) -> Option<M> {
158        self.connections
159            .get(&peer)
160            .expect("No connection found for peer")
161            .receive()
162            .await
163    }
164}
165
166#[derive(Clone)]
167struct P2PConnection<M> {
168    outgoing_sender: Sender<M>,
169    incoming_receiver: Receiver<M>,
170}
171
172impl<M: Send + 'static> P2PConnection<M> {
173    fn new(
174        our_id: PeerId,
175        peer_id: PeerId,
176        connector: DynP2PConnector<M>,
177        incoming_connections: Receiver<DynP2PConnection<M>>,
178        status_sender: watch::Sender<P2PConnectionState>,
179        max_connection_age: Option<Duration>,
180        task_group: &TaskGroup,
181    ) -> P2PConnection<M> {
182        // We use small message queues here to avoid outdated messages such as requests
183        // for signed session outcomes to queue up while a peer is disconnected. The
184        // consensus expects an unreliable networking layer and will resend lost
185        // messages accordingly. Furthermore, during the DKG there will never be more
186        // than two messages in those channels at once, due to its sequential
187        // nature there.
188        let (outgoing_sender, outgoing_receiver) = bounded(5);
189        let (incoming_sender, incoming_receiver) = bounded(5);
190
191        task_group.spawn_cancellable(
192            format!("io-state-machine-{peer_id}"),
193            async move {
194                info!(target: LOG_NET_PEER, "Starting peer connection state machine");
195
196                let mut state_machine = P2PConnectionStateMachine {
197                    common: P2PConnectionSMCommon {
198                        incoming_sender,
199                        outgoing_receiver,
200                        our_id_str: our_id.to_string(),
201                        our_id,
202                        peer_id_str: peer_id.to_string(),
203                        peer_id,
204                        connector,
205                        incoming_connections,
206                        status_sender,
207                        max_connection_age,
208                        connection_deadline: None,
209                    },
210                    state: P2PConnectionSMState::Disconnected {
211                        backoff: api_networking_backoff(),
212                        last_error: None,
213                    },
214                };
215
216                while let Some(sm) = state_machine.state_transition().await {
217                    state_machine = sm;
218                }
219
220                info!(target: LOG_NET_PEER, "Shutting down peer connection state machine");
221            }
222            .instrument(info_span!("io-state-machine", ?peer_id)),
223        );
224
225        P2PConnection {
226            outgoing_sender,
227            incoming_receiver,
228        }
229    }
230
231    fn try_send(&self, message: M) {
232        if self.outgoing_sender.try_send(message).is_err() {
233            debug!(target: LOG_NET_PEER, "Outgoing message channel is full");
234        }
235    }
236
237    async fn receive(&self) -> Option<M> {
238        self.incoming_receiver.recv().await.ok()
239    }
240}
241
242struct P2PConnectionStateMachine<M> {
243    state: P2PConnectionSMState<M>,
244    common: P2PConnectionSMCommon<M>,
245}
246
247struct P2PConnectionSMCommon<M> {
248    incoming_sender: async_channel::Sender<M>,
249    outgoing_receiver: async_channel::Receiver<M>,
250    our_id: PeerId,
251    our_id_str: String,
252    peer_id: PeerId,
253    peer_id_str: String,
254    connector: DynP2PConnector<M>,
255    incoming_connections: Receiver<DynP2PConnection<M>>,
256    status_sender: watch::Sender<P2PConnectionState>,
257    /// Drop a connection once it exceeds this age so it is re-established;
258    /// disabled if `None`.
259    max_connection_age: Option<Duration>,
260    /// Point in time at which the current connection exceeds the maximum
261    /// age; set whenever a new connection is established.
262    connection_deadline: Option<Instant>,
263}
264
265enum P2PConnectionSMState<M> {
266    Disconnected {
267        backoff: FibonacciBackoff,
268        last_error: Option<String>,
269    },
270    Connected(DynP2PConnection<M>),
271}
272
273/// How often live connection metadata (currently only the round-trip time) is
274/// re-published while a connection stays up.
275const METADATA_REFRESH_INTERVAL: Duration = Duration::from_secs(10);
276
277/// How long a halted send half waits for a frame that is already being read.
278/// A peer that stalls part way through a frame must not hold the state machine
279/// forever, so the wait is bounded. The same grace was proposed for both halves
280/// in #9121.
281const RECEIVE_DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
282
283/// Why the send half of the connection stopped.
284enum SendHalt<M> {
285    /// The state machine is shutting down.
286    Shutdown,
287    /// The connection failed and should be re-established.
288    Failed(anyhow::Error),
289    /// The peer opened a replacement connection that supersedes this one.
290    Replaced(DynP2PConnection<M>),
291    /// The connection exceeded the maximum age and should be re-established.
292    MaxAge,
293}
294
295enum ReceiveHalt {
296    /// The connection failed and should be re-established.
297    Failed(anyhow::Error),
298    /// The send half halted and the receive half stopped between frames.
299    Stopped,
300}
301
302impl<M: Send + 'static> P2PConnectionStateMachine<M> {
303    async fn state_transition(mut self) -> Option<Self> {
304        match self.state {
305            P2PConnectionSMState::Disconnected {
306                backoff,
307                last_error,
308            } => {
309                self.common.status_sender.send_replace(P2PConnectionState {
310                    connected: None,
311                    last_error,
312                });
313
314                self.common.transition_disconnected(backoff).await
315            }
316            P2PConnectionSMState::Connected(connection) => {
317                // Subscribe before taking the snapshot so an update racing with
318                // the snapshot remains queued for the connected transition.
319                let status_updates = connection.connection_status_updates();
320
321                self.common.refresh_status(&connection);
322
323                self.common
324                    .transition_connected(connection, status_updates)
325                    .await
326            }
327        }
328        .map(|state| P2PConnectionStateMachine {
329            common: self.common,
330            state,
331        })
332    }
333}
334
335impl<M: Send + 'static> P2PConnectionSMCommon<M> {
336    async fn transition_connected(
337        &mut self,
338        connection: DynP2PConnection<M>,
339        mut status_updates: Option<DynConnectionStatusUpdates>,
340    ) -> Option<P2PConnectionSMState<M>> {
341        // The send and receive halves are driven as two long-lived futures that
342        // are never cancelled part-way through a message. Multiplexing a single
343        // send and a single receive in one `select!` meant that a send parked on
344        // transport flow control could no longer poll `receive`, so two peers
345        // that both started writing a message larger than the transport window
346        // deadlocked permanently and silently.
347        //
348        // Interleaving reads inside the parked send is *not* sufficient: it just
349        // turns a write/write deadlock into a read/read one. The halves have to
350        // be genuinely concurrent.
351        //
352        // Replacement connections and the max-age deadline are observed inside
353        // the send half, between messages, so they can never cancel a send that
354        // is already in flight. A send half that halts stops the receive half
355        // between frames for the same reason.
356        let send_loop = Self::send_loop(
357            &connection,
358            &self.outgoing_receiver,
359            &self.incoming_connections,
360            self.connection_deadline,
361            &self.our_id_str,
362            &self.peer_id_str,
363        );
364        let stop_receiving = Notify::new();
365
366        let receive_loop = Self::receive_loop(
367            &connection,
368            &self.incoming_sender,
369            &stop_receiving,
370            &self.our_id_str,
371            &self.peer_id_str,
372        );
373
374        tokio::pin!(send_loop, receive_loop);
375
376        loop {
377            tokio::select! {
378                halt = &mut send_loop => {
379                    // A frame may be part way through `read_to_end`, which is not
380                    // cancel-safe, so the receive half is stopped between frames
381                    // rather than dropped here. Shutdown is the exception: nothing
382                    // reads the incoming queue after it.
383                    if !matches!(halt, SendHalt::Shutdown) {
384                        stop_receiving.notify_one();
385
386                        let _ = timeout(RECEIVE_DRAIN_TIMEOUT, &mut receive_loop).await;
387                    }
388
389                    return match halt {
390                        SendHalt::Shutdown => None,
391                        SendHalt::Failed(e) => Some(self.disconnect(e)),
392                        SendHalt::MaxAge => {
393                            Some(self.disconnect(anyhow!("Connection exceeded the maximum age")))
394                        }
395                        SendHalt::Replaced(connection) => {
396                            info!(target: LOG_NET_PEER, "Connected to peer");
397
398                            self.connection_deadline =
399                                self.max_connection_age.map(|age| Instant::now() + age);
400
401                            Some(P2PConnectionSMState::Connected(connection))
402                        }
403                    };
404                },
405                halt = &mut receive_loop => {
406                    return Some(self.disconnect(match halt {
407                        ReceiveHalt::Failed(e) => e,
408                        ReceiveHalt::Stopped => anyhow!("Receive half stopped unprompted"),
409                    }));
410                },
411                Some(()) = async {
412                    match status_updates.as_mut() {
413                        Some(status_updates) => status_updates.next().await,
414                        None => std::future::pending().await,
415                    }
416                } => {
417                    self.refresh_status(&connection);
418                },
419                () = sleep(METADATA_REFRESH_INTERVAL) => {
420                    // Connection metadata used to be refreshed as a side effect
421                    // of every message, which the long-lived halves no longer
422                    // provide. Only `rtt` needs this; transport changes still
423                    // arrive on the status update stream above.
424                    self.refresh_status(&connection);
425                },
426            }
427        }
428    }
429
430    /// Drains the outgoing queue onto the connection. Replacement connections
431    /// and the max-age deadline are observed here, between sends, so they can
432    /// never cancel the non-cancel-safe `send` and silently drop a message
433    /// that the DKG would not resend. Only a failure of the connection itself
434    /// can still lose the message in flight; the networking layer is
435    /// unreliable by design.
436    async fn send_loop(
437        connection: &DynP2PConnection<M>,
438        outgoing_receiver: &Receiver<M>,
439        incoming_connections: &Receiver<DynP2PConnection<M>>,
440        connection_deadline: Option<Instant>,
441        our_id_str: &str,
442        peer_id_str: &str,
443    ) -> SendHalt<M> {
444        loop {
445            // All branches are cancel-safe: `recv` on an async channel does not
446            // take an item unless it completes and the timer holds no state.
447            let message = tokio::select! {
448                message = outgoing_receiver.recv() => match message {
449                    Ok(message) => message,
450                    Err(..) => return SendHalt::Shutdown,
451                },
452                connection = incoming_connections.recv() => return match connection {
453                    Ok(connection) => SendHalt::Replaced(connection),
454                    Err(..) => SendHalt::Shutdown,
455                },
456                () = async {
457                    match connection_deadline {
458                        Some(deadline) => sleep_until(deadline).await,
459                        None => std::future::pending().await,
460                    }
461                } => return SendHalt::MaxAge,
462            };
463
464            PEER_MESSAGES_COUNT
465                .with_label_values(&[our_id_str, peer_id_str, "outgoing"])
466                .inc();
467
468            if let Err(e) = connection.send(message).await {
469                return SendHalt::Failed(e);
470            }
471        }
472    }
473
474    /// Drains the connection into the incoming queue. Only returns when the
475    /// connection fails.
476    async fn receive_loop(
477        connection: &DynP2PConnection<M>,
478        incoming_sender: &Sender<M>,
479        stop: &Notify,
480        our_id_str: &str,
481        peer_id_str: &str,
482    ) -> ReceiveHalt {
483        loop {
484            // `receive` is cancel-safe, `read_to_end` below is not, so this is
485            // the only point at which the halted send half may stop us.
486            let mut frame = tokio::select! {
487                biased;
488                () = stop.notified() => return ReceiveHalt::Stopped,
489                frame = connection.receive() => match frame {
490                    Ok(frame) => frame,
491                    Err(e) => return ReceiveHalt::Failed(e),
492                },
493            };
494
495            match frame.read_to_end().await {
496                Ok(message) => {
497                    PEER_MESSAGES_COUNT
498                        .with_label_values(&[our_id_str, peer_id_str, "incoming"])
499                        .inc();
500
501                    if incoming_sender.try_send(message).is_err() {
502                        debug!(target: LOG_NET_PEER, "Incoming message channel is full");
503                    }
504                }
505                Err(e) => return ReceiveHalt::Failed(e),
506            }
507        }
508    }
509
510    /// Publishes a fresh metadata snapshot for the live connection.
511    ///
512    /// Refreshing in place rather than re-entering the connected state means a
513    /// send already in flight is never cancelled by a status event.
514    fn refresh_status(&self, connection: &DynP2PConnection<M>) {
515        let status = P2PConnectionStatus {
516            conn_type: connection
517                .connection_type()
518                .or_else(|| self.connector.connection_type(self.peer_id)),
519            rtt: connection.rtt(),
520        };
521
522        self.status_sender.send_replace(P2PConnectionState {
523            connected: Some(status),
524            last_error: None,
525        });
526    }
527
528    fn disconnect(&self, error: anyhow::Error) -> P2PConnectionSMState<M> {
529        let last_error = error.fmt_compact().to_string();
530
531        info!(
532            target: LOG_NET_PEER,
533            error = %last_error,
534            "Disconnected from peer"
535        );
536
537        PEER_DISCONNECT_COUNT
538            .with_label_values(&[&self.our_id_str, &self.peer_id_str])
539            .inc();
540
541        P2PConnectionSMState::Disconnected {
542            backoff: api_networking_backoff(),
543            last_error: Some(last_error),
544        }
545    }
546
547    async fn transition_disconnected(
548        &mut self,
549        mut backoff: FibonacciBackoff,
550    ) -> Option<P2PConnectionSMState<M>> {
551        tokio::select! {
552            connection = self.incoming_connections.recv() => {
553                PEER_CONNECT_COUNT
554                    .with_label_values(&[self.our_id_str.as_str(), self.peer_id_str.as_str(), "incoming"])
555                    .inc();
556
557                info!(target: LOG_NET_PEER, "Connected to peer");
558
559                self.connection_deadline = self.max_connection_age.map(|age| Instant::now() + age);
560
561                Some(P2PConnectionSMState::Connected(connection.ok()?))
562            },
563            // to prevent "reconnection ping-pongs", only the side with lower PeerId reconnects
564            () = sleep(backoff.next().expect("Unlimited retries")), if self.our_id < self.peer_id => {
565                info!(target: LOG_NET_PEER, "Attempting to reconnect to peer");
566
567                match self.connector.connect(self.peer_id).await {
568                    Ok(connection) => {
569                        PEER_CONNECT_COUNT
570                            .with_label_values(&[self.our_id_str.as_str(), self.peer_id_str.as_str(), "outgoing"])
571                            .inc();
572
573                        info!(target: LOG_NET_PEER, "Connected to peer");
574
575                        self.connection_deadline = self.max_connection_age.map(|age| Instant::now() + age);
576
577                        Some(P2PConnectionSMState::Connected(connection))
578                    }
579                    Err(e) => {
580                        let last_error = e.fmt_compact().to_string();
581
582                        warn!(
583                            target: LOG_CONSENSUS,
584                            error = %last_error,
585                            "Failed to connect to peer"
586                        );
587
588                        Some(P2PConnectionSMState::Disconnected {
589                            backoff,
590                            last_error: Some(last_error),
591                        })
592                    }
593                }
594            },
595        }
596    }
597}