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