Skip to main content

cuprate_p2p/
connection_maintainer.rs

1//! Outbound Connection Maintainer.
2//!
3//! This module handles maintaining the number of outbound connections defined in the [`P2PConfig`].
4//! It also handles making extra connections when the peer set is under load or when we need data that
5//! no connected peer has.
6use std::sync::Arc;
7
8use rand::{distributions::Bernoulli, prelude::*};
9use tokio::{
10    sync::{mpsc, OwnedSemaphorePermit, Semaphore},
11    task::JoinSet,
12    time::{sleep, timeout},
13};
14use tower::{Service, ServiceExt};
15use tracing::{instrument, Instrument, Span};
16
17use cuprate_p2p_core::{
18    client::{Client, ConnectRequest, HandshakeError, PeerSyncCallback},
19    services::{AddressBookRequest, AddressBookResponse},
20    AddressBook, NetworkZone,
21};
22
23use crate::{
24    config::P2PConfig,
25    constants::{HANDSHAKE_TIMEOUT, MAX_SEED_CONNECTIONS, OUTBOUND_CONNECTION_ATTEMPT_TIMEOUT},
26};
27
28enum OutboundConnectorError {
29    MaxConnections,
30    FailedToConnectToSeeds,
31    NoAvailablePeers,
32}
33
34/// A request from the peer set to make an outbound connection.
35///
36/// This will only be sent when the peer set is under load from the rest of Cuprate or the peer
37/// set needs specific data that none of the currently connected peers have.
38pub struct MakeConnectionRequest {
39    /// The block needed that no connected peers have due to pruning.
40    block_needed: Option<usize>,
41}
42
43/// The outbound connection count keeper.
44///
45/// This handles maintaining a minimum number of connections and making extra connections when needed, upto a maximum.
46pub struct OutboundConnectionKeeper<Z: NetworkZone, A, C> {
47    /// The pool of currently connected peers.
48    pub new_peers_tx: mpsc::Sender<Client<Z>>,
49    /// The channel that tells us to make new _extra_ outbound connections.
50    pub make_connection_rx: mpsc::Receiver<MakeConnectionRequest>,
51    /// The address book service
52    pub address_book_svc: A,
53    /// The service to connect to a specific peer.
54    pub connector_svc: C,
55    /// A semaphore to keep the amount of outbound peers constant.
56    pub outbound_semaphore: Arc<Semaphore>,
57    /// The amount of peers we connected to because we needed more peers. If the `outbound_semaphore`
58    /// is full, and we need to connect to more peers for blocks or because not enough peers are ready
59    /// we add a permit to the semaphore and keep track here, upto a value in config.
60    pub extra_peers: usize,
61    /// The p2p config.
62    pub config: P2PConfig<Z>,
63    /// The [`Bernoulli`] distribution, when sampled will return true if we should connect to a gray peer or
64    /// false if we should connect to a white peer.
65    ///
66    /// This is weighted to the percentage given in `config`.
67    pub peer_type_gen: Bernoulli,
68    /// A callback used to notify the syncer about peer sync state changes.
69    pub peer_sync_callback: Option<PeerSyncCallback>,
70}
71
72impl<Z, A, C> OutboundConnectionKeeper<Z, A, C>
73where
74    Z: NetworkZone,
75    A: AddressBook<Z>,
76    C: Service<ConnectRequest<Z>, Response = Client<Z>, Error = HandshakeError>,
77    C::Future: Send + 'static,
78{
79    pub fn new(
80        config: P2PConfig<Z>,
81        new_peers_tx: mpsc::Sender<Client<Z>>,
82        make_connection_rx: mpsc::Receiver<MakeConnectionRequest>,
83        address_book_svc: A,
84        connector_svc: C,
85        peer_sync_callback: Option<PeerSyncCallback>,
86    ) -> Self {
87        let peer_type_gen = Bernoulli::new(config.gray_peers_percent)
88            .expect("Gray peer percent is incorrect should be 0..=1");
89
90        Self {
91            new_peers_tx,
92            make_connection_rx,
93            address_book_svc,
94            connector_svc,
95            outbound_semaphore: Arc::new(Semaphore::new(config.outbound_connections)),
96            extra_peers: 0,
97            config,
98            peer_type_gen,
99            peer_sync_callback,
100        }
101    }
102
103    /// Connects to random seeds to get peers and immediately disconnects
104    #[instrument(level = "info", skip(self))]
105    async fn connect_to_random_seeds(&mut self) -> Result<(), OutboundConnectorError> {
106        let seeds = self
107            .config
108            .seeds
109            .choose_multiple(&mut thread_rng(), MAX_SEED_CONNECTIONS);
110
111        assert_ne!(seeds.len(), 0, "No seed nodes available to get peers from");
112
113        let mut allowed_errors = seeds.len();
114
115        let mut handshake_futs = JoinSet::new();
116
117        for seed in seeds {
118            tracing::info!("Getting peers from seed node: {}", seed);
119
120            let addr = *seed;
121            let fut = timeout(
122                HANDSHAKE_TIMEOUT,
123                self.connector_svc
124                    .ready()
125                    .await
126                    .expect("Connector had an error in `poll_ready`")
127                    .call(ConnectRequest { addr, permit: None }),
128            );
129            // Spawn the handshake on a separate task with a timeout, so we don't get stuck connecting to a peer.
130            handshake_futs.spawn(
131                async move {
132                    match fut.await {
133                        Err(_) => {
134                            tracing::warn!("Timed out connecting to seed node: {addr}");
135                            false
136                        }
137                        Ok(Err(e)) => {
138                            tracing::warn!("Failed to connect to seed node {addr}: {e}");
139                            false
140                        }
141                        Ok(Ok(_)) => true,
142                    }
143                }
144                .instrument(Span::current()),
145            );
146        }
147
148        while let Some(res) = handshake_futs.join_next().await {
149            if !res.unwrap_or(false) {
150                allowed_errors -= 1;
151            }
152        }
153
154        if allowed_errors == 0 {
155            Err(OutboundConnectorError::FailedToConnectToSeeds)
156        } else {
157            Ok(())
158        }
159    }
160
161    /// Connects to a given outbound peer.
162    #[instrument(level = "info", skip_all)]
163    async fn connect_to_outbound_peer(&mut self, permit: OwnedSemaphorePermit, addr: Z::Addr) {
164        let new_peers_tx = self.new_peers_tx.clone();
165        let peer_sync_callback = self.peer_sync_callback.clone();
166        let connection_fut = self
167            .connector_svc
168            .ready()
169            .await
170            .expect("Connector had an error in `poll_ready`")
171            .call(ConnectRequest {
172                addr,
173                permit: Some(permit),
174            });
175
176        tokio::spawn(
177            async move {
178                if let Ok(Ok(peer)) = timeout(HANDSHAKE_TIMEOUT, connection_fut).await {
179                    let csd = peer.info.core_sync_data.lock().unwrap().clone();
180                    if new_peers_tx.send(peer).await.is_ok() {
181                        if let Some(ref peer_sync_callback) = peer_sync_callback {
182                            peer_sync_callback.call(&csd);
183                        }
184                    }
185                }
186            }
187            .instrument(Span::current()),
188        );
189    }
190
191    /// Handles a request from the peer set for more peers.
192    #[expect(
193        clippy::significant_drop_tightening,
194        reason = "we need to hold onto a permit"
195    )]
196    async fn handle_peer_request(
197        &mut self,
198        req: &MakeConnectionRequest,
199    ) -> Result<(), OutboundConnectorError> {
200        // try to get a permit.
201        let permit = Arc::clone(&self.outbound_semaphore)
202            .try_acquire_owned()
203            .or_else(|_| {
204                // if we can't get a permit add one if we are below the max number of connections.
205                if self.extra_peers >= self.config.extra_outbound_connections {
206                    // If we can't add a permit return an error.
207                    Err(OutboundConnectorError::MaxConnections)
208                } else {
209                    self.outbound_semaphore.add_permits(1);
210                    self.extra_peers += 1;
211                    Ok(Arc::clone(&self.outbound_semaphore)
212                        .try_acquire_owned()
213                        .unwrap())
214                }
215            })?;
216
217        // try to get a random peer on any network zone from the address book.
218        let peer = self
219            .address_book_svc
220            .ready()
221            .await
222            .expect("Error in address book!")
223            .call(AddressBookRequest::TakeRandomPeer {
224                height: req.block_needed,
225            })
226            .await;
227
228        match peer {
229            Err(_) => {
230                // TODO: We should probably send peer requests to our connected peers rather than go to seeds.
231                tracing::warn!("No peers in address book which are available and have the data we need. Getting peers from seed nodes.");
232
233                self.connect_to_random_seeds().await?;
234                Err(OutboundConnectorError::NoAvailablePeers)
235            }
236
237            Ok(AddressBookResponse::Peer(peer)) => {
238                self.connect_to_outbound_peer(permit, peer.adr).await;
239                Ok(())
240            }
241            Ok(_) => panic!("peer list sent incorrect response!"),
242        }
243    }
244
245    /// Handles a free permit, by either connecting to a new peer or by removing a permit if we are above the
246    /// minimum number of outbound connections.
247    #[instrument(level = "debug", skip(self, permit))]
248    async fn handle_free_permit(
249        &mut self,
250        permit: OwnedSemaphorePermit,
251    ) -> Result<(), OutboundConnectorError> {
252        if self.extra_peers > 0 {
253            tracing::debug!(
254                "Permit available but we are over the minimum number of peers, forgetting permit."
255            );
256            permit.forget();
257            self.extra_peers -= 1;
258            return Ok(());
259        }
260
261        tracing::debug!("Permit available, making outbound connection.");
262
263        let req = if self.peer_type_gen.sample(&mut thread_rng()) {
264            AddressBookRequest::TakeRandomGrayPeer { height: None }
265        } else {
266            // This will try white peers first then gray.
267            AddressBookRequest::TakeRandomPeer { height: None }
268        };
269
270        let Ok(AddressBookResponse::Peer(peer)) = self
271            .address_book_svc
272            .ready()
273            .await
274            .expect("Error in address book!")
275            .call(req)
276            .await
277        else {
278            tracing::warn!("No peers in peer list to make connection to.");
279            self.connect_to_random_seeds().await?;
280            return Err(OutboundConnectorError::NoAvailablePeers);
281        };
282
283        self.connect_to_outbound_peer(permit, peer.adr).await;
284        Ok(())
285    }
286
287    /// Runs the outbound connection count keeper.
288    pub async fn run(mut self) {
289        tracing::info!(
290            "Starting outbound connection maintainer, target outbound connections: {}",
291            self.config.outbound_connections
292        );
293
294        loop {
295            tokio::select! {
296                biased;
297                peer_req = self.make_connection_rx.recv() => {
298                    let Some(peer_req) = peer_req else {
299                        tracing::info!("Shutting down outbound connector, make connection channel closed.");
300                        return;
301                    };
302                    #[expect(clippy::let_underscore_must_use, reason = "We can't really do much about errors in this function.")]
303                    let _ = self.handle_peer_request(&peer_req).await;
304                },
305                // This future is not cancellation safe as you will lose your space in the queue but as we are the only place
306                // that actually requires permits that should be ok.
307                Ok(permit) = Arc::clone(&self.outbound_semaphore).acquire_owned() => {
308                    if self.handle_free_permit(permit).await.is_err() {
309                        // if we got an error then we still have a permit free so to prevent this from just looping
310                        // uncontrollably add a timeout.
311                        sleep(OUTBOUND_CONNECTION_ATTEMPT_TIMEOUT).await;
312                    }
313                }
314            }
315        }
316    }
317}