1#[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 pub connected: Option<P2PConnectionStatus>,
38 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 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 max_connection_age: Option<Duration>,
260 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
273const METADATA_REFRESH_INTERVAL: Duration = Duration::from_secs(10);
276
277const RECEIVE_DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
282
283const SEND_TIMEOUT: Duration = Duration::from_mins(1);
287
288enum SendHalt<M> {
290 Shutdown,
292 Failed(anyhow::Error),
294 Replaced(DynP2PConnection<M>),
296 MaxAge,
298}
299
300enum ReceiveHalt {
301 Failed(anyhow::Error),
303 Stopped,
305}
306
307impl<M: Send + 'static> P2PConnectionStateMachine<M> {
308 async fn state_transition(mut self) -> Option<Self> {
309 match self.state {
310 P2PConnectionSMState::Disconnected {
311 backoff,
312 last_error,
313 } => {
314 self.common.status_sender.send_replace(P2PConnectionState {
315 connected: None,
316 last_error,
317 });
318
319 self.common.transition_disconnected(backoff).await
320 }
321 P2PConnectionSMState::Connected(connection) => {
322 let status_updates = connection.connection_status_updates();
325
326 self.common.refresh_status(&connection);
327
328 self.common
329 .transition_connected(connection, status_updates)
330 .await
331 }
332 }
333 .map(|state| P2PConnectionStateMachine {
334 common: self.common,
335 state,
336 })
337 }
338}
339
340impl<M: Send + 'static> P2PConnectionSMCommon<M> {
341 async fn transition_connected(
342 &mut self,
343 connection: DynP2PConnection<M>,
344 mut status_updates: Option<DynConnectionStatusUpdates>,
345 ) -> Option<P2PConnectionSMState<M>> {
346 let send_loop = Self::send_loop(
362 &connection,
363 &self.outgoing_receiver,
364 &self.incoming_connections,
365 self.connection_deadline,
366 &self.our_id_str,
367 &self.peer_id_str,
368 );
369 let stop_receiving = Notify::new();
370
371 let receive_loop = Self::receive_loop(
372 &connection,
373 &self.incoming_sender,
374 &stop_receiving,
375 &self.our_id_str,
376 &self.peer_id_str,
377 );
378
379 tokio::pin!(send_loop, receive_loop);
380
381 loop {
382 tokio::select! {
383 halt = &mut send_loop => {
384 if !matches!(halt, SendHalt::Shutdown) {
389 stop_receiving.notify_one();
390
391 let _ = timeout(RECEIVE_DRAIN_TIMEOUT, &mut receive_loop).await;
392 }
393
394 return match halt {
395 SendHalt::Shutdown => None,
396 SendHalt::Failed(e) => Some(self.disconnect(e)),
397 SendHalt::MaxAge => {
398 Some(self.disconnect(anyhow!("Connection exceeded the maximum age")))
399 }
400 SendHalt::Replaced(connection) => {
401 info!(target: LOG_NET_PEER, "Connected to peer");
402
403 self.connection_deadline =
404 self.max_connection_age.map(|age| Instant::now() + age);
405
406 Some(P2PConnectionSMState::Connected(connection))
407 }
408 };
409 },
410 halt = &mut receive_loop => {
411 return Some(self.disconnect(match halt {
412 ReceiveHalt::Failed(e) => e,
413 ReceiveHalt::Stopped => anyhow!("Receive half stopped unprompted"),
414 }));
415 },
416 Some(()) = async {
417 match status_updates.as_mut() {
418 Some(status_updates) => status_updates.next().await,
419 None => std::future::pending().await,
420 }
421 } => {
422 self.refresh_status(&connection);
423 },
424 () = sleep(METADATA_REFRESH_INTERVAL) => {
425 self.refresh_status(&connection);
430 },
431 }
432 }
433 }
434
435 async fn send_loop(
442 connection: &DynP2PConnection<M>,
443 outgoing_receiver: &Receiver<M>,
444 incoming_connections: &Receiver<DynP2PConnection<M>>,
445 connection_deadline: Option<Instant>,
446 our_id_str: &str,
447 peer_id_str: &str,
448 ) -> SendHalt<M> {
449 loop {
450 let message = tokio::select! {
453 message = outgoing_receiver.recv() => match message {
454 Ok(message) => message,
455 Err(..) => return SendHalt::Shutdown,
456 },
457 connection = incoming_connections.recv() => return match connection {
458 Ok(connection) => SendHalt::Replaced(connection),
459 Err(..) => SendHalt::Shutdown,
460 },
461 () = async {
462 match connection_deadline {
463 Some(deadline) => sleep_until(deadline).await,
464 None => std::future::pending().await,
465 }
466 } => return SendHalt::MaxAge,
467 };
468
469 PEER_MESSAGES_COUNT
470 .with_label_values(&[our_id_str, peer_id_str, "outgoing"])
471 .inc();
472
473 match timeout(SEND_TIMEOUT, connection.send(message)).await {
479 Ok(Ok(())) => {}
480 Ok(Err(e)) => return SendHalt::Failed(e),
481 Err(_) => {
482 return SendHalt::Failed(anyhow!(
483 "Sending a message did not complete within {SEND_TIMEOUT:?}"
484 ));
485 }
486 }
487 }
488 }
489
490 async fn receive_loop(
493 connection: &DynP2PConnection<M>,
494 incoming_sender: &Sender<M>,
495 stop: &Notify,
496 our_id_str: &str,
497 peer_id_str: &str,
498 ) -> ReceiveHalt {
499 loop {
500 let mut frame = tokio::select! {
503 biased;
504 () = stop.notified() => return ReceiveHalt::Stopped,
505 frame = connection.receive() => match frame {
506 Ok(frame) => frame,
507 Err(e) => return ReceiveHalt::Failed(e),
508 },
509 };
510
511 match frame.read_to_end().await {
512 Ok(message) => {
513 PEER_MESSAGES_COUNT
514 .with_label_values(&[our_id_str, peer_id_str, "incoming"])
515 .inc();
516
517 if incoming_sender.try_send(message).is_err() {
518 debug!(target: LOG_NET_PEER, "Incoming message channel is full");
519 }
520 }
521 Err(e) => return ReceiveHalt::Failed(e),
522 }
523 }
524 }
525
526 fn refresh_status(&self, connection: &DynP2PConnection<M>) {
531 let status = P2PConnectionStatus {
532 conn_type: connection
533 .connection_type()
534 .or_else(|| self.connector.connection_type(self.peer_id)),
535 rtt: connection.rtt(),
536 };
537
538 self.status_sender.send_replace(P2PConnectionState {
539 connected: Some(status),
540 last_error: None,
541 });
542 }
543
544 fn disconnect(&self, error: anyhow::Error) -> P2PConnectionSMState<M> {
545 let last_error = error.fmt_compact().to_string();
546
547 info!(
548 target: LOG_NET_PEER,
549 error = %last_error,
550 "Disconnected from peer"
551 );
552
553 PEER_DISCONNECT_COUNT
554 .with_label_values(&[&self.our_id_str, &self.peer_id_str])
555 .inc();
556
557 P2PConnectionSMState::Disconnected {
558 backoff: api_networking_backoff(),
559 last_error: Some(last_error),
560 }
561 }
562
563 async fn transition_disconnected(
564 &mut self,
565 mut backoff: FibonacciBackoff,
566 ) -> Option<P2PConnectionSMState<M>> {
567 tokio::select! {
568 connection = self.incoming_connections.recv() => {
569 PEER_CONNECT_COUNT
570 .with_label_values(&[self.our_id_str.as_str(), self.peer_id_str.as_str(), "incoming"])
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.ok()?))
578 },
579 () = sleep(backoff.next().expect("Unlimited retries")), if self.our_id < self.peer_id => {
581 info!(target: LOG_NET_PEER, "Attempting to reconnect to peer");
582
583 match self.connector.connect(self.peer_id).await {
584 Ok(connection) => {
585 PEER_CONNECT_COUNT
586 .with_label_values(&[self.our_id_str.as_str(), self.peer_id_str.as_str(), "outgoing"])
587 .inc();
588
589 info!(target: LOG_NET_PEER, "Connected to peer");
590
591 self.connection_deadline = self.max_connection_age.map(|age| Instant::now() + age);
592
593 Some(P2PConnectionSMState::Connected(connection))
594 }
595 Err(e) => {
596 let last_error = e.fmt_compact().to_string();
597
598 warn!(
599 target: LOG_CONSENSUS,
600 error = %last_error,
601 "Failed to connect to peer"
602 );
603
604 Some(P2PConnectionSMState::Disconnected {
605 backoff,
606 last_error: Some(last_error),
607 })
608 }
609 }
610 },
611 }
612 }
613}