Skip to main content

cuprate_p2p/
lib.rs

1//! Cuprate's P2P Crate.
2//!
3//! This crate contains a [`NetworkInterface`] which allows interacting with the Monero P2P network on
4//! a certain [`NetworkZone`]
5use std::sync::Arc;
6
7use futures::FutureExt;
8use tokio::{
9    sync::mpsc,
10    task::JoinSet,
11    time::{sleep, Duration},
12};
13use tower::{buffer::Buffer, util::BoxCloneService, Service, ServiceExt};
14use tracing::{instrument, Instrument, Span};
15
16use cuprate_p2p_core::{
17    client::Connector,
18    client::PeerSyncCallback,
19    services::{AddressBookRequest, AddressBookResponse},
20    CoreSyncSvc, NetworkZone, ProtocolRequestHandlerMaker, Transport,
21};
22
23pub mod block_downloader;
24mod broadcast;
25pub mod config;
26pub mod connection_maintainer;
27pub mod constants;
28mod inbound_server;
29mod peer_set;
30
31use block_downloader::{
32    BlockDownloaderConfig, BlockDownloaderHandle, ChainSvcRequest, ChainSvcResponse,
33};
34pub use broadcast::{BroadcastRequest, BroadcastSvc};
35pub use config::{AddressBookConfig, P2PConfig, TransportConfig};
36use connection_maintainer::MakeConnectionRequest;
37use peer_set::PeerSet;
38pub use peer_set::{ClientDropGuard, PeerSetRequest, PeerSetResponse};
39
40/// Interval for checking inbound connection status (1 hour)
41const INBOUND_CONNECTION_MONITOR_INTERVAL: Duration = Duration::from_secs(3600);
42
43/// Monitors for inbound connections and logs a warning if none are detected.
44///
45/// This task runs every hour to check if there are inbound connections available.
46/// If `max_inbound_connections` is 0, the task will exit without logging.
47#[expect(clippy::infinite_loop)]
48async fn inbound_connection_monitor(
49    inbound_semaphore: Arc<tokio::sync::Semaphore>,
50    max_inbound_connections: usize,
51    p2p_port: u16,
52) {
53    // Skip monitoring if inbound connections are disabled
54    if max_inbound_connections == 0 {
55        return;
56    }
57
58    loop {
59        // Wait for the monitoring interval
60        sleep(INBOUND_CONNECTION_MONITOR_INTERVAL).await;
61
62        // Check if we have any inbound connections
63        // If available permits equals max_inbound_connections, no peers are connected
64        let available_permits = inbound_semaphore.available_permits();
65        if available_permits == max_inbound_connections {
66            tracing::warn!(
67                "No incoming connections - check firewalls/routers allow port {}",
68                p2p_port
69            );
70        }
71    }
72}
73
74/// Initializes the P2P [`NetworkInterface`] for a specific [`NetworkZone`].
75///
76/// This function starts all the tasks to maintain/accept/make connections.
77///
78/// If [`P2PConfig::offline`] no connections will be made or accepted.
79///
80/// # Usage
81/// You must provide:
82/// - A protocol request handler, which is given to each connection
83/// - A core sync service, which keeps track of the sync state of our node
84#[instrument(level = "error", name = "net", skip_all, fields(zone = Z::NAME))]
85pub async fn initialize_network<Z, T, PR, CS>(
86    protocol_request_handler_maker: PR,
87    core_sync_svc: CS,
88    config: P2PConfig<Z>,
89    transport_config: TransportConfig<Z, T>,
90    peer_sync_callback: Option<PeerSyncCallback>,
91) -> Result<NetworkInterface<Z>, tower::BoxError>
92where
93    Z: NetworkZone,
94    T: Transport<Z>,
95    Z::Addr: borsh::BorshDeserialize + borsh::BorshSerialize,
96    PR: ProtocolRequestHandlerMaker<Z> + Clone,
97    CS: CoreSyncSvc + Clone,
98{
99    let max_connections = config
100        .max_inbound_connections
101        .checked_add(config.outbound_connections)
102        .unwrap()
103        .max(1);
104
105    let address_book = Buffer::new(
106        cuprate_address_book::init_address_book(config.address_book_config.clone()).await?,
107        max_connections,
108    );
109
110    // Use the default config. Changing the defaults affects tx fluff times, which could affect D++ so for now don't allow changing
111    // this.
112    let (broadcast_svc, outbound_mkr, inbound_mkr) =
113        broadcast::init_broadcast_channels(broadcast::BroadcastConfig::default());
114
115    let (new_connection_tx, new_connection_rx) = mpsc::channel(max_connections);
116    let (make_connection_tx, make_connection_rx) = mpsc::channel(3);
117
118    let peer_set = PeerSet::new(new_connection_rx);
119
120    if config.offline {
121        tracing::warn!("Offline mode enabled, not connecting to or listening for peers.");
122
123        return Ok(NetworkInterface {
124            peer_set: Buffer::new(peer_set, 10).boxed_clone(),
125            broadcast_svc,
126            make_connection_tx,
127            address_book: address_book.boxed_clone(),
128            _background_tasks: Arc::new(JoinSet::new()),
129        });
130    }
131
132    let mut basic_node_data = config.basic_node_data();
133
134    if !Z::CHECK_NODE_ID {
135        basic_node_data.peer_id = 1;
136    }
137
138    let mut outbound_handshaker_builder =
139        cuprate_p2p_core::client::HandshakerBuilder::<Z, T, _, _, _, _>::new(
140            basic_node_data,
141            transport_config.client_config,
142        )
143        .with_address_book(address_book.clone())
144        .with_core_sync_svc(core_sync_svc)
145        .with_protocol_request_handler_maker(protocol_request_handler_maker)
146        .with_broadcast_stream_maker(outbound_mkr)
147        .with_connection_parent_span(Span::current());
148
149    if let Some(ref cb) = peer_sync_callback {
150        outbound_handshaker_builder =
151            outbound_handshaker_builder.with_peer_sync_callback(cb.clone());
152    }
153
154    let inbound_handshaker = outbound_handshaker_builder
155        .clone()
156        .with_broadcast_stream_maker(inbound_mkr)
157        .build();
158
159    let outbound_handshaker = outbound_handshaker_builder.build();
160
161    let outbound_connector = Connector::new(outbound_handshaker);
162    let outbound_connection_maintainer = connection_maintainer::OutboundConnectionKeeper::new(
163        config.clone(),
164        new_connection_tx.clone(),
165        make_connection_rx,
166        address_book.clone(),
167        outbound_connector,
168        peer_sync_callback.clone(),
169    );
170
171    let inbound_listener = if let Some(config) = transport_config.server_config {
172        Some(T::incoming_connection_listener(config).await?)
173    } else {
174        tracing::warn!("No inbound server config provided, not listening for inbound connections.");
175        None
176    };
177
178    // Create semaphore for limiting inbound connections and monitoring
179    let inbound_semaphore = Arc::new(tokio::sync::Semaphore::new(config.max_inbound_connections));
180
181    let mut background_tasks = JoinSet::new();
182
183    background_tasks.spawn(
184        outbound_connection_maintainer
185            .run()
186            .instrument(Span::current()),
187    );
188
189    // Spawn inbound connection monitor task
190    if inbound_listener.is_some() {
191        background_tasks.spawn(
192            inbound_connection_monitor(
193                Arc::clone(&inbound_semaphore),
194                config.max_inbound_connections,
195                config.p2p_port,
196            )
197            .instrument(tracing::info_span!("inbound_connection_monitor")),
198        );
199    }
200
201    background_tasks.spawn(
202        inbound_server::inbound_server(
203            new_connection_tx,
204            inbound_handshaker,
205            address_book.clone(),
206            config,
207            inbound_listener,
208            inbound_semaphore,
209            peer_sync_callback,
210        )
211        .map(|res| {
212            if let Err(e) = res {
213                tracing::error!("Error in inbound connection listener: {e}");
214            }
215
216            tracing::info!("Inbound connection listener shutdown");
217        })
218        .instrument(Span::current()),
219    );
220
221    Ok(NetworkInterface {
222        peer_set: Buffer::new(peer_set, 10).boxed_clone(),
223        broadcast_svc,
224        make_connection_tx,
225        address_book: address_book.boxed_clone(),
226        _background_tasks: Arc::new(background_tasks),
227    })
228}
229
230/// The interface to Monero's P2P network on a certain [`NetworkZone`].
231#[derive(Clone)]
232pub struct NetworkInterface<N: NetworkZone> {
233    /// A pool of free connected peers.
234    peer_set: BoxCloneService<PeerSetRequest, PeerSetResponse<N>, tower::BoxError>,
235    /// A [`Service`] that allows broadcasting to all connected peers.
236    broadcast_svc: BroadcastSvc<N>,
237    /// A channel to request extra connections.
238    #[expect(dead_code, reason = "will be used eventually")]
239    make_connection_tx: mpsc::Sender<MakeConnectionRequest>,
240    /// The address book service.
241    address_book: BoxCloneService<AddressBookRequest<N>, AddressBookResponse<N>, tower::BoxError>,
242    /// Background tasks that will be aborted when this interface is dropped.
243    _background_tasks: Arc<JoinSet<()>>,
244}
245
246impl<N: NetworkZone> NetworkInterface<N> {
247    /// Returns a service which allows broadcasting messages to all the connected peers in a specific [`NetworkZone`].
248    pub fn broadcast_svc(&self) -> BroadcastSvc<N> {
249        self.broadcast_svc.clone()
250    }
251
252    /// Starts the block downloader.
253    ///
254    /// The downloader runs until it is aborted with [`BlockDownloaderHandle::task`].
255    pub fn block_downloader<C>(
256        &self,
257        our_chain_service: C,
258        config: BlockDownloaderConfig,
259    ) -> BlockDownloaderHandle
260    where
261        C: Service<ChainSvcRequest<N>, Response = ChainSvcResponse<N>, Error = tower::BoxError>
262            + Send
263            + 'static,
264        C::Future: Send + 'static,
265    {
266        block_downloader::download_blocks(self.peer_set.clone(), our_chain_service, config)
267    }
268
269    /// Returns the address book service.
270    pub fn address_book(
271        &self,
272    ) -> BoxCloneService<AddressBookRequest<N>, AddressBookResponse<N>, tower::BoxError> {
273        self.address_book.clone()
274    }
275
276    /// Borrows the `PeerSet`, for access to connected peers.
277    pub fn peer_set(
278        &mut self,
279    ) -> &mut BoxCloneService<PeerSetRequest, PeerSetResponse<N>, tower::BoxError> {
280        &mut self.peer_set
281    }
282}