1#[cfg(feature = "native")]
7use crate::data::error::Error;
8use crate::data::error::Result;
9use ant_protocol::transport::{DHTNode, MultiAddr, PeerId, WitnessedCloseGroup};
10#[cfg(feature = "native")]
11use ant_protocol::{
12 transport::{CoreNodeConfig, IPDiversityConfig, NodeMode, P2PNode},
13 MAX_WIRE_MESSAGE_SIZE,
14};
15use serde::{Deserialize, Serialize};
16#[cfg(feature = "native")]
17use std::net::SocketAddr;
18#[cfg(feature = "native")]
19use std::sync::Arc;
20
21pub const REBOOTSTRAP_THRESHOLD: usize = 3;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32pub struct NetworkHealth {
33 pub write_ready: bool,
36 pub connected_peers: u32,
38 pub routing_table_size: u32,
40 pub rebootstrap_threshold: u32,
42}
43
44impl NetworkHealth {
45 #[must_use]
57 pub fn from_counts(connected_peers: usize, routing_table_size: usize) -> Self {
58 Self {
59 write_ready: routing_table_size.max(connected_peers) >= REBOOTSTRAP_THRESHOLD,
60 connected_peers: connected_peers.try_into().unwrap_or(u32::MAX),
61 routing_table_size: routing_table_size.try_into().unwrap_or(u32::MAX),
62 rebootstrap_threshold: REBOOTSTRAP_THRESHOLD as u32,
63 }
64 }
65}
66
67#[cfg(feature = "native")]
70pub(crate) struct ClosestPeerDiagnostics {
71 pub peer_id: PeerId,
72 pub addresses: Vec<MultiAddr>,
73 pub address_types: Vec<String>,
74 pub local_last_seen_age_ms: Option<u64>,
76 pub publisher_address_set_age_ms: Option<u64>,
78 pub publisher_address_set_unix_ns: Option<u64>,
79}
80
81#[derive(Clone)]
86pub struct Network {
87 #[cfg(feature = "native")]
88 node: Arc<P2PNode>,
89 #[cfg(not(feature = "native"))]
90 backend: std::rc::Rc<dyn BrowserNetwork>,
91}
92
93pub type PeerAddresses = Vec<(PeerId, Vec<MultiAddr>)>;
95
96#[derive(Clone)]
100pub struct ReadProgress {
101 target: [u8; 32],
102 local: PeerId,
103 sender: tokio::sync::watch::Sender<PeerAddresses>,
104}
105
106impl ReadProgress {
107 pub(crate) fn new(
108 target: [u8; 32],
109 local: PeerId,
110 sender: tokio::sync::watch::Sender<PeerAddresses>,
111 ) -> Self {
112 Self {
113 target,
114 local,
115 sender,
116 }
117 }
118
119 pub fn offer(&self, peers: PeerAddresses) {
122 if self.sender.is_closed() {
123 return;
124 }
125 self.sender.send_modify(|current| {
126 for peer in peers {
127 if peer.0 == self.local || peer.1.is_empty() {
128 continue;
129 }
130 if let Some(existing) = current.iter_mut().find(|entry| entry.0 == peer.0) {
131 *existing = peer;
132 } else {
133 current.push(peer);
134 }
135 }
136 current.sort_by_key(|peer| {
137 ant_protocol::transport::xor_distance(peer.0.as_bytes(), &self.target)
138 });
139 current.truncate(crate::client_engine::read::MAX_GET_FALLBACK_PEERS);
140 });
141 }
142}
143
144#[cfg(not(feature = "native"))]
147pub trait BrowserNetwork {
148 fn peer_id(&self) -> &PeerId;
150 fn find_closest_peers<'a>(
152 &'a self,
153 target: &'a [u8; 32],
154 count: usize,
155 ) -> futures::future::LocalBoxFuture<'a, Result<PeerAddresses>>;
156 fn find_read_peers<'a>(
159 &'a self,
160 target: &'a [u8; 32],
161 count: usize,
162 _progress: ReadProgress,
163 ) -> futures::future::LocalBoxFuture<'a, Result<PeerAddresses>> {
164 self.find_closest_peers(target, count)
165 }
166 fn find_witnessed_close_group<'a>(
168 &'a self,
169 target: &'a [u8; 32],
170 count: usize,
171 view_count: usize,
172 ) -> futures::future::LocalBoxFuture<'a, Result<WitnessedCloseGroup>>;
173 fn known_peers(&self) -> Vec<DHTNode>;
175 fn connected_read_peers(&self) -> Vec<PeerId> {
178 Vec::new()
179 }
180 fn request<'a>(
182 &'a self,
183 peer: &'a PeerId,
184 addrs: &'a [MultiAddr],
185 request: ant_protocol::ChunkMessage,
186 timeout: std::time::Duration,
187 ) -> futures::future::LocalBoxFuture<'a, Result<ant_protocol::ChunkMessage>>;
188}
189
190impl Network {
191 #[cfg(feature = "native")]
212 pub async fn new(
213 bootstrap_peers: &[SocketAddr],
214 allow_loopback: bool,
215 ipv6: bool,
216 ) -> Result<Self> {
217 let seeds: Vec<_> = bootstrap_peers
218 .iter()
219 .copied()
220 .map(MultiAddr::quic)
221 .collect();
222 Self::new_multiaddrs(&seeds, allow_loopback, ipv6).await
223 }
224
225 #[cfg(feature = "native")]
227 pub async fn new_multiaddrs(
228 bootstrap_peers: &[MultiAddr],
229 allow_loopback: bool,
230 ipv6: bool,
231 ) -> Result<Self> {
232 let seeds = bootstrap_peers
233 .iter()
234 .map(|addr| crate::network_defaults::parse_quic_seed(&addr.to_string()))
235 .collect::<std::result::Result<Vec<_>, _>>()
236 .map_err(|e| Error::Network(e.to_string()))?;
237 let mut core_config = CoreNodeConfig::builder()
238 .port(0)
239 .ipv6(ipv6)
240 .local(allow_loopback)
241 .mode(NodeMode::Client)
242 .max_message_size(MAX_WIRE_MESSAGE_SIZE)
243 .build()
244 .map_err(|e| Error::Network(format!("Failed to create core config: {e}")))?;
245
246 core_config.diversity_config = Some(IPDiversityConfig::permissive());
251
252 core_config.bootstrap_peers = seeds;
253
254 let node = P2PNode::new(core_config)
255 .await
256 .map_err(|e| Error::Network(format!("Failed to create P2P node: {e}")))?;
257
258 node.start()
259 .await
260 .map_err(|e| Error::Network(format!("Failed to start P2P node: {e}")))?;
261
262 Ok(Self {
263 node: Arc::new(node),
264 })
265 }
266
267 #[must_use]
269 #[cfg(feature = "native")]
270 pub fn from_node(node: Arc<P2PNode>) -> Self {
271 Self { node }
272 }
273
274 #[must_use]
276 #[cfg(feature = "native")]
277 pub fn node(&self) -> &Arc<P2PNode> {
278 &self.node
279 }
280
281 #[must_use]
283 #[cfg(feature = "native")]
284 pub fn peer_id(&self) -> &PeerId {
285 self.node.peer_id()
286 }
287
288 #[cfg(feature = "native")]
298 pub async fn find_closest_peers(
299 &self,
300 target: &[u8; 32],
301 count: usize,
302 ) -> Result<Vec<(PeerId, Vec<MultiAddr>)>> {
303 let local_peer_id = self.node.peer_id();
304
305 let closest_nodes = self
307 .node
308 .dht()
309 .find_closest_nodes(target, count + 1)
310 .await
311 .map_err(|e| Error::Network(format!("DHT closest-nodes lookup failed: {e}")))?;
312
313 Ok(closest_nodes
314 .into_iter()
315 .filter(|n| n.peer_id != *local_peer_id)
316 .take(count)
317 .map(|n| {
318 let addrs = n.addresses_by_priority();
319 (n.peer_id, addrs)
320 })
321 .collect())
322 }
323
324 #[cfg(feature = "native")]
327 pub(crate) async fn find_closest_peers_with_diagnostics(
328 &self,
329 target: &[u8; 32],
330 count: usize,
331 ) -> Result<Vec<ClosestPeerDiagnostics>> {
332 let local_peer_id = self.node.peer_id();
333 let closest_nodes = self
334 .node
335 .dht()
336 .find_closest_nodes(target, count + 1)
337 .await
338 .map_err(|e| Error::Network(format!("DHT closest-nodes lookup failed: {e}")))?;
339 let now_ns = std::time::SystemTime::now()
340 .duration_since(std::time::UNIX_EPOCH)
341 .unwrap_or_default()
342 .as_nanos();
343 let now_ns = u64::try_from(now_ns).unwrap_or(u64::MAX);
344
345 let mut result = Vec::with_capacity(count);
346 for node in closest_nodes
347 .into_iter()
348 .filter(|node| node.peer_id != *local_peer_id)
349 .take(count)
350 {
351 let publisher_address_set_unix_ns = node.publisher_address_set_unix_ns();
352 let publisher_address_set_age_ms = publisher_address_set_unix_ns
355 .and_then(|published| now_ns.checked_sub(published))
356 .map(|age_ns| age_ns / 1_000_000);
357 let local_last_seen_age_ms = self
358 .node
359 .peer_last_seen_elapsed(&node.peer_id)
360 .await
361 .map(|age| u64::try_from(age.as_millis()).unwrap_or(u64::MAX));
362 let address_context = node.address_and_type_labels_by_priority();
363 let (addresses, address_types) = address_context
364 .into_iter()
365 .map(|(address, label)| (address, label.to_string()))
366 .unzip();
367 result.push(ClosestPeerDiagnostics {
368 peer_id: node.peer_id,
369 addresses,
370 address_types,
371 local_last_seen_age_ms,
372 publisher_address_set_age_ms,
373 publisher_address_set_unix_ns,
374 });
375 }
376 Ok(result)
377 }
378
379 pub async fn find_witnessed_close_group(
390 &self,
391 target: &[u8; 32],
392 count: usize,
393 ) -> Result<WitnessedCloseGroup> {
394 self.find_witnessed_close_group_with_view_count(target, count, count)
395 .await
396 }
397
398 #[cfg(feature = "native")]
408 pub async fn find_witnessed_close_group_with_view_count(
409 &self,
410 target: &[u8; 32],
411 count: usize,
412 view_count: usize,
413 ) -> Result<WitnessedCloseGroup> {
414 self.node
415 .dht()
416 .find_witnessed_close_group_with_view_count(target, count, view_count)
417 .await
418 .map_err(|e| Error::Network(format!("DHT witnessed close-group lookup failed: {e}")))
419 }
420
421 #[cfg(feature = "native")]
423 pub async fn connected_peers(&self) -> Vec<PeerId> {
424 self.node.connected_peers().await
425 }
426
427 #[cfg(feature = "native")]
437 pub async fn health(&self) -> NetworkHealth {
438 let connected_peers = self.node.peer_count().await;
439 let routing_table_size = self.node.dht_manager().get_routing_table_size().await;
440 NetworkHealth::from_counts(connected_peers, routing_table_size)
441 }
442}
443
444impl Network {
445 #[cfg(not(feature = "native"))]
447 pub fn from_browser(backend: std::rc::Rc<dyn BrowserNetwork>) -> Self {
448 Self { backend }
449 }
450
451 #[cfg(not(feature = "native"))]
453 pub fn peer_id(&self) -> &PeerId {
454 self.backend.peer_id()
455 }
456
457 #[cfg(not(feature = "native"))]
459 pub async fn find_closest_peers(
460 &self,
461 target: &[u8; 32],
462 count: usize,
463 ) -> Result<Vec<(PeerId, Vec<MultiAddr>)>> {
464 self.backend.find_closest_peers(target, count).await
465 }
466
467 #[cfg(not(feature = "native"))]
469 pub async fn find_witnessed_close_group_with_view_count(
470 &self,
471 target: &[u8; 32],
472 count: usize,
473 view_count: usize,
474 ) -> Result<WitnessedCloseGroup> {
475 self.backend
476 .find_witnessed_close_group(target, count, view_count)
477 .await
478 }
479
480 #[cfg(not(feature = "native"))]
482 pub async fn connected_peers(&self) -> Vec<PeerId> {
483 self.backend
484 .known_peers()
485 .into_iter()
486 .map(|node| node.peer_id)
487 .collect()
488 }
489
490 pub async fn known_peers(&self) -> Vec<DHTNode> {
492 #[cfg(feature = "native")]
493 {
494 self.node.dht().routing_table_peers().await
495 }
496 #[cfg(not(feature = "native"))]
497 {
498 self.backend.known_peers()
499 }
500 }
501
502 pub(crate) async fn seed_read_candidates(&self, progress: &ReadProgress) {
504 progress.offer(
505 self.known_peers()
506 .await
507 .into_iter()
508 .map(|node| {
509 let addrs = node.addresses_by_priority();
510 (node.peer_id, addrs)
511 })
512 .collect(),
513 );
514 }
515
516 #[cfg(not(feature = "native"))]
517 pub(crate) async fn find_read_peers(
518 &self,
519 target: &[u8; 32],
520 count: usize,
521 progress: ReadProgress,
522 ) -> Result<PeerAddresses> {
523 self.backend.find_read_peers(target, count, progress).await
524 }
525}
526
527#[allow(clippy::too_many_arguments)]
529pub(crate) async fn send_and_await_chunk_response<T, E: From<crate::data::error::Error>>(
530 network: &Network,
531 target_peer: &PeerId,
532 message_bytes: Vec<u8>,
533 request_id: u64,
534 timeout: std::time::Duration,
535 peer_addrs: &[MultiAddr],
536 response_handler: impl Fn(ant_protocol::ChunkMessageBody) -> Option<std::result::Result<T, E>>,
537 send_error: impl FnOnce(String) -> E,
538 timeout_error: impl FnOnce() -> E,
539) -> std::result::Result<T, E> {
540 #[cfg(feature = "native")]
541 {
542 ant_protocol::send_and_await_chunk_response(
543 network.node(),
544 target_peer,
545 message_bytes,
546 request_id,
547 timeout,
548 peer_addrs,
549 response_handler,
550 send_error,
551 timeout_error,
552 )
553 .await
554 }
555 #[cfg(not(feature = "native"))]
556 {
557 let response = match ant_protocol::ChunkMessage::decode(&message_bytes) {
558 Ok(request) => {
559 network
560 .backend
561 .request(target_peer, peer_addrs, request, timeout)
562 .await
563 }
564 Err(error) => Err(crate::data::error::Error::Protocol(error.to_string())),
565 };
566 let response = match response {
567 Ok(response) => response,
568 Err(error @ crate::data::error::Error::Timeout(_)) => return Err(error.into()),
571 Err(error) => return Err(send_error(error.to_string())),
572 };
573 if response.request_id != request_id {
574 return Err(timeout_error());
575 }
576 response_handler(response.body).unwrap_or_else(|| Err(timeout_error()))
577 }
578}
579
580#[cfg(test)]
581mod tests {
582 use super::*;
583
584 #[test]
585 fn write_ready_false_with_no_peers() {
586 let h = NetworkHealth::from_counts(0, 0);
587 assert!(!h.write_ready);
588 assert_eq!(h.connected_peers, 0);
589 assert_eq!(h.routing_table_size, 0);
590 assert_eq!(h.rebootstrap_threshold, REBOOTSTRAP_THRESHOLD as u32);
591 }
592
593 #[test]
594 fn write_ready_false_below_threshold_on_both_signals() {
595 assert!(!NetworkHealth::from_counts(1, 0).write_ready);
598 assert!(!NetworkHealth::from_counts(2, 2).write_ready);
599 }
600
601 #[test]
602 fn write_ready_true_via_connections_despite_low_routing_table() {
603 assert!(NetworkHealth::from_counts(10, 2).write_ready);
607 }
608
609 #[test]
610 fn write_ready_true_via_routing_table_alone() {
611 assert!(NetworkHealth::from_counts(0, REBOOTSTRAP_THRESHOLD).write_ready);
612 }
613
614 #[test]
615 fn write_ready_true_at_exact_threshold_on_connections() {
616 assert!(NetworkHealth::from_counts(REBOOTSTRAP_THRESHOLD, 0).write_ready);
617 }
618
619 #[test]
620 fn counts_saturate_at_u32_max() {
621 let h = NetworkHealth::from_counts(usize::MAX, usize::MAX);
622 assert_eq!(h.connected_peers, u32::MAX);
623 assert_eq!(h.routing_table_size, u32::MAX);
624 assert!(h.write_ready);
625 }
626}