1#[cfg(feature = "native")]
7use crate::data::client::diagnostics::{
8 bounded_error, unix_now_ms, DownloadDiagnosticsOutcome, DownloadDiagnosticsRecord,
9 DownloadDiagnosticsSender, DownloadRequestCorrelation,
10};
11#[cfg(feature = "native")]
12use crate::data::network::ClosestPeerDiagnostics;
13#[cfg(feature = "native")]
14use ant_protocol::{
15 send_and_await_chunk_response_with_metadata, transport::PeerRouteKind, ChunkProtocolResponse,
16};
17#[cfg(feature = "native")]
18use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
19#[cfg(feature = "native")]
20static ACTIVE_DIAGNOSTIC_REQUESTS: AtomicUsize = AtomicUsize::new(0);
21
22#[cfg(feature = "native")]
23static NEXT_DIAGNOSTIC_LOOKUP_ID: AtomicUsize = AtomicUsize::new(1);
24
25#[cfg(feature = "native")]
26struct ActiveDiagnosticRequestGuard;
27
28#[cfg(feature = "native")]
29impl ActiveDiagnosticRequestGuard {
30 fn enter() -> (Self, usize) {
31 let active = ACTIVE_DIAGNOSTIC_REQUESTS.fetch_add(1, AtomicOrdering::Relaxed) + 1;
32 (Self, active)
33 }
34}
35
36#[cfg(feature = "native")]
37impl Drop for ActiveDiagnosticRequestGuard {
38 fn drop(&mut self) {
39 ACTIVE_DIAGNOSTIC_REQUESTS.fetch_sub(1, AtomicOrdering::Relaxed);
40 }
41}
42
43#[cfg(feature = "native")]
44fn encode_diagnostic_chunk_get_request(
45 address: &XorName,
46 correlation: &DownloadRequestCorrelation,
47) -> Result<Vec<u8>> {
48 ChunkMessage {
49 request_id: correlation.request_id,
50 body: ChunkMessageBody::GetRequest(ChunkGetRequest::new(*address)),
51 }
52 .encode()
53 .map_err(|e| Error::Protocol(format!("Failed to encode GET request: {e}")))
54}
55
56#[cfg(feature = "native")]
57pub(crate) struct ChunkFetchDiagnostics<'a> {
58 sender: &'a DownloadDiagnosticsSender,
59 file_attempt: usize,
60 chunk_index: usize,
61 chunk_address: [u8; 32],
62 fetch_cap: usize,
63}
64
65#[cfg(feature = "native")]
66impl<'a> ChunkFetchDiagnostics<'a> {
67 pub(crate) fn new(
68 sender: &'a DownloadDiagnosticsSender,
69 file_attempt: usize,
70 chunk_index: usize,
71 chunk_address: [u8; 32],
72 fetch_cap: usize,
73 ) -> Self {
74 Self {
75 sender,
76 file_attempt,
77 chunk_index,
78 chunk_address,
79 fetch_cap,
80 }
81 }
82
83 #[allow(clippy::too_many_arguments)]
86 fn emit_peer_attempt(
87 &self,
88 sweep: &'static str,
89 peer_attempt: usize,
90 lookup_duration_ms: Option<u64>,
91 lookup_correlation_id: &str,
92 peer_context: &ClosestPeerDiagnostics,
93 expected_peer: &PeerId,
94 source_peer: Option<&PeerId>,
95 transport_source: Option<&MultiAddr>,
96 route: PeerRouteKind,
97 peer_connected_before_request: bool,
98 active_requests_at_start: usize,
99 request_started_unix_ms: u64,
100 request_completed_unix_ms: u64,
101 correlation: &DownloadRequestCorrelation,
102 response_elapsed_ms: u64,
103 bytes: u64,
104 outcome: DownloadDiagnosticsOutcome,
105 error: Option<String>,
106 ) {
107 self.sender
108 .try_emit(DownloadDiagnosticsRecord::peer_attempt(
109 self.file_attempt,
110 self.chunk_index,
111 &self.chunk_address,
112 sweep,
113 peer_attempt,
114 lookup_duration_ms,
115 lookup_correlation_id,
116 &expected_peer.to_string(),
117 peer_context
118 .addresses
119 .iter()
120 .map(ToString::to_string)
121 .collect(),
122 peer_context.address_types.clone(),
123 peer_context.local_last_seen_age_ms,
124 peer_context.publisher_address_set_age_ms,
125 peer_context.publisher_address_set_unix_ns,
126 source_peer.map(ToString::to_string).as_deref(),
127 transport_source.map(ToString::to_string).as_deref(),
128 route.as_str(),
129 (route == PeerRouteKind::Unknown)
130 .then_some(DownloadDiagnosticsRecord::ROUTE_UNKNOWN_NOTE),
131 Some(peer_connected_before_request),
132 Some(active_requests_at_start),
133 Some(self.fetch_cap),
134 request_started_unix_ms,
135 request_completed_unix_ms,
136 correlation,
137 response_elapsed_ms,
138 bytes,
139 outcome,
140 error,
141 ));
142 }
143
144 fn emit_chunk_level(
146 &self,
147 sweep: &'static str,
148 bytes: u64,
149 outcome: DownloadDiagnosticsOutcome,
150 error: Option<String>,
151 ) {
152 self.sender.try_emit(DownloadDiagnosticsRecord::chunk_level(
153 self.file_attempt,
154 self.chunk_index,
155 &self.chunk_address,
156 sweep,
157 Some(self.fetch_cap),
158 bytes,
159 outcome,
160 error,
161 ));
162 }
163}
164
165#[cfg(feature = "native")]
166fn classify_peer_attempt(
167 result: &Result<Option<DataChunk>>,
168) -> (DownloadDiagnosticsOutcome, u64, bool, Option<String>) {
169 match result {
170 Ok(Some(chunk)) => (
171 DownloadDiagnosticsOutcome::Found,
172 chunk.content.len() as u64,
173 true,
174 None,
175 ),
176 Ok(None) => (DownloadDiagnosticsOutcome::NotFound, 0, true, None),
177 Err(Error::Timeout(msg)) => (
178 DownloadDiagnosticsOutcome::Timeout,
179 0,
180 false,
181 Some(bounded_error("timeout", msg)),
182 ),
183 Err(Error::Network(msg)) => (
184 DownloadDiagnosticsOutcome::NetworkError,
185 0,
186 false,
187 Some(bounded_error("network", msg)),
188 ),
189 Err(Error::InvalidData(msg)) => (
192 DownloadDiagnosticsOutcome::ProtocolError,
193 0,
194 true,
195 Some(bounded_error("protocol", msg)),
196 ),
197 Err(Error::Protocol(msg)) => (
201 DownloadDiagnosticsOutcome::ProtocolError,
202 0,
203 false,
204 Some(bounded_error("protocol", msg)),
205 ),
206 Err(e) => (
207 DownloadDiagnosticsOutcome::ProtocolError,
208 0,
209 false,
210 Some(bounded_error("protocol", &e.to_string())),
211 ),
212 }
213}
214use crate::data::client::adaptive::Outcome;
215use crate::data::client::batch::{finalize_batch_payment, PreparedChunk};
216use crate::data::client::peer_xor_distance;
217use crate::data::client::Client;
218use crate::data::error::{Error, Result};
219use crate::data::network::send_and_await_chunk_response;
220use ant_protocol::evm::{QuoteHash, TxHash};
221use ant_protocol::transport::{MultiAddr, PeerId};
222use ant_protocol::{
223 compute_address, detect_proof_type, ChunkGetRequest, ChunkGetResponse, ChunkMessage,
224 ChunkMessageBody, ChunkPutRequest, ChunkPutResponse, DataChunk, ProofType, ProtocolError,
225 XorName, CLOSE_GROUP_MAJORITY,
226};
227use bytes::Bytes;
228use futures::stream::{self, StreamExt};
229use std::collections::HashMap;
230use tracing::{debug, info, warn};
231use web_time::{Duration, Instant};
232
233const CHUNK_DATA_TYPE: u32 = 0;
235
236use crate::transfer_policy::{PutRejection, PutShortfall};
237
238fn classify_put_failure(error: &Error) -> PutRejection {
244 match error {
245 Error::RemotePut { source, .. } => match source {
246 ProtocolError::StorageFailed(_) => PutRejection::Full,
247 ProtocolError::PaymentFailed(_) => PutRejection::PriceFloor,
248 _ => PutRejection::OtherRemote,
249 },
250 Error::Payment(_) => PutRejection::PriceFloor,
256 Error::Timeout(_) => PutRejection::Timeout,
258 _ => PutRejection::Dial,
261 }
262}
263
264fn put_shortfall_error(
283 timeout: usize,
284 dial: usize,
285 first_app_rejection: Option<Error>,
286 shortfall_message: String,
287) -> Error {
288 match crate::transfer_policy::put_shortfall(timeout, dial, first_app_rejection.is_some()) {
289 PutShortfall::ResponseTimeout => Error::InsufficientPeers(shortfall_message),
290 PutShortfall::RemoteRejection => {
291 first_app_rejection.unwrap_or(Error::CloseGroupShortfall(shortfall_message))
292 }
293 PutShortfall::PeerChurn => Error::CloseGroupShortfall(shortfall_message),
294 }
295}
296
297#[cfg(test)]
298use crate::client_engine::read::is_authoritative_not_found;
299
300const STORE_RESPONSE_TIMEOUT: Duration = Duration::from_secs(10);
302
303const DIAGNOSTIC_TIMEOUT_PADDING_WAVES: usize = 1;
305
306pub struct ChunkPeerGetResult {
308 pub peer_id: PeerId,
310 pub peer_addrs: Vec<MultiAddr>,
312 pub xor_distance: [u8; 32],
314 pub chunk_result: Result<Option<DataChunk>>,
316}
317
318#[derive(Clone)]
319struct ChunkPeerGetTarget {
320 index: usize,
321 peer_id: PeerId,
322 peer_addrs: Vec<MultiAddr>,
323 xor_distance: [u8; 32],
324}
325
326fn chunk_peer_get_targets(
327 peers: Vec<(PeerId, Vec<MultiAddr>)>,
328 address: &XorName,
329) -> Vec<ChunkPeerGetTarget> {
330 peers
331 .into_iter()
332 .enumerate()
333 .map(|(index, (peer_id, peer_addrs))| ChunkPeerGetTarget {
334 index,
335 peer_id,
336 peer_addrs,
337 xor_distance: peer_xor_distance(&peer_id, address),
338 })
339 .collect()
340}
341
342fn sort_chunk_peer_get_results(results: &mut [ChunkPeerGetResult]) {
343 results.sort_by_key(|result| result.xor_distance);
344}
345
346fn diagnostic_peer_get_concurrency(peer_count: usize, close_group_size: usize) -> usize {
347 peer_count.min(close_group_size.max(1))
348}
349
350fn diagnostic_peer_get_overall_timeout(
351 per_peer_timeout: Duration,
352 target_count: usize,
353 concurrency_limit: usize,
354) -> Duration {
355 let concurrency_limit = concurrency_limit.max(1);
356 let peer_get_waves = target_count.div_ceil(concurrency_limit);
357 let timeout_waves = peer_get_waves.saturating_add(DIAGNOSTIC_TIMEOUT_PADDING_WAVES);
358 let timeout_waves = u32::try_from(timeout_waves).unwrap_or(u32::MAX);
359
360 per_peer_timeout.saturating_mul(timeout_waves)
361}
362
363fn timed_out_chunk_peer_get_result(
364 target: &ChunkPeerGetTarget,
365 address: &XorName,
366 timeout: Duration,
367) -> ChunkPeerGetResult {
368 let addr_hex = hex::encode(address);
369 let timeout_secs = timeout.as_secs();
370 ChunkPeerGetResult {
371 peer_id: target.peer_id,
372 peer_addrs: target.peer_addrs.clone(),
373 xor_distance: target.xor_distance,
374 chunk_result: Err(Error::Timeout(format!(
375 "Diagnostic chunk GET sweep timed out before peer {} completed for chunk {addr_hex} after {timeout_secs}s",
376 target.peer_id
377 ))),
378 }
379}
380
381fn store_response_timeout_for_proof(proof: &[u8], merkle_timeout_secs: u64) -> Duration {
382 match detect_proof_type(proof) {
383 Some(ProofType::Merkle) => Duration::from_secs(merkle_timeout_secs),
384 _ => STORE_RESPONSE_TIMEOUT,
385 }
386}
387
388impl Client {
389 pub(crate) async fn chunk_get_observed(&self, address: &XorName) -> Result<Option<DataChunk>> {
400 self.chunk_get_observed_from_closest_peers(
401 address,
402 self.config().close_group_size,
403 #[cfg(feature = "native")]
404 None,
405 )
406 .await
407 }
408
409 pub(crate) async fn chunk_get_observed_from_closest_peers(
410 &self,
411 address: &XorName,
412 peer_count: usize,
413 #[cfg(feature = "native")] diag: Option<&ChunkFetchDiagnostics<'_>>,
414 ) -> Result<Option<DataChunk>> {
415 let epoch = self.controller().fetch.observation_epoch();
416 let started = Instant::now();
417 let result = self
418 .chunk_get_from_closest_peers_with_diagnostics(
419 address,
420 peer_count,
421 #[cfg(feature = "native")]
422 diag,
423 )
424 .await;
425 let latency = started.elapsed();
426 let bytes = result
427 .as_ref()
428 .ok()
429 .and_then(Option::as_ref)
430 .map_or(0, |chunk| chunk.content.len() as u64);
431 self.controller().fetch.observe_fetch_in_epoch(
432 chunk_get_outcome(&result),
433 latency,
434 bytes,
435 epoch,
436 );
437 result
438 }
439}
440
441pub(crate) fn chunk_get_outcome(result: &Result<Option<DataChunk>>) -> Outcome {
458 match result {
459 Ok(Some(_)) => Outcome::Success,
460 Ok(None) => Outcome::Timeout,
461 Err(Error::Timeout(_)) => Outcome::Timeout,
462 Err(Error::Network(_)) => Outcome::NetworkError,
463 Err(_) => Outcome::ApplicationError,
464 }
465}
466
467impl Client {
468 pub async fn chunk_put(&self, content: Bytes) -> Result<XorName> {
479 let address = compute_address(&content);
480 let data_size = u64::try_from(content.len())
481 .map_err(|e| Error::InvalidData(format!("content size too large: {e}")))?;
482
483 match self
484 .pay_for_storage(&address, data_size, CHUNK_DATA_TYPE)
485 .await
486 {
487 Ok((proof, peers)) => self.chunk_put_to_close_group(content, proof, &peers).await,
488 Err(Error::AlreadyStored) => {
489 debug!(
490 "Chunk {} already stored on network, skipping payment",
491 hex::encode(address)
492 );
493 Ok(address)
494 }
495 Err(e) => Err(e),
496 }
497 }
498
499 #[cfg(feature = "test-utils")]
513 pub async fn chunk_put_with_dead_initial_peers(
514 &self,
515 content: Bytes,
516 dead_count: usize,
517 ) -> Result<XorName> {
518 let address = compute_address(&content);
519 let data_size = u64::try_from(content.len())
520 .map_err(|e| Error::InvalidData(format!("content size too large: {e}")))?;
521 let (proof, real_peers) = self
522 .pay_for_storage(&address, data_size, CHUNK_DATA_TYPE)
523 .await?;
524 let mut peers: Vec<(PeerId, Vec<MultiAddr>)> = (0..dead_count)
528 .map(|_| (PeerId::random(), Vec::new()))
529 .collect();
530 peers.extend(real_peers);
531 self.chunk_put_to_close_group(content, proof, &peers).await
532 }
533
534 pub(crate) async fn chunk_put_to_close_group(
550 &self,
551 content: Bytes,
552 proof: Vec<u8>,
553 peers: &[(PeerId, Vec<MultiAddr>)],
554 ) -> Result<XorName> {
555 let address = compute_address(&content);
556
557 let outcome = crate::client_engine::quorum_with_fallback(
558 peers.iter().cloned(),
559 CLOSE_GROUP_MAJORITY,
560 |(peer_id, addrs)| {
561 let content = content.clone();
562 let proof = proof.clone();
563 async move { self.spawn_chunk_put(content, proof, peer_id, addrs).await.1 }
564 },
565 )
566 .await;
567 let success_count = outcome.successful_targets.len();
568 let mut failures: Vec<String> = Vec::new();
569 let mut full = 0usize;
577 let mut price_floor = 0usize;
578 let mut other_remote = 0usize;
579 let mut timeout = 0usize;
580 let mut dial = 0usize;
581 let mut first_app_rejection: Option<Error> = None;
582
583 for ((peer_id, _), error) in outcome.failures {
584 warn!("Failed to store chunk on {peer_id}: {error}");
585 failures.push(format!("{peer_id}: {error}"));
586 match classify_put_failure(&error) {
587 PutRejection::Full => full += 1,
588 PutRejection::PriceFloor => price_floor += 1,
589 PutRejection::OtherRemote => other_remote += 1,
590 PutRejection::Timeout => timeout += 1,
591 PutRejection::Dial => dial += 1,
592 }
593 if matches!(error, Error::RemotePut { .. } | Error::Payment(_))
598 && first_app_rejection.is_none()
599 {
600 first_app_rejection = Some(error);
601 }
602 }
603
604 if outcome.reached {
605 debug!(
606 "Chunk {} stored on {success_count} peers (majority reached)",
607 hex::encode(address)
608 );
609 return Ok(address);
610 }
611
612 let aggregate = format!(
617 "Stored on {success_count} peers, need {CLOSE_GROUP_MAJORITY} \
618 (full: {full}, price-floor: {price_floor}, other-rejection: {other_remote}, \
619 timeout: {timeout}, dial: {dial}). Failures: [{}]",
620 failures.join("; ")
621 );
622 Err(put_shortfall_error(
623 timeout,
624 dial,
625 first_app_rejection,
626 aggregate,
627 ))
628 }
629
630 async fn spawn_chunk_put(
633 &self,
634 content: Bytes,
635 proof: Vec<u8>,
636 peer_id: PeerId,
637 addrs: Vec<MultiAddr>,
638 ) -> (PeerId, Result<XorName>) {
639 let result = self
640 .chunk_put_with_proof(content, proof, &peer_id, &addrs)
641 .await;
642 (peer_id, result)
643 }
644
645 pub async fn chunk_put_with_proof(
654 &self,
655 content: Bytes,
656 proof: Vec<u8>,
657 target_peer: &PeerId,
658 peer_addrs: &[MultiAddr],
659 ) -> Result<XorName> {
660 let address = compute_address(&content);
661 let node = self.network();
662 let timeout =
663 store_response_timeout_for_proof(&proof, self.config().merkle_store_timeout_secs);
664 let timeout_secs = timeout.as_secs();
665
666 let request_id = self.next_request_id();
667 let request = ChunkPutRequest::with_payment(address, content, proof);
671 let message = ChunkMessage {
672 request_id,
673 body: ChunkMessageBody::PutRequest(request),
674 };
675 let message_bytes = message
676 .encode()
677 .map_err(|e| Error::Protocol(format!("Failed to encode PUT request: {e}")))?;
678
679 let addr_hex = hex::encode(address);
680
681 let result = send_and_await_chunk_response(
682 node,
683 target_peer,
684 message_bytes,
685 request_id,
686 timeout,
687 peer_addrs,
688 |body| match body {
689 ChunkMessageBody::PutResponse(ChunkPutResponse::Success { address: addr }) => {
690 debug!("Chunk stored at {}", hex::encode(addr));
691 Some(Ok(addr))
692 }
693 ChunkMessageBody::PutResponse(ChunkPutResponse::AlreadyExists {
694 address: addr,
695 }) => {
696 debug!("Chunk already exists at {}", hex::encode(addr));
697 Some(Ok(addr))
698 }
699 ChunkMessageBody::PutResponse(ChunkPutResponse::PaymentRequired { message }) => {
700 Some(Err(Error::Payment(format!("Payment required: {message}"))))
701 }
702 ChunkMessageBody::PutResponse(ChunkPutResponse::Error(e)) => {
703 Some(Err(Error::RemotePut {
709 address: addr_hex.clone(),
710 source: e,
711 }))
712 }
713 _ => None,
714 },
715 |e| Error::Network(format!("Failed to send PUT to peer: {e}")),
716 || {
717 Error::Timeout(format!(
718 "Timeout waiting for store response after {timeout_secs}s"
719 ))
720 },
721 )
722 .await;
723
724 result
725 }
726
727 pub async fn chunk_get(&self, address: &XorName) -> Result<Option<DataChunk>> {
744 self.chunk_get_from_closest_peers(address, self.config().close_group_size)
745 .await
746 }
747
748 pub async fn chunk_get_from_closest_peers(
759 &self,
760 address: &XorName,
761 peer_count: usize,
762 ) -> Result<Option<DataChunk>> {
763 self.chunk_get_from_closest_peers_with_diagnostics(
764 address,
765 peer_count,
766 #[cfg(feature = "native")]
767 None,
768 )
769 .await
770 }
771
772 async fn chunk_get_from_closest_peers_with_diagnostics(
773 &self,
774 address: &XorName,
775 peer_count: usize,
776 #[cfg(feature = "native")] diag: Option<&ChunkFetchDiagnostics<'_>>,
777 ) -> Result<Option<DataChunk>> {
778 if let Some(cached) = self.chunk_cache().get(address) {
780 if crate::record::verify(address, &cached).is_ok() {
781 debug!("Cache hit for chunk {}", hex::encode(address));
782 #[cfg(feature = "native")]
783 if let Some(diag) = diag {
784 diag.emit_chunk_level(
785 "initial",
786 cached.len() as u64,
787 DownloadDiagnosticsOutcome::CacheHit,
788 None,
789 );
790 }
791 return Ok(Some(DataChunk::new(*address, cached)));
792 }
793 debug!(
795 "Cache corruption detected for {}: evicting",
796 hex::encode(address)
797 );
798 self.chunk_cache().remove(address);
799 }
800
801 #[cfg(feature = "native")]
802 let observation = diag.map(|_| std::sync::Mutex::new(ReadObservation::default()));
803 let result = crate::client_engine::read::retrieve_progressive(
804 *address,
805 peer_count,
806 |sender| {
807 #[cfg(feature = "native")]
808 let observation = &observation;
809 async move {
810 let progress = crate::data::network::ReadProgress::new(
811 *address,
812 *self.network().peer_id(),
813 sender,
814 );
815 self.network().seed_read_candidates(&progress).await;
816 #[cfg(feature = "native")]
817 let lookup_started = Instant::now();
818 #[cfg(feature = "native")]
819 let mut contexts = Vec::new();
820 #[cfg(feature = "native")]
821 let closest_result = if diag.is_some() {
822 self.network()
823 .find_closest_peers_with_diagnostics(address, peer_count)
824 .await
825 .map(|found| {
826 let peers = found
827 .iter()
828 .map(|c| (c.peer_id, c.addresses.clone()))
829 .collect();
830 contexts = found;
831 peers
832 })
833 } else {
834 self.closest_peers(address, peer_count).await
835 };
836 #[cfg(not(feature = "native"))]
837 let closest_result = self
838 .network()
839 .find_read_peers(address, peer_count, progress)
840 .await;
841 let closest = closest_result.unwrap_or_else(|e| {
842 #[cfg(feature = "native")]
843 if let (Some(diag), Some(observation)) = (diag, &observation) {
844 let round = observation.lock().unwrap_or_else(|e| e.into_inner()).round;
845 diag.emit_chunk_level(
846 if round == 0 { "initial" } else { "retry" },
847 0,
848 DownloadDiagnosticsOutcome::LookupError,
849 Some(bounded_error("lookup", &e.to_string())),
850 );
851 }
852 info!(
853 "Chunk discovery failed for {}: {e}; trying known peers",
854 hex::encode(address)
855 );
856 Vec::new()
857 });
858 let known = self
859 .network()
860 .known_peers()
861 .await
862 .into_iter()
863 .filter(|node| node.peer_id != *self.network().peer_id())
864 .map(|node| {
865 let addrs = node.addresses_by_priority();
866 (node.peer_id, addrs)
867 })
868 .collect();
869 #[cfg(feature = "native")]
870 if let (Some(diag), Some(observation)) = (diag, &observation) {
871 let mut state = observation.lock().unwrap_or_else(|e| e.into_inner());
872 state.round += 1;
873 state.peer_attempt = 0;
874 state.lookup_ms =
875 u64::try_from(lookup_started.elapsed().as_millis()).unwrap_or(u64::MAX);
876 state.lookup_id = format!(
877 "{}-{}-{}-{}",
878 diag.file_attempt,
879 diag.chunk_index,
880 hex::encode(address),
881 NEXT_DIAGNOSTIC_LOOKUP_ID.fetch_add(1, AtomicOrdering::Relaxed)
882 );
883 state.contexts = contexts.into_iter().map(|c| (c.peer_id, c)).collect();
884 }
885 crate::client_engine::read::ReadCandidates { closest, known }
886 }
887 },
888 |(peer, _)| *peer.as_bytes(),
889 |(peer, addrs), early| {
890 #[cfg(feature = "native")]
891 let observation = &observation;
892 async move {
893 if early {
894 #[cfg(feature = "native")]
895 if let Some(diag) = diag {
896 let early_observation = std::sync::Mutex::new(ReadObservation {
897 lookup_id: format!(
898 "{}-{}-early-{}",
899 diag.file_attempt,
900 diag.chunk_index,
901 NEXT_DIAGNOSTIC_LOOKUP_ID.fetch_add(1, AtomicOrdering::Relaxed)
902 ),
903 ..ReadObservation::default()
904 });
905 return self
906 .chunk_get_diagnostic_attempt(
907 address,
908 &peer,
909 &addrs,
910 diag,
911 &early_observation,
912 )
913 .await;
914 }
915 }
916 #[cfg(feature = "native")]
917 if let (Some(diag), Some(observation)) = (diag, observation) {
918 return self
919 .chunk_get_diagnostic_attempt(address, &peer, &addrs, diag, observation)
920 .await;
921 }
922 self.chunk_get_from_peer(address, &peer, &addrs).await
923 }
924 },
925 |error| {
926 matches!(
927 error,
928 Error::Timeout(_) | Error::Network(_) | Error::Protocol(_)
929 )
930 },
931 crate::runtime::sleep,
932 )
933 .await?;
934 #[cfg(feature = "native")]
935 if result.is_none() {
936 if let (Some(diag), Some(observation)) = (diag, &observation) {
937 let round = observation.lock().unwrap_or_else(|e| e.into_inner()).round;
938 diag.emit_chunk_level(
939 if round == 1 { "initial" } else { "retry" },
940 0,
941 DownloadDiagnosticsOutcome::Exhausted,
942 None,
943 );
944 }
945 }
946 if let Some(chunk) = &result {
947 self.chunk_cache().put(chunk.address, chunk.content.clone());
948 }
949 Ok(result)
950 }
951
952 pub async fn chunk_get_from_close_group(
962 &self,
963 address: &XorName,
964 ) -> Result<Vec<ChunkPeerGetResult>> {
965 self.chunk_get_from_closest_peer_group(address, self.config().close_group_size)
966 .await
967 }
968
969 pub async fn chunk_get_from_closest_peer_group(
980 &self,
981 address: &XorName,
982 peer_count: usize,
983 ) -> Result<Vec<ChunkPeerGetResult>> {
984 let peers = self.closest_peers(address, peer_count).await?;
985 let targets = chunk_peer_get_targets(peers, address);
986 let concurrency_limit =
987 diagnostic_peer_get_concurrency(peer_count, self.config().close_group_size);
988 let per_peer_timeout = Duration::from_secs(self.config().chunk_get_timeout_secs);
989 let overall_timeout =
990 diagnostic_peer_get_overall_timeout(per_peer_timeout, targets.len(), concurrency_limit);
991
992 let mut completed = vec![false; targets.len()];
993 let mut results = Vec::with_capacity(targets.len());
994 let mut get_results = stream::iter(targets.iter().cloned())
995 .map(|target| async move {
996 let chunk_result = self
997 .chunk_get_from_peer(address, &target.peer_id, &target.peer_addrs)
998 .await;
999
1000 if let Ok(Some(chunk)) = &chunk_result {
1001 self.chunk_cache().put(chunk.address, chunk.content.clone());
1002 }
1003
1004 (
1005 target.index,
1006 ChunkPeerGetResult {
1007 peer_id: target.peer_id,
1008 peer_addrs: target.peer_addrs,
1009 xor_distance: target.xor_distance,
1010 chunk_result,
1011 },
1012 )
1013 })
1014 .buffer_unordered(concurrency_limit);
1015
1016 let collect_results = async {
1017 while let Some((index, result)) = get_results.next().await {
1018 completed[index] = true;
1019 results.push(result);
1020 }
1021 };
1022
1023 if crate::runtime::timeout(overall_timeout, collect_results)
1024 .await
1025 .is_err()
1026 {
1027 for target in &targets {
1028 if !completed[target.index] {
1029 results.push(timed_out_chunk_peer_get_result(
1030 target,
1031 address,
1032 overall_timeout,
1033 ));
1034 }
1035 }
1036 }
1037
1038 sort_chunk_peer_get_results(&mut results);
1039 Ok(results)
1040 }
1041
1042 async fn chunk_get_from_peer(
1044 &self,
1045 address: &XorName,
1046 peer: &PeerId,
1047 peer_addrs: &[MultiAddr],
1048 ) -> Result<Option<DataChunk>> {
1049 let node = self.network();
1050 let request_id = self.next_request_id();
1051 let request = ChunkGetRequest::new(*address);
1052 let message = ChunkMessage {
1053 request_id,
1054 body: ChunkMessageBody::GetRequest(request),
1055 };
1056 let message_bytes = message
1057 .encode()
1058 .map_err(|e| Error::Protocol(format!("Failed to encode GET request: {e}")))?;
1059
1060 let timeout = Duration::from_secs(self.config().chunk_get_timeout_secs);
1061 let addr_hex = hex::encode(address);
1062 let timeout_secs = self.config().chunk_get_timeout_secs;
1063
1064 let result = send_and_await_chunk_response(
1065 node,
1066 peer,
1067 message_bytes,
1068 request_id,
1069 timeout,
1070 peer_addrs,
1071 |body| match body {
1072 ChunkMessageBody::GetResponse(ChunkGetResponse::Success {
1073 address: addr,
1074 content,
1075 }) => {
1076 if addr != *address {
1077 return Some(Err(Error::InvalidData(format!(
1078 "Mismatched chunk address: expected {addr_hex}, got {}",
1079 hex::encode(addr)
1080 ))));
1081 }
1082
1083 if let Err(error) = crate::record::verify(&addr, &content) {
1084 return Some(Err(Error::InvalidData(error)));
1085 }
1086
1087 debug!(
1088 "Retrieved chunk {} ({} bytes) from peer {peer}",
1089 hex::encode(addr),
1090 content.len()
1091 );
1092 Some(Ok(Some(DataChunk::new(addr, Bytes::from(content)))))
1093 }
1094 ChunkMessageBody::GetResponse(ChunkGetResponse::NotFound { .. }) => Some(Ok(None)),
1095 ChunkMessageBody::GetResponse(ChunkGetResponse::Error(e)) => Some(Err(
1096 Error::Protocol(format!("Remote GET error for {addr_hex}: {e}")),
1097 )),
1098 _ => None,
1099 },
1100 |e| Error::Network(format!("Failed to send GET to peer {peer}: {e}")),
1101 || {
1102 Error::Timeout(format!(
1103 "Timeout waiting for chunk {addr_hex} from {peer} after {timeout_secs}s"
1104 ))
1105 },
1106 )
1107 .await;
1108
1109 result
1110 }
1111
1112 pub async fn chunk_exists(&self, address: &XorName) -> Result<bool> {
1118 self.chunk_get(address).await.map(|opt| opt.is_some())
1119 }
1120
1121 pub async fn finalize_chunk(
1139 &self,
1140 prepared: PreparedChunk,
1141 tx_hash_map: &HashMap<QuoteHash, TxHash>,
1142 ) -> Result<XorName> {
1143 let mut paid = finalize_batch_payment(vec![prepared], tx_hash_map)?;
1144 let chunk = paid.pop().ok_or_else(|| {
1148 Error::Payment(
1149 "finalize_batch_payment returned no paid chunks for a single \
1150 prepared chunk — internal invariant violated"
1151 .into(),
1152 )
1153 })?;
1154 self.chunk_put_to_close_group(chunk.content, chunk.proof_bytes, &chunk.quoted_peers)
1155 .await
1156 }
1157}
1158
1159#[cfg(test)]
1160mod tests {
1161 use super::*;
1162 use ant_protocol::{PROOF_TAG_MERKLE, PROOF_TAG_SINGLE_NODE};
1163
1164 #[cfg(feature = "native")]
1165 #[test]
1166 fn diagnostic_correlation_is_identical_on_wire_and_in_record() {
1167 let address = [7u8; 32];
1168 let correlation = DownloadRequestCorrelation::new(
1169 9_903,
1170 &PeerId::from_bytes([42; TEST_XORNAME_BYTE_LEN]),
1171 );
1172 let encoded = encode_diagnostic_chunk_get_request(&address, &correlation).unwrap();
1173 let wire = ChunkMessage::decode(&encoded).unwrap();
1174 assert_eq!(wire.request_id, correlation.request_id);
1175 assert!(matches!(wire.body, ChunkMessageBody::GetRequest(_)));
1176
1177 let record = DownloadDiagnosticsRecord::peer_attempt(
1178 1,
1179 1,
1180 &address,
1181 "initial",
1182 1,
1183 None,
1184 "lookup-1",
1185 "expected-peer",
1186 Vec::new(),
1187 Vec::new(),
1188 None,
1189 None,
1190 None,
1191 None,
1192 None,
1193 "unknown",
1194 None,
1195 Some(false),
1196 Some(1),
1197 Some(8),
1198 100,
1199 200,
1200 &correlation,
1201 100,
1202 0,
1203 DownloadDiagnosticsOutcome::Timeout,
1204 Some("timeout".to_string()),
1205 );
1206 assert_eq!(record.request_id, Some(wire.request_id));
1207 assert_eq!(record.local_peer_id, Some(correlation.local_peer_id));
1208 }
1209 #[cfg(feature = "native")]
1210 #[test]
1211 fn classify_peer_attempt_pins_outcomes_and_response_attribution() {
1212 let chunk = DataChunk::new([0u8; 32], Bytes::from_static(b"payload"));
1213 let cases = [
1214 (
1215 Ok(Some(chunk)),
1216 DownloadDiagnosticsOutcome::Found,
1217 7,
1218 true,
1219 None,
1220 ),
1221 (
1222 Ok(None),
1223 DownloadDiagnosticsOutcome::NotFound,
1224 0,
1225 true,
1226 None,
1227 ),
1228 (
1229 Err(Error::Timeout("late".to_string())),
1230 DownloadDiagnosticsOutcome::Timeout,
1231 0,
1232 false,
1233 Some("timeout: late"),
1234 ),
1235 (
1236 Err(Error::Network("dial".to_string())),
1237 DownloadDiagnosticsOutcome::NetworkError,
1238 0,
1239 false,
1240 Some("network: dial"),
1241 ),
1242 (
1243 Err(Error::InvalidData("hash".to_string())),
1244 DownloadDiagnosticsOutcome::ProtocolError,
1245 0,
1246 true,
1247 Some("protocol: hash"),
1248 ),
1249 (
1250 Err(Error::Protocol("remote".to_string())),
1251 DownloadDiagnosticsOutcome::ProtocolError,
1252 0,
1253 false,
1254 Some("protocol: remote"),
1255 ),
1256 ];
1257
1258 for (result, expected_outcome, expected_bytes, expected_response, expected_error) in cases {
1259 let (outcome, bytes, got_response, error) = classify_peer_attempt(&result);
1260 assert_eq!(outcome, expected_outcome);
1261 assert_eq!(bytes, expected_bytes);
1262 assert_eq!(got_response, expected_response);
1263 assert_eq!(error.as_deref(), expected_error);
1264 }
1265 }
1266 const TEST_MERKLE_TIMEOUT_SECS: u64 = 60;
1268 const UNKNOWN_PROOF_TAG: u8 = 0xff;
1270 const TEST_XORNAME_BYTE_LEN: usize = 32;
1272 const TEST_DISTANCE_TAIL_INDEX: usize = TEST_XORNAME_BYTE_LEN - 1;
1274
1275 #[test]
1276 fn classify_put_failure_maps_remote_timeout_and_dial_reasons() {
1277 let remote = |source| Error::RemotePut {
1278 address: "test-addr".to_string(),
1279 source,
1280 };
1281 assert!(matches!(
1282 classify_put_failure(&remote(ProtocolError::StorageFailed("full".to_string()))),
1283 PutRejection::Full
1284 ));
1285 assert!(matches!(
1286 classify_put_failure(&remote(ProtocolError::PaymentFailed(
1287 "below floor".to_string()
1288 ))),
1289 PutRejection::PriceFloor
1290 ));
1291 assert!(matches!(
1292 classify_put_failure(&remote(ProtocolError::Internal("boom".to_string()))),
1293 PutRejection::OtherRemote
1294 ));
1295 assert!(matches!(
1298 classify_put_failure(&Error::Payment("Payment required: more".to_string())),
1299 PutRejection::PriceFloor
1300 ));
1301 assert!(matches!(
1303 classify_put_failure(&Error::Timeout("no response".to_string())),
1304 PutRejection::Timeout
1305 ));
1306 assert!(matches!(
1308 classify_put_failure(&Error::Network("dial failed".to_string())),
1309 PutRejection::Dial
1310 ));
1311 }
1312
1313 #[test]
1314 fn put_shortfall_routes_by_failure_mix() {
1315 let app = || Error::Payment("Payment required: more".to_string());
1316 let msg = || "shortfall".to_string();
1317
1318 assert!(matches!(
1322 put_shortfall_error(0, 0, Some(app()), msg()),
1323 Error::Payment(_)
1324 ));
1325 assert!(matches!(
1328 put_shortfall_error(1, 0, Some(app()), msg()),
1329 Error::InsufficientPeers(_)
1330 ));
1331 assert!(matches!(
1332 put_shortfall_error(1, 3, None, msg()),
1333 Error::InsufficientPeers(_)
1334 ));
1335 assert!(matches!(
1338 put_shortfall_error(0, 2, None, msg()),
1339 Error::CloseGroupShortfall(_)
1340 ));
1341 assert!(matches!(
1343 put_shortfall_error(0, 1, Some(app()), msg()),
1344 Error::CloseGroupShortfall(_)
1345 ));
1346 }
1347
1348 fn chunk_peer_get_result(peer_seed: u8, distance_tail: u8) -> ChunkPeerGetResult {
1349 let mut xor_distance = [0; TEST_XORNAME_BYTE_LEN];
1350 xor_distance[TEST_DISTANCE_TAIL_INDEX] = distance_tail;
1351
1352 ChunkPeerGetResult {
1353 peer_id: PeerId::from_bytes([peer_seed; TEST_XORNAME_BYTE_LEN]),
1354 peer_addrs: Vec::new(),
1355 xor_distance,
1356 chunk_result: Ok(None),
1357 }
1358 }
1359
1360 #[test]
1361 fn authoritative_not_found_requires_unanimous_well_sampled_response() {
1362 assert!(is_authoritative_not_found(7, 7));
1365 assert!(is_authoritative_not_found(
1368 CLOSE_GROUP_MAJORITY,
1369 CLOSE_GROUP_MAJORITY
1370 ));
1371
1372 assert!(!is_authoritative_not_found(1, 1));
1377 assert!(!is_authoritative_not_found(3, 3));
1378 assert!(!is_authoritative_not_found(
1379 CLOSE_GROUP_MAJORITY - 1,
1380 CLOSE_GROUP_MAJORITY - 1
1381 ));
1382
1383 assert!(!is_authoritative_not_found(4, 7));
1386 assert!(!is_authoritative_not_found(6, 7));
1387
1388 assert!(!is_authoritative_not_found(0, 7));
1390
1391 assert!(!is_authoritative_not_found(0, 0));
1394 }
1395
1396 #[test]
1397 fn chunk_get_outcome_classifies_each_result_kind() {
1398 let chunk = DataChunk::new([0u8; 32], Bytes::from_static(b"x"));
1401 assert_eq!(
1402 chunk_get_outcome(&Ok(Some(chunk))),
1403 Outcome::Success,
1404 "found-chunk must be Success",
1405 );
1406
1407 assert_eq!(
1412 chunk_get_outcome(&Ok(None)),
1413 Outcome::Timeout,
1414 "Ok(None) must be Timeout — that's the controller's load-shedding signal",
1415 );
1416
1417 assert_eq!(
1419 chunk_get_outcome(&Err(Error::Timeout("t".into()))),
1420 Outcome::Timeout,
1421 );
1422 assert_eq!(
1423 chunk_get_outcome(&Err(Error::Network("n".into()))),
1424 Outcome::NetworkError,
1425 );
1426
1427 assert_eq!(
1430 chunk_get_outcome(&Err(Error::Protocol("p".into()))),
1431 Outcome::ApplicationError,
1432 );
1433 }
1434
1435 #[test]
1436 fn single_node_proof_uses_store_response_timeout() {
1437 let timeout =
1438 store_response_timeout_for_proof(&[PROOF_TAG_SINGLE_NODE], TEST_MERKLE_TIMEOUT_SECS);
1439
1440 assert_eq!(timeout, STORE_RESPONSE_TIMEOUT);
1441 }
1442
1443 #[test]
1444 fn unknown_proof_uses_store_response_timeout() {
1445 let timeout =
1446 store_response_timeout_for_proof(&[UNKNOWN_PROOF_TAG], TEST_MERKLE_TIMEOUT_SECS);
1447
1448 assert_eq!(timeout, STORE_RESPONSE_TIMEOUT);
1449 }
1450
1451 #[test]
1452 fn merkle_proof_uses_configured_store_timeout() {
1453 let timeout =
1454 store_response_timeout_for_proof(&[PROOF_TAG_MERKLE], TEST_MERKLE_TIMEOUT_SECS);
1455
1456 assert_eq!(timeout, Duration::from_secs(TEST_MERKLE_TIMEOUT_SECS));
1457 }
1458
1459 #[test]
1460 fn chunk_peer_get_results_sort_by_xor_distance() {
1461 let mut results = vec![
1462 chunk_peer_get_result(3, 3),
1463 chunk_peer_get_result(1, 1),
1464 chunk_peer_get_result(2, 2),
1465 ];
1466
1467 sort_chunk_peer_get_results(&mut results);
1468
1469 let ordered_distances = results
1470 .iter()
1471 .map(|result| result.xor_distance[TEST_DISTANCE_TAIL_INDEX])
1472 .collect::<Vec<_>>();
1473 assert_eq!(ordered_distances, vec![1, 2, 3]);
1474 }
1475
1476 #[test]
1477 fn diagnostic_peer_get_overall_timeout_allows_one_wave_plus_padding() {
1478 const PER_PEER_TIMEOUT_SECS: u64 = 10;
1479 const EXPECTED_WAVES_WITH_PADDING: u64 = 2;
1480 const TARGET_COUNT: usize = 7;
1481 const CONCURRENCY_LIMIT: usize = 7;
1482
1483 let timeout = diagnostic_peer_get_overall_timeout(
1484 Duration::from_secs(PER_PEER_TIMEOUT_SECS),
1485 TARGET_COUNT,
1486 CONCURRENCY_LIMIT,
1487 );
1488
1489 assert_eq!(
1490 timeout,
1491 Duration::from_secs(PER_PEER_TIMEOUT_SECS * EXPECTED_WAVES_WITH_PADDING)
1492 );
1493 }
1494
1495 #[test]
1496 fn diagnostic_peer_get_overall_timeout_scales_with_peer_count() {
1497 const PER_PEER_TIMEOUT_SECS: u64 = 10;
1498 const TARGET_COUNT: usize = 20;
1499 const CLOSE_GROUP_SIZE: usize = 7;
1500 const EXPECTED_WAVES_WITH_PADDING: u64 = 4;
1501
1502 let concurrency_limit = diagnostic_peer_get_concurrency(TARGET_COUNT, CLOSE_GROUP_SIZE);
1503 let timeout = diagnostic_peer_get_overall_timeout(
1504 Duration::from_secs(PER_PEER_TIMEOUT_SECS),
1505 TARGET_COUNT,
1506 concurrency_limit,
1507 );
1508
1509 assert_eq!(
1510 timeout,
1511 Duration::from_secs(PER_PEER_TIMEOUT_SECS * EXPECTED_WAVES_WITH_PADDING)
1512 );
1513 }
1514
1515 #[test]
1522 fn default_merkle_store_timeout_satisfies_storer_invariant() {
1523 use crate::data::client::ClientConfig;
1524 const STORER_CLOSENESS_LOOKUP_TIMEOUT_SECS: u64 = 240;
1525 const MIN_PADDING_SECS: u64 = 30;
1526 let config = ClientConfig::default();
1527 assert!(
1528 config.merkle_store_timeout_secs
1529 >= STORER_CLOSENESS_LOOKUP_TIMEOUT_SECS + MIN_PADDING_SECS,
1530 "merkle_store_timeout_secs ({}) must be >= storer CLOSENESS_LOOKUP_TIMEOUT ({}) + padding ({})",
1531 config.merkle_store_timeout_secs,
1532 STORER_CLOSENESS_LOOKUP_TIMEOUT_SECS,
1533 MIN_PADDING_SECS,
1534 );
1535 }
1536
1537 #[test]
1546 fn non_merkle_put_ignores_merkle_timeout_value() {
1547 let absurd_merkle_timeout = 9_999;
1548 for tag in [PROOF_TAG_SINGLE_NODE, UNKNOWN_PROOF_TAG] {
1549 let timeout = store_response_timeout_for_proof(&[tag], absurd_merkle_timeout);
1550 assert_eq!(
1551 timeout, STORE_RESPONSE_TIMEOUT,
1552 "non-merkle proof tag {tag:#x} should ignore merkle timeout {absurd_merkle_timeout}",
1553 );
1554 }
1555 }
1556}
1557
1558#[cfg(feature = "native")]
1559impl Client {
1560 async fn chunk_get_from_peer_with_metadata(
1561 &self,
1562 address: &XorName,
1563 peer: &PeerId,
1564 peer_addrs: &[MultiAddr],
1565 correlation: &DownloadRequestCorrelation,
1566 ) -> Result<ChunkProtocolResponse<Option<DataChunk>, Error>> {
1567 let node = self.network().node();
1568 let message_bytes = encode_diagnostic_chunk_get_request(address, correlation)?;
1569
1570 let timeout = Duration::from_secs(self.config().chunk_get_timeout_secs);
1571 let addr_hex = hex::encode(address);
1572 let timeout_secs = self.config().chunk_get_timeout_secs;
1573
1574 send_and_await_chunk_response_with_metadata(
1575 node,
1576 peer,
1577 message_bytes,
1578 correlation.request_id,
1579 timeout,
1580 peer_addrs,
1581 |body| match body {
1582 ChunkMessageBody::GetResponse(ChunkGetResponse::Success {
1583 address: addr,
1584 content,
1585 }) => {
1586 if addr != *address {
1587 return Some(Err(Error::InvalidData(format!(
1588 "Mismatched chunk address: expected {addr_hex}, got {}",
1589 hex::encode(addr)
1590 ))));
1591 }
1592 let computed = compute_address(&content);
1593 if computed != addr {
1594 return Some(Err(Error::InvalidData(format!(
1595 "Invalid chunk content: expected hash {addr_hex}, got {}",
1596 hex::encode(computed)
1597 ))));
1598 }
1599 debug!(
1600 "Retrieved chunk {} ({} bytes) from peer {peer}",
1601 hex::encode(addr),
1602 content.len()
1603 );
1604 Some(Ok(Some(DataChunk::new(addr, Bytes::from(content)))))
1605 }
1606 ChunkMessageBody::GetResponse(ChunkGetResponse::NotFound { .. }) => Some(Ok(None)),
1607 ChunkMessageBody::GetResponse(ChunkGetResponse::Error(e)) => Some(Err(
1608 Error::Protocol(format!("Remote GET error for {addr_hex}: {e}")),
1609 )),
1610 _ => None,
1611 },
1612 |e| Error::Network(format!("Failed to send GET to peer {peer}: {e}")),
1613 || {
1614 Error::Timeout(format!(
1615 "Timeout waiting for chunk {addr_hex} from {peer} after {timeout_secs}s"
1616 ))
1617 },
1618 )
1619 .await
1620 }
1621}
1622
1623#[cfg(feature = "native")]
1624#[derive(Default)]
1625struct ReadObservation {
1626 round: usize,
1627 peer_attempt: usize,
1628 lookup_ms: u64,
1629 lookup_id: String,
1630 contexts: std::collections::HashMap<PeerId, ClosestPeerDiagnostics>,
1631}
1632#[cfg(feature = "native")]
1633impl Client {
1634 async fn chunk_get_diagnostic_attempt(
1635 &self,
1636 address: &XorName,
1637 peer: &PeerId,
1638 addrs: &[MultiAddr],
1639 diag: &ChunkFetchDiagnostics<'_>,
1640 observation: &std::sync::Mutex<ReadObservation>,
1641 ) -> Result<Option<DataChunk>> {
1642 let (sweep, peer_attempt_no, lookup_duration_opt, lookup_correlation_id, peer_context) = {
1643 let mut state = observation.lock().unwrap_or_else(|e| e.into_inner());
1644 state.peer_attempt += 1;
1645 let context = state
1646 .contexts
1647 .remove(peer)
1648 .unwrap_or_else(|| ClosestPeerDiagnostics {
1649 peer_id: *peer,
1650 addresses: addrs.to_vec(),
1651 address_types: Vec::new(),
1652 local_last_seen_age_ms: None,
1653 publisher_address_set_age_ms: None,
1654 publisher_address_set_unix_ns: None,
1655 });
1656 (
1657 match state.round {
1658 0 => "early",
1659 1 => "initial",
1660 _ => "retry",
1661 },
1662 state.peer_attempt,
1663 (state.round != 0).then_some(state.lookup_ms),
1664 state.lookup_id.clone(),
1665 context,
1666 )
1667 };
1668 let node = self.network().node();
1669 let peer_connected_before_request = node.is_peer_connected(peer).await;
1670 let (active_guard, active_requests_at_start) = ActiveDiagnosticRequestGuard::enter();
1671 let request_started_unix_ms = unix_now_ms();
1672 let resp_start = Instant::now();
1673 let correlation = DownloadRequestCorrelation::new(self.next_request_id(), node.peer_id());
1674 let observed = self
1675 .chunk_get_from_peer_with_metadata(address, peer, addrs, &correlation)
1676 .await;
1677 let response_elapsed_ms =
1678 u64::try_from(resp_start.elapsed().as_millis()).unwrap_or(u64::MAX);
1679 let request_completed_unix_ms = unix_now_ms();
1680 drop(active_guard);
1683
1684 let (result, source_peer, transport_source, route) = match observed {
1685 Ok(response) => {
1686 let route = node
1687 .classify_peer_transport_route(
1688 &response.source_peer,
1689 response.transport_source.as_ref(),
1690 )
1691 .await;
1692 (
1693 response.result,
1694 Some(response.source_peer),
1695 response.transport_source,
1696 route,
1697 )
1698 }
1699 Err(error) => (Err(error), None, None, PeerRouteKind::Unknown),
1700 };
1701 let (outcome, bytes, _got_response, error) = classify_peer_attempt(&result);
1702 let lookup = if peer_attempt_no == 1 {
1703 lookup_duration_opt
1704 } else {
1705 None
1706 };
1707 diag.emit_peer_attempt(
1708 sweep,
1709 peer_attempt_no,
1710 lookup,
1711 &lookup_correlation_id,
1712 &peer_context,
1713 peer,
1714 source_peer.as_ref(),
1715 transport_source.as_ref(),
1716 route,
1717 peer_connected_before_request,
1718 active_requests_at_start,
1719 request_started_unix_ms,
1720 request_completed_unix_ms,
1721 &correlation,
1722 response_elapsed_ms,
1723 bytes,
1724 outcome,
1725 error,
1726 );
1727 result
1728 }
1729}