1use crate::data::client::adaptive::Outcome;
7use crate::data::client::batch::{finalize_batch_payment, PreparedChunk};
8use crate::data::client::diagnostics::{
9 bounded_error, unix_now_ms, DownloadDiagnosticsOutcome, DownloadDiagnosticsRecord,
10 DownloadDiagnosticsSender, DownloadRequestCorrelation,
11};
12use crate::data::client::peer_xor_distance;
13use crate::data::client::Client;
14use crate::data::error::{Error, Result};
15use crate::data::network::ClosestPeerDiagnostics;
16use ant_protocol::evm::{QuoteHash, TxHash};
17use ant_protocol::transport::{MultiAddr, PeerId, PeerRouteKind};
18use ant_protocol::{
19 compute_address, detect_proof_type, send_and_await_chunk_response,
20 send_and_await_chunk_response_with_metadata, ChunkGetRequest, ChunkGetResponse, ChunkMessage,
21 ChunkMessageBody, ChunkProtocolResponse, ChunkPutRequest, ChunkPutResponse, DataChunk,
22 ProofType, ProtocolError, XorName, CLOSE_GROUP_MAJORITY,
23};
24use bytes::Bytes;
25use futures::stream::{self, FuturesUnordered, StreamExt};
26use std::collections::HashMap;
27use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
28use std::time::{Duration, Instant};
29use tracing::{debug, info, warn};
30
31const CHUNK_DATA_TYPE: u32 = 0;
33
34static ACTIVE_DIAGNOSTIC_REQUESTS: AtomicUsize = AtomicUsize::new(0);
37static NEXT_DIAGNOSTIC_LOOKUP_ID: AtomicUsize = AtomicUsize::new(1);
38
39struct ActiveDiagnosticRequestGuard;
40
41impl ActiveDiagnosticRequestGuard {
42 fn enter() -> (Self, usize) {
43 let active = ACTIVE_DIAGNOSTIC_REQUESTS.fetch_add(1, AtomicOrdering::Relaxed) + 1;
44 (Self, active)
45 }
46}
47
48impl Drop for ActiveDiagnosticRequestGuard {
49 fn drop(&mut self) {
50 ACTIVE_DIAGNOSTIC_REQUESTS.fetch_sub(1, AtomicOrdering::Relaxed);
51 }
52}
53
54fn encode_diagnostic_chunk_get_request(
55 address: &XorName,
56 correlation: &DownloadRequestCorrelation,
57) -> Result<Vec<u8>> {
58 ChunkMessage {
59 request_id: correlation.request_id,
60 body: ChunkMessageBody::GetRequest(ChunkGetRequest::new(*address)),
61 }
62 .encode()
63 .map_err(|e| Error::Protocol(format!("Failed to encode GET request: {e}")))
64}
65
66#[derive(Clone, Copy)]
73enum PutRejection {
74 Full,
77 PriceFloor,
83 OtherRemote,
85 Timeout,
90 Dial,
96}
97
98fn classify_put_failure(error: &Error) -> PutRejection {
104 match error {
105 Error::RemotePut { source, .. } => match source {
106 ProtocolError::StorageFailed(_) => PutRejection::Full,
107 ProtocolError::PaymentFailed(_) => PutRejection::PriceFloor,
108 _ => PutRejection::OtherRemote,
109 },
110 Error::Payment(_) => PutRejection::PriceFloor,
116 Error::Timeout(_) => PutRejection::Timeout,
118 _ => PutRejection::Dial,
121 }
122}
123
124fn put_shortfall_error(
143 timeout: usize,
144 dial: usize,
145 first_app_rejection: Option<Error>,
146 shortfall_message: String,
147) -> Error {
148 if timeout > 0 {
149 return Error::InsufficientPeers(shortfall_message);
150 }
151 if dial == 0 {
152 if let Some(app_rejection) = first_app_rejection {
153 return app_rejection;
154 }
155 }
156 Error::CloseGroupShortfall(shortfall_message)
157}
158
159struct CloseGroupOutcome {
171 chunk: Option<DataChunk>,
172 queried: usize,
173 not_found: usize,
174 timeout: usize,
175 network_err: usize,
176 protocol_err: usize,
183}
184
185fn is_authoritative_not_found(not_found: usize, queried: usize) -> bool {
208 queried >= CLOSE_GROUP_MAJORITY && not_found == queried
209}
210
211const STORE_RESPONSE_TIMEOUT: Duration = Duration::from_secs(10);
213
214const DIAGNOSTIC_TIMEOUT_PADDING_WAVES: usize = 1;
216
217pub struct ChunkPeerGetResult {
219 pub peer_id: PeerId,
221 pub peer_addrs: Vec<MultiAddr>,
223 pub xor_distance: [u8; 32],
225 pub chunk_result: Result<Option<DataChunk>>,
227}
228
229#[derive(Clone)]
230struct ChunkPeerGetTarget {
231 index: usize,
232 peer_id: PeerId,
233 peer_addrs: Vec<MultiAddr>,
234 xor_distance: [u8; 32],
235}
236
237pub(crate) struct ChunkFetchDiagnostics<'a> {
245 sender: &'a DownloadDiagnosticsSender,
246 file_attempt: usize,
247 chunk_index: usize,
248 chunk_address: [u8; 32],
249 fetch_cap: usize,
250}
251
252impl<'a> ChunkFetchDiagnostics<'a> {
253 pub(crate) fn new(
254 sender: &'a DownloadDiagnosticsSender,
255 file_attempt: usize,
256 chunk_index: usize,
257 chunk_address: [u8; 32],
258 fetch_cap: usize,
259 ) -> Self {
260 Self {
261 sender,
262 file_attempt,
263 chunk_index,
264 chunk_address,
265 fetch_cap,
266 }
267 }
268
269 #[allow(clippy::too_many_arguments)]
272 fn emit_peer_attempt(
273 &self,
274 sweep: &'static str,
275 peer_attempt: usize,
276 lookup_duration_ms: Option<u64>,
277 lookup_correlation_id: &str,
278 peer_context: &ClosestPeerDiagnostics,
279 expected_peer: &PeerId,
280 source_peer: Option<&PeerId>,
281 transport_source: Option<&MultiAddr>,
282 route: PeerRouteKind,
283 peer_connected_before_request: bool,
284 active_requests_at_start: usize,
285 request_started_unix_ms: u64,
286 request_completed_unix_ms: u64,
287 correlation: &DownloadRequestCorrelation,
288 response_elapsed_ms: u64,
289 bytes: u64,
290 outcome: DownloadDiagnosticsOutcome,
291 error: Option<String>,
292 ) {
293 self.sender
294 .try_emit(DownloadDiagnosticsRecord::peer_attempt(
295 self.file_attempt,
296 self.chunk_index,
297 &self.chunk_address,
298 sweep,
299 peer_attempt,
300 lookup_duration_ms,
301 lookup_correlation_id,
302 &expected_peer.to_string(),
303 peer_context
304 .addresses
305 .iter()
306 .map(ToString::to_string)
307 .collect(),
308 peer_context.address_types.clone(),
309 peer_context.local_last_seen_age_ms,
310 peer_context.publisher_address_set_age_ms,
311 peer_context.publisher_address_set_unix_ns,
312 source_peer.map(ToString::to_string).as_deref(),
313 transport_source.map(ToString::to_string).as_deref(),
314 route.as_str(),
315 (route == PeerRouteKind::Unknown)
316 .then_some(DownloadDiagnosticsRecord::ROUTE_UNKNOWN_NOTE),
317 Some(peer_connected_before_request),
318 Some(active_requests_at_start),
319 Some(self.fetch_cap),
320 request_started_unix_ms,
321 request_completed_unix_ms,
322 correlation,
323 response_elapsed_ms,
324 bytes,
325 outcome,
326 error,
327 ));
328 }
329
330 fn emit_chunk_level(
332 &self,
333 sweep: &'static str,
334 bytes: u64,
335 outcome: DownloadDiagnosticsOutcome,
336 error: Option<String>,
337 ) {
338 self.sender.try_emit(DownloadDiagnosticsRecord::chunk_level(
339 self.file_attempt,
340 self.chunk_index,
341 &self.chunk_address,
342 sweep,
343 Some(self.fetch_cap),
344 bytes,
345 outcome,
346 error,
347 ));
348 }
349}
350
351fn classify_peer_attempt(
356 result: &Result<Option<DataChunk>>,
357) -> (DownloadDiagnosticsOutcome, u64, bool, Option<String>) {
358 match result {
359 Ok(Some(chunk)) => (
360 DownloadDiagnosticsOutcome::Found,
361 chunk.content.len() as u64,
362 true,
363 None,
364 ),
365 Ok(None) => (DownloadDiagnosticsOutcome::NotFound, 0, true, None),
366 Err(Error::Timeout(msg)) => (
367 DownloadDiagnosticsOutcome::Timeout,
368 0,
369 false,
370 Some(bounded_error("timeout", msg)),
371 ),
372 Err(Error::Network(msg)) => (
373 DownloadDiagnosticsOutcome::NetworkError,
374 0,
375 false,
376 Some(bounded_error("network", msg)),
377 ),
378 Err(Error::InvalidData(msg)) => (
381 DownloadDiagnosticsOutcome::ProtocolError,
382 0,
383 true,
384 Some(bounded_error("protocol", msg)),
385 ),
386 Err(Error::Protocol(msg)) => (
390 DownloadDiagnosticsOutcome::ProtocolError,
391 0,
392 false,
393 Some(bounded_error("protocol", msg)),
394 ),
395 Err(e) => (
396 DownloadDiagnosticsOutcome::ProtocolError,
397 0,
398 false,
399 Some(bounded_error("protocol", &e.to_string())),
400 ),
401 }
402}
403
404fn chunk_peer_get_targets(
405 peers: Vec<(PeerId, Vec<MultiAddr>)>,
406 address: &XorName,
407) -> Vec<ChunkPeerGetTarget> {
408 peers
409 .into_iter()
410 .enumerate()
411 .map(|(index, (peer_id, peer_addrs))| ChunkPeerGetTarget {
412 index,
413 peer_id,
414 peer_addrs,
415 xor_distance: peer_xor_distance(&peer_id, address),
416 })
417 .collect()
418}
419
420fn sort_chunk_peer_get_results(results: &mut [ChunkPeerGetResult]) {
421 results.sort_by_key(|result| result.xor_distance);
422}
423
424fn diagnostic_peer_get_concurrency(peer_count: usize, close_group_size: usize) -> usize {
425 peer_count.min(close_group_size.max(1))
426}
427
428fn diagnostic_peer_get_overall_timeout(
429 per_peer_timeout: Duration,
430 target_count: usize,
431 concurrency_limit: usize,
432) -> Duration {
433 let concurrency_limit = concurrency_limit.max(1);
434 let peer_get_waves = target_count.div_ceil(concurrency_limit);
435 let timeout_waves = peer_get_waves.saturating_add(DIAGNOSTIC_TIMEOUT_PADDING_WAVES);
436 let timeout_waves = u32::try_from(timeout_waves).unwrap_or(u32::MAX);
437
438 per_peer_timeout.saturating_mul(timeout_waves)
439}
440
441fn timed_out_chunk_peer_get_result(
442 target: &ChunkPeerGetTarget,
443 address: &XorName,
444 timeout: Duration,
445) -> ChunkPeerGetResult {
446 let addr_hex = hex::encode(address);
447 let timeout_secs = timeout.as_secs();
448 ChunkPeerGetResult {
449 peer_id: target.peer_id,
450 peer_addrs: target.peer_addrs.clone(),
451 xor_distance: target.xor_distance,
452 chunk_result: Err(Error::Timeout(format!(
453 "Diagnostic chunk GET sweep timed out before peer {} completed for chunk {addr_hex} after {timeout_secs}s",
454 target.peer_id
455 ))),
456 }
457}
458
459fn store_response_timeout_for_proof(proof: &[u8], merkle_timeout_secs: u64) -> Duration {
460 match detect_proof_type(proof) {
461 Some(ProofType::Merkle) => Duration::from_secs(merkle_timeout_secs),
462 _ => STORE_RESPONSE_TIMEOUT,
463 }
464}
465
466impl Client {
467 pub(crate) async fn chunk_get_observed(&self, address: &XorName) -> Result<Option<DataChunk>> {
478 self.chunk_get_observed_from_closest_peers(address, self.config().close_group_size, None)
479 .await
480 }
481
482 pub(crate) async fn chunk_get_observed_from_closest_peers(
483 &self,
484 address: &XorName,
485 peer_count: usize,
486 diag: Option<&ChunkFetchDiagnostics<'_>>,
487 ) -> Result<Option<DataChunk>> {
488 let started = Instant::now();
489 let result = self
490 .chunk_get_from_closest_peers_with_diagnostics(address, peer_count, diag)
491 .await;
492 let latency = started.elapsed();
493 let bytes = result
494 .as_ref()
495 .ok()
496 .and_then(Option::as_ref)
497 .map_or(0, |chunk| chunk.content.len() as u64);
498 self.controller()
499 .fetch
500 .observe_with_bytes(chunk_get_outcome(&result), latency, bytes);
501 result
502 }
503}
504
505pub(crate) fn chunk_get_outcome(result: &Result<Option<DataChunk>>) -> Outcome {
522 match result {
523 Ok(Some(_)) => Outcome::Success,
524 Ok(None) => Outcome::Timeout,
525 Err(Error::Timeout(_)) => Outcome::Timeout,
526 Err(Error::Network(_)) => Outcome::NetworkError,
527 Err(_) => Outcome::ApplicationError,
528 }
529}
530
531impl Client {
532 pub async fn chunk_put(&self, content: Bytes) -> Result<XorName> {
543 let address = compute_address(&content);
544 let data_size = u64::try_from(content.len())
545 .map_err(|e| Error::InvalidData(format!("content size too large: {e}")))?;
546
547 match self
548 .pay_for_storage(&address, data_size, CHUNK_DATA_TYPE)
549 .await
550 {
551 Ok((proof, peers)) => self.chunk_put_to_close_group(content, proof, &peers).await,
552 Err(Error::AlreadyStored) => {
553 debug!(
554 "Chunk {} already stored on network, skipping payment",
555 hex::encode(address)
556 );
557 Ok(address)
558 }
559 Err(e) => Err(e),
560 }
561 }
562
563 #[cfg(feature = "test-utils")]
577 pub async fn chunk_put_with_dead_initial_peers(
578 &self,
579 content: Bytes,
580 dead_count: usize,
581 ) -> Result<XorName> {
582 let address = compute_address(&content);
583 let data_size = u64::try_from(content.len())
584 .map_err(|e| Error::InvalidData(format!("content size too large: {e}")))?;
585 let (proof, real_peers) = self
586 .pay_for_storage(&address, data_size, CHUNK_DATA_TYPE)
587 .await?;
588 let mut peers: Vec<(PeerId, Vec<MultiAddr>)> = (0..dead_count)
592 .map(|_| (PeerId::random(), Vec::new()))
593 .collect();
594 peers.extend(real_peers);
595 self.chunk_put_to_close_group(content, proof, &peers).await
596 }
597
598 pub(crate) async fn chunk_put_to_close_group(
614 &self,
615 content: Bytes,
616 proof: Vec<u8>,
617 peers: &[(PeerId, Vec<MultiAddr>)],
618 ) -> Result<XorName> {
619 let address = compute_address(&content);
620
621 let initial_count = peers.len().min(CLOSE_GROUP_MAJORITY);
622 let (initial_peers, fallback_peers) = peers.split_at(initial_count);
623 let mut fallback_iter = fallback_peers.iter();
624
625 let mut put_futures = FuturesUnordered::new();
626 for (peer_id, addrs) in initial_peers {
627 put_futures.push(self.spawn_chunk_put(
628 content.clone(),
629 proof.clone(),
630 *peer_id,
631 addrs.clone(),
632 ));
633 }
634
635 let mut success_count = 0usize;
636 let mut failures: Vec<String> = Vec::new();
637 let mut full = 0usize;
645 let mut price_floor = 0usize;
646 let mut other_remote = 0usize;
647 let mut timeout = 0usize;
648 let mut dial = 0usize;
649 let mut first_app_rejection: Option<Error> = None;
650
651 while let Some((peer_id, result)) = put_futures.next().await {
652 match result {
653 Ok(_) => {
654 success_count += 1;
655 if success_count >= CLOSE_GROUP_MAJORITY {
656 debug!(
657 "Chunk {} stored on {success_count} peers (majority reached)",
658 hex::encode(address)
659 );
660 return Ok(address);
661 }
662 }
663 Err(e) => {
664 warn!("Failed to store chunk on {peer_id}: {e}");
665 failures.push(format!("{peer_id}: {e}"));
666 match classify_put_failure(&e) {
667 PutRejection::Full => full += 1,
668 PutRejection::PriceFloor => price_floor += 1,
669 PutRejection::OtherRemote => other_remote += 1,
670 PutRejection::Timeout => timeout += 1,
671 PutRejection::Dial => dial += 1,
672 }
673 if matches!(e, Error::RemotePut { .. } | Error::Payment(_))
679 && first_app_rejection.is_none()
680 {
681 first_app_rejection = Some(e);
682 }
683
684 if let Some((fb_peer, fb_addrs)) = fallback_iter.next() {
687 debug!(
688 "Falling back to peer {fb_peer} for chunk {}",
689 hex::encode(address)
690 );
691 put_futures.push(self.spawn_chunk_put(
692 content.clone(),
693 proof.clone(),
694 *fb_peer,
695 fb_addrs.clone(),
696 ));
697 }
698 }
699 }
700 }
701
702 let aggregate = format!(
707 "Stored on {success_count} peers, need {CLOSE_GROUP_MAJORITY} \
708 (full: {full}, price-floor: {price_floor}, other-rejection: {other_remote}, \
709 timeout: {timeout}, dial: {dial}). Failures: [{}]",
710 failures.join("; ")
711 );
712 Err(put_shortfall_error(
713 timeout,
714 dial,
715 first_app_rejection,
716 aggregate,
717 ))
718 }
719
720 async fn spawn_chunk_put(
723 &self,
724 content: Bytes,
725 proof: Vec<u8>,
726 peer_id: PeerId,
727 addrs: Vec<MultiAddr>,
728 ) -> (PeerId, Result<XorName>) {
729 let result = self
730 .chunk_put_with_proof(content, proof, &peer_id, &addrs)
731 .await;
732 (peer_id, result)
733 }
734
735 pub async fn chunk_put_with_proof(
744 &self,
745 content: Bytes,
746 proof: Vec<u8>,
747 target_peer: &PeerId,
748 peer_addrs: &[MultiAddr],
749 ) -> Result<XorName> {
750 let address = compute_address(&content);
751 let node = self.network().node();
752 let timeout =
753 store_response_timeout_for_proof(&proof, self.config().merkle_store_timeout_secs);
754 let timeout_secs = timeout.as_secs();
755
756 let request_id = self.next_request_id();
757 let request = ChunkPutRequest::with_payment(address, content, proof);
761 let message = ChunkMessage {
762 request_id,
763 body: ChunkMessageBody::PutRequest(request),
764 };
765 let message_bytes = message
766 .encode()
767 .map_err(|e| Error::Protocol(format!("Failed to encode PUT request: {e}")))?;
768
769 let addr_hex = hex::encode(address);
770
771 let result = send_and_await_chunk_response(
772 node,
773 target_peer,
774 message_bytes,
775 request_id,
776 timeout,
777 peer_addrs,
778 |body| match body {
779 ChunkMessageBody::PutResponse(ChunkPutResponse::Success { address: addr }) => {
780 debug!("Chunk stored at {}", hex::encode(addr));
781 Some(Ok(addr))
782 }
783 ChunkMessageBody::PutResponse(ChunkPutResponse::AlreadyExists {
784 address: addr,
785 }) => {
786 debug!("Chunk already exists at {}", hex::encode(addr));
787 Some(Ok(addr))
788 }
789 ChunkMessageBody::PutResponse(ChunkPutResponse::PaymentRequired { message }) => {
790 Some(Err(Error::Payment(format!("Payment required: {message}"))))
791 }
792 ChunkMessageBody::PutResponse(ChunkPutResponse::Error(e)) => {
793 Some(Err(Error::RemotePut {
799 address: addr_hex.clone(),
800 source: e,
801 }))
802 }
803 _ => None,
804 },
805 |e| Error::Network(format!("Failed to send PUT to peer: {e}")),
806 || {
807 Error::Timeout(format!(
808 "Timeout waiting for store response after {timeout_secs}s"
809 ))
810 },
811 )
812 .await;
813
814 result
815 }
816
817 pub async fn chunk_get(&self, address: &XorName) -> Result<Option<DataChunk>> {
842 self.chunk_get_from_closest_peers(address, self.config().close_group_size)
843 .await
844 }
845
846 pub async fn chunk_get_from_closest_peers(
857 &self,
858 address: &XorName,
859 peer_count: usize,
860 ) -> Result<Option<DataChunk>> {
861 self.chunk_get_from_closest_peers_with_diagnostics(address, peer_count, None)
862 .await
863 }
864
865 async fn chunk_get_from_closest_peers_with_diagnostics(
866 &self,
867 address: &XorName,
868 peer_count: usize,
869 diag: Option<&ChunkFetchDiagnostics<'_>>,
870 ) -> Result<Option<DataChunk>> {
871 if let Some(cached) = self.chunk_cache().get(address) {
873 let computed = compute_address(&cached);
874 if computed == *address {
875 debug!("Cache hit for chunk {}", hex::encode(address));
876 if let Some(diag) = diag {
877 diag.emit_chunk_level(
878 "initial",
879 cached.len() as u64,
880 DownloadDiagnosticsOutcome::CacheHit,
881 None,
882 );
883 }
884 return Ok(Some(DataChunk::new(*address, cached)));
885 }
886 debug!(
888 "Cache corruption detected for {}: evicting",
889 hex::encode(address)
890 );
891 self.chunk_cache().remove(address);
892 }
893
894 let addr_hex = hex::encode(address);
895
896 let first = match self
906 .chunk_get_try_closest_peers(address, peer_count, diag, "initial")
907 .await
908 {
909 Ok(outcome) => outcome,
910 Err(e) => {
911 info!("chunk_get first close-group lookup failed for {addr_hex}: {e}; will retry");
912 CloseGroupOutcome {
913 chunk: None,
914 queried: 0,
915 not_found: 0,
916 timeout: 0,
917 network_err: 0,
918 protocol_err: 0,
919 }
920 }
921 };
922 if let Some(chunk) = first.chunk {
923 self.chunk_cache().put(chunk.address, chunk.content.clone());
924 return Ok(Some(chunk));
925 }
926
927 if is_authoritative_not_found(first.not_found, first.queried) {
932 info!(
933 "chunk_get giving up on {addr_hex} (unanimous NotFound): \
934 queried={} not_found={} timeout={} network_err={} protocol_err={}",
935 first.queried,
936 first.not_found,
937 first.timeout,
938 first.network_err,
939 first.protocol_err,
940 );
941 return Ok(None);
942 }
943
944 info!(
951 "chunk_get retrying {addr_hex} after reachability failure: \
952 queried={} not_found={} timeout={} network_err={} protocol_err={}",
953 first.queried, first.not_found, first.timeout, first.network_err, first.protocol_err,
954 );
955
956 tokio::time::sleep(Duration::from_secs(1)).await;
961
962 let retry = match self
966 .chunk_get_try_closest_peers(address, peer_count, diag, "retry")
967 .await
968 {
969 Ok(o) => o,
970 Err(e) => {
971 info!(
972 "chunk_get retry close-group lookup failed for {addr_hex}: {e}; \
973 first(queried={} not_found={} timeout={} network_err={} protocol_err={})",
974 first.queried,
975 first.not_found,
976 first.timeout,
977 first.network_err,
978 first.protocol_err,
979 );
980 return Ok(None);
981 }
982 };
983 if let Some(chunk) = retry.chunk {
984 info!("chunk_get retry succeeded for {addr_hex}");
985 self.chunk_cache().put(chunk.address, chunk.content.clone());
986 return Ok(Some(chunk));
987 }
988
989 info!(
990 "chunk_get exhausted close group after retry for {addr_hex}: \
991 first(queried={} not_found={} timeout={} network_err={} protocol_err={}) \
992 retry(queried={} not_found={} timeout={} network_err={} protocol_err={})",
993 first.queried,
994 first.not_found,
995 first.timeout,
996 first.network_err,
997 first.protocol_err,
998 retry.queried,
999 retry.not_found,
1000 retry.timeout,
1001 retry.network_err,
1002 retry.protocol_err,
1003 );
1004 Ok(None)
1005 }
1006
1007 async fn chunk_get_try_closest_peers(
1015 &self,
1016 address: &XorName,
1017 peer_count: usize,
1018 diag: Option<&ChunkFetchDiagnostics<'_>>,
1019 sweep: &'static str,
1020 ) -> Result<CloseGroupOutcome> {
1021 let lookup_start = Instant::now();
1022 let (peers, peer_contexts) = if diag.is_some() {
1023 match self
1024 .network()
1025 .find_closest_peers_with_diagnostics(address, peer_count)
1026 .await
1027 {
1028 Ok(contexts) => {
1029 let peers = contexts
1030 .iter()
1031 .map(|context| (context.peer_id, context.addresses.clone()))
1032 .collect();
1033 (peers, Some(contexts))
1034 }
1035 Err(e) => {
1036 if let Some(diag) = diag {
1037 diag.emit_chunk_level(
1038 sweep,
1039 0,
1040 DownloadDiagnosticsOutcome::LookupError,
1041 Some(bounded_error("lookup", &e.to_string())),
1042 );
1043 }
1044 return Err(e);
1045 }
1046 }
1047 } else {
1048 match self.closest_peers(address, peer_count).await {
1051 Ok(peers) => (peers, None),
1052 Err(e) => return Err(e),
1053 }
1054 };
1055 let lookup_duration_ms =
1056 u64::try_from(lookup_start.elapsed().as_millis()).unwrap_or(u64::MAX);
1057 let lookup_duration_opt = Some(lookup_duration_ms);
1058 let addr_hex = hex::encode(address);
1059 let lookup_correlation_id = diag.map(|diag| {
1060 let sequence = NEXT_DIAGNOSTIC_LOOKUP_ID.fetch_add(1, AtomicOrdering::Relaxed);
1061 format!(
1062 "{}-{}-{}-{}-{sequence}",
1063 diag.file_attempt, diag.chunk_index, sweep, addr_hex
1064 )
1065 });
1066 let queried = peers.len();
1067 let mut not_found = 0usize;
1068 let mut timeout = 0usize;
1069 let mut network_err = 0usize;
1070 let mut protocol_err = 0usize;
1071
1072 for (peer_attempt, (peer, addrs)) in peers.iter().enumerate() {
1073 let peer_attempt_no = peer_attempt + 1;
1074 let result = if let Some(diag) = diag {
1075 let Some(peer_context) = peer_contexts
1076 .as_ref()
1077 .and_then(|contexts| contexts.get(peer_attempt))
1078 else {
1079 return Err(Error::Network(
1080 "diagnostics peer context missing for selected peer".to_string(),
1081 ));
1082 };
1083 let Some(lookup_correlation_id) = lookup_correlation_id.as_deref() else {
1084 return Err(Error::Network(
1085 "diagnostics lookup correlation ID missing".to_string(),
1086 ));
1087 };
1088 let node = self.network().node();
1089 let peer_connected_before_request = node.is_peer_connected(peer).await;
1090 let (active_guard, active_requests_at_start) =
1091 ActiveDiagnosticRequestGuard::enter();
1092 let request_started_unix_ms = unix_now_ms();
1093 let resp_start = Instant::now();
1094 let correlation =
1095 DownloadRequestCorrelation::new(self.next_request_id(), node.peer_id());
1096 let observed = self
1097 .chunk_get_from_peer_with_metadata(address, peer, addrs, &correlation)
1098 .await;
1099 let response_elapsed_ms =
1100 u64::try_from(resp_start.elapsed().as_millis()).unwrap_or(u64::MAX);
1101 let request_completed_unix_ms = unix_now_ms();
1102 drop(active_guard);
1105
1106 let (result, source_peer, transport_source, route) = match observed {
1107 Ok(response) => {
1108 let route = node
1109 .classify_peer_transport_route(
1110 &response.source_peer,
1111 response.transport_source.as_ref(),
1112 )
1113 .await;
1114 (
1115 response.result,
1116 Some(response.source_peer),
1117 response.transport_source,
1118 route,
1119 )
1120 }
1121 Err(error) => (Err(error), None, None, PeerRouteKind::Unknown),
1122 };
1123 let (outcome, bytes, _got_response, error) = classify_peer_attempt(&result);
1124 let lookup = if peer_attempt_no == 1 {
1125 lookup_duration_opt
1126 } else {
1127 None
1128 };
1129 diag.emit_peer_attempt(
1130 sweep,
1131 peer_attempt_no,
1132 lookup,
1133 lookup_correlation_id,
1134 peer_context,
1135 peer,
1136 source_peer.as_ref(),
1137 transport_source.as_ref(),
1138 route,
1139 peer_connected_before_request,
1140 active_requests_at_start,
1141 request_started_unix_ms,
1142 request_completed_unix_ms,
1143 &correlation,
1144 response_elapsed_ms,
1145 bytes,
1146 outcome,
1147 error,
1148 );
1149 result
1150 } else {
1151 self.chunk_get_from_peer(address, peer, addrs).await
1154 };
1155 match result {
1156 Ok(Some(chunk)) => {
1157 return Ok(CloseGroupOutcome {
1158 chunk: Some(chunk),
1159 queried,
1160 not_found,
1161 timeout,
1162 network_err,
1163 protocol_err,
1164 });
1165 }
1166 Ok(None) => {
1167 not_found += 1;
1168 debug!("Chunk {addr_hex} not found on peer {peer}, trying next");
1169 }
1170 Err(Error::Timeout(_)) => {
1171 timeout += 1;
1172 debug!("Peer {peer} timed out for chunk {addr_hex}, trying next");
1173 }
1174 Err(Error::Network(_)) => {
1175 network_err += 1;
1176 debug!("Peer {peer} unreachable for chunk {addr_hex}, trying next");
1177 }
1178 Err(Error::Protocol(ref e)) => {
1187 protocol_err += 1;
1188 debug!(
1189 "Peer {peer} returned protocol error for chunk {addr_hex} ({e}), trying next"
1190 );
1191 }
1192 Err(e) => return Err(e),
1193 }
1194 }
1195
1196 if let Some(diag) = diag {
1200 diag.emit_chunk_level(sweep, 0, DownloadDiagnosticsOutcome::Exhausted, None);
1201 }
1202
1203 Ok(CloseGroupOutcome {
1204 chunk: None,
1205 queried,
1206 not_found,
1207 timeout,
1208 network_err,
1209 protocol_err,
1210 })
1211 }
1212
1213 pub async fn chunk_get_from_close_group(
1223 &self,
1224 address: &XorName,
1225 ) -> Result<Vec<ChunkPeerGetResult>> {
1226 self.chunk_get_from_closest_peer_group(address, self.config().close_group_size)
1227 .await
1228 }
1229
1230 pub async fn chunk_get_from_closest_peer_group(
1241 &self,
1242 address: &XorName,
1243 peer_count: usize,
1244 ) -> Result<Vec<ChunkPeerGetResult>> {
1245 let peers = self.closest_peers(address, peer_count).await?;
1246 let targets = chunk_peer_get_targets(peers, address);
1247 let concurrency_limit =
1248 diagnostic_peer_get_concurrency(peer_count, self.config().close_group_size);
1249 let per_peer_timeout = Duration::from_secs(self.config().chunk_get_timeout_secs);
1250 let overall_timeout =
1251 diagnostic_peer_get_overall_timeout(per_peer_timeout, targets.len(), concurrency_limit);
1252
1253 let mut completed = vec![false; targets.len()];
1254 let mut results = Vec::with_capacity(targets.len());
1255 let mut get_results = stream::iter(targets.iter().cloned())
1256 .map(|target| async move {
1257 let chunk_result = self
1258 .chunk_get_from_peer(address, &target.peer_id, &target.peer_addrs)
1259 .await;
1260
1261 if let Ok(Some(chunk)) = &chunk_result {
1262 self.chunk_cache().put(chunk.address, chunk.content.clone());
1263 }
1264
1265 (
1266 target.index,
1267 ChunkPeerGetResult {
1268 peer_id: target.peer_id,
1269 peer_addrs: target.peer_addrs,
1270 xor_distance: target.xor_distance,
1271 chunk_result,
1272 },
1273 )
1274 })
1275 .buffer_unordered(concurrency_limit);
1276
1277 let collect_results = async {
1278 while let Some((index, result)) = get_results.next().await {
1279 completed[index] = true;
1280 results.push(result);
1281 }
1282 };
1283
1284 if tokio::time::timeout(overall_timeout, collect_results)
1285 .await
1286 .is_err()
1287 {
1288 for target in &targets {
1289 if !completed[target.index] {
1290 results.push(timed_out_chunk_peer_get_result(
1291 target,
1292 address,
1293 overall_timeout,
1294 ));
1295 }
1296 }
1297 }
1298
1299 sort_chunk_peer_get_results(&mut results);
1300 Ok(results)
1301 }
1302
1303 async fn chunk_get_from_peer(
1305 &self,
1306 address: &XorName,
1307 peer: &PeerId,
1308 peer_addrs: &[MultiAddr],
1309 ) -> Result<Option<DataChunk>> {
1310 let node = self.network().node();
1311 let request_id = self.next_request_id();
1312 let request = ChunkGetRequest::new(*address);
1313 let message = ChunkMessage {
1314 request_id,
1315 body: ChunkMessageBody::GetRequest(request),
1316 };
1317 let message_bytes = message
1318 .encode()
1319 .map_err(|e| Error::Protocol(format!("Failed to encode GET request: {e}")))?;
1320
1321 let timeout = Duration::from_secs(self.config().chunk_get_timeout_secs);
1322 let addr_hex = hex::encode(address);
1323 let timeout_secs = self.config().chunk_get_timeout_secs;
1324
1325 let result = send_and_await_chunk_response(
1326 node,
1327 peer,
1328 message_bytes,
1329 request_id,
1330 timeout,
1331 peer_addrs,
1332 |body| match body {
1333 ChunkMessageBody::GetResponse(ChunkGetResponse::Success {
1334 address: addr,
1335 content,
1336 }) => {
1337 if addr != *address {
1338 return Some(Err(Error::InvalidData(format!(
1339 "Mismatched chunk address: expected {addr_hex}, got {}",
1340 hex::encode(addr)
1341 ))));
1342 }
1343
1344 let computed = compute_address(&content);
1345 if computed != addr {
1346 return Some(Err(Error::InvalidData(format!(
1347 "Invalid chunk content: expected hash {addr_hex}, got {}",
1348 hex::encode(computed)
1349 ))));
1350 }
1351
1352 debug!(
1353 "Retrieved chunk {} ({} bytes) from peer {peer}",
1354 hex::encode(addr),
1355 content.len()
1356 );
1357 Some(Ok(Some(DataChunk::new(addr, Bytes::from(content)))))
1358 }
1359 ChunkMessageBody::GetResponse(ChunkGetResponse::NotFound { .. }) => Some(Ok(None)),
1360 ChunkMessageBody::GetResponse(ChunkGetResponse::Error(e)) => Some(Err(
1361 Error::Protocol(format!("Remote GET error for {addr_hex}: {e}")),
1362 )),
1363 _ => None,
1364 },
1365 |e| Error::Network(format!("Failed to send GET to peer {peer}: {e}")),
1366 || {
1367 Error::Timeout(format!(
1368 "Timeout waiting for chunk {addr_hex} from {peer} after {timeout_secs}s"
1369 ))
1370 },
1371 )
1372 .await;
1373
1374 result
1375 }
1376
1377 async fn chunk_get_from_peer_with_metadata(
1382 &self,
1383 address: &XorName,
1384 peer: &PeerId,
1385 peer_addrs: &[MultiAddr],
1386 correlation: &DownloadRequestCorrelation,
1387 ) -> Result<ChunkProtocolResponse<Option<DataChunk>, Error>> {
1388 let node = self.network().node();
1389 let message_bytes = encode_diagnostic_chunk_get_request(address, correlation)?;
1390
1391 let timeout = Duration::from_secs(self.config().chunk_get_timeout_secs);
1392 let addr_hex = hex::encode(address);
1393 let timeout_secs = self.config().chunk_get_timeout_secs;
1394
1395 send_and_await_chunk_response_with_metadata(
1396 node,
1397 peer,
1398 message_bytes,
1399 correlation.request_id,
1400 timeout,
1401 peer_addrs,
1402 |body| match body {
1403 ChunkMessageBody::GetResponse(ChunkGetResponse::Success {
1404 address: addr,
1405 content,
1406 }) => {
1407 if addr != *address {
1408 return Some(Err(Error::InvalidData(format!(
1409 "Mismatched chunk address: expected {addr_hex}, got {}",
1410 hex::encode(addr)
1411 ))));
1412 }
1413 let computed = compute_address(&content);
1414 if computed != addr {
1415 return Some(Err(Error::InvalidData(format!(
1416 "Invalid chunk content: expected hash {addr_hex}, got {}",
1417 hex::encode(computed)
1418 ))));
1419 }
1420 debug!(
1421 "Retrieved chunk {} ({} bytes) from peer {peer}",
1422 hex::encode(addr),
1423 content.len()
1424 );
1425 Some(Ok(Some(DataChunk::new(addr, Bytes::from(content)))))
1426 }
1427 ChunkMessageBody::GetResponse(ChunkGetResponse::NotFound { .. }) => Some(Ok(None)),
1428 ChunkMessageBody::GetResponse(ChunkGetResponse::Error(e)) => Some(Err(
1429 Error::Protocol(format!("Remote GET error for {addr_hex}: {e}")),
1430 )),
1431 _ => None,
1432 },
1433 |e| Error::Network(format!("Failed to send GET to peer {peer}: {e}")),
1434 || {
1435 Error::Timeout(format!(
1436 "Timeout waiting for chunk {addr_hex} from {peer} after {timeout_secs}s"
1437 ))
1438 },
1439 )
1440 .await
1441 }
1442
1443 pub async fn chunk_exists(&self, address: &XorName) -> Result<bool> {
1449 self.chunk_get(address).await.map(|opt| opt.is_some())
1450 }
1451
1452 pub async fn finalize_chunk(
1470 &self,
1471 prepared: PreparedChunk,
1472 tx_hash_map: &HashMap<QuoteHash, TxHash>,
1473 ) -> Result<XorName> {
1474 let mut paid = finalize_batch_payment(vec![prepared], tx_hash_map)?;
1475 let chunk = paid.pop().ok_or_else(|| {
1479 Error::Payment(
1480 "finalize_batch_payment returned no paid chunks for a single \
1481 prepared chunk — internal invariant violated"
1482 .into(),
1483 )
1484 })?;
1485 self.chunk_put_to_close_group(chunk.content, chunk.proof_bytes, &chunk.quoted_peers)
1486 .await
1487 }
1488}
1489
1490#[cfg(test)]
1491mod tests {
1492 use super::*;
1493 use ant_protocol::{PROOF_TAG_MERKLE, PROOF_TAG_SINGLE_NODE};
1494
1495 const TEST_MERKLE_TIMEOUT_SECS: u64 = 60;
1497 const UNKNOWN_PROOF_TAG: u8 = 0xff;
1499 const TEST_XORNAME_BYTE_LEN: usize = 32;
1501 const TEST_DISTANCE_TAIL_INDEX: usize = TEST_XORNAME_BYTE_LEN - 1;
1503
1504 #[test]
1505 fn diagnostic_correlation_is_identical_on_wire_and_in_record() {
1506 let address = [7u8; 32];
1507 let correlation = DownloadRequestCorrelation::new(
1508 9_903,
1509 &PeerId::from_bytes([42; TEST_XORNAME_BYTE_LEN]),
1510 );
1511 let encoded = encode_diagnostic_chunk_get_request(&address, &correlation).unwrap();
1512 let wire = ChunkMessage::decode(&encoded).unwrap();
1513 assert_eq!(wire.request_id, correlation.request_id);
1514 assert!(matches!(wire.body, ChunkMessageBody::GetRequest(_)));
1515
1516 let record = DownloadDiagnosticsRecord::peer_attempt(
1517 1,
1518 1,
1519 &address,
1520 "initial",
1521 1,
1522 None,
1523 "lookup-1",
1524 "expected-peer",
1525 Vec::new(),
1526 Vec::new(),
1527 None,
1528 None,
1529 None,
1530 None,
1531 None,
1532 "unknown",
1533 None,
1534 Some(false),
1535 Some(1),
1536 Some(8),
1537 100,
1538 200,
1539 &correlation,
1540 100,
1541 0,
1542 DownloadDiagnosticsOutcome::Timeout,
1543 Some("timeout".to_string()),
1544 );
1545 assert_eq!(record.request_id, Some(wire.request_id));
1546 assert_eq!(record.local_peer_id, Some(correlation.local_peer_id));
1547 }
1548
1549 #[test]
1550 fn classify_peer_attempt_pins_outcomes_and_response_attribution() {
1551 let chunk = DataChunk::new([0u8; 32], Bytes::from_static(b"payload"));
1552 let cases = [
1553 (
1554 Ok(Some(chunk)),
1555 DownloadDiagnosticsOutcome::Found,
1556 7,
1557 true,
1558 None,
1559 ),
1560 (
1561 Ok(None),
1562 DownloadDiagnosticsOutcome::NotFound,
1563 0,
1564 true,
1565 None,
1566 ),
1567 (
1568 Err(Error::Timeout("late".to_string())),
1569 DownloadDiagnosticsOutcome::Timeout,
1570 0,
1571 false,
1572 Some("timeout: late"),
1573 ),
1574 (
1575 Err(Error::Network("dial".to_string())),
1576 DownloadDiagnosticsOutcome::NetworkError,
1577 0,
1578 false,
1579 Some("network: dial"),
1580 ),
1581 (
1582 Err(Error::InvalidData("hash".to_string())),
1583 DownloadDiagnosticsOutcome::ProtocolError,
1584 0,
1585 true,
1586 Some("protocol: hash"),
1587 ),
1588 (
1589 Err(Error::Protocol("remote".to_string())),
1590 DownloadDiagnosticsOutcome::ProtocolError,
1591 0,
1592 false,
1593 Some("protocol: remote"),
1594 ),
1595 ];
1596
1597 for (result, expected_outcome, expected_bytes, expected_response, expected_error) in cases {
1598 let (outcome, bytes, got_response, error) = classify_peer_attempt(&result);
1599 assert_eq!(outcome, expected_outcome);
1600 assert_eq!(bytes, expected_bytes);
1601 assert_eq!(got_response, expected_response);
1602 assert_eq!(error.as_deref(), expected_error);
1603 }
1604 }
1605
1606 #[test]
1607 fn classify_put_failure_maps_remote_timeout_and_dial_reasons() {
1608 let remote = |source| Error::RemotePut {
1609 address: "test-addr".to_string(),
1610 source,
1611 };
1612 assert!(matches!(
1613 classify_put_failure(&remote(ProtocolError::StorageFailed("full".to_string()))),
1614 PutRejection::Full
1615 ));
1616 assert!(matches!(
1617 classify_put_failure(&remote(ProtocolError::PaymentFailed(
1618 "below floor".to_string()
1619 ))),
1620 PutRejection::PriceFloor
1621 ));
1622 assert!(matches!(
1623 classify_put_failure(&remote(ProtocolError::Internal("boom".to_string()))),
1624 PutRejection::OtherRemote
1625 ));
1626 assert!(matches!(
1629 classify_put_failure(&Error::Payment("Payment required: more".to_string())),
1630 PutRejection::PriceFloor
1631 ));
1632 assert!(matches!(
1634 classify_put_failure(&Error::Timeout("no response".to_string())),
1635 PutRejection::Timeout
1636 ));
1637 assert!(matches!(
1639 classify_put_failure(&Error::Network("dial failed".to_string())),
1640 PutRejection::Dial
1641 ));
1642 }
1643
1644 #[test]
1645 fn put_shortfall_routes_by_failure_mix() {
1646 let app = || Error::Payment("Payment required: more".to_string());
1647 let msg = || "shortfall".to_string();
1648
1649 assert!(matches!(
1653 put_shortfall_error(0, 0, Some(app()), msg()),
1654 Error::Payment(_)
1655 ));
1656 assert!(matches!(
1659 put_shortfall_error(1, 0, Some(app()), msg()),
1660 Error::InsufficientPeers(_)
1661 ));
1662 assert!(matches!(
1663 put_shortfall_error(1, 3, None, msg()),
1664 Error::InsufficientPeers(_)
1665 ));
1666 assert!(matches!(
1669 put_shortfall_error(0, 2, None, msg()),
1670 Error::CloseGroupShortfall(_)
1671 ));
1672 assert!(matches!(
1674 put_shortfall_error(0, 1, Some(app()), msg()),
1675 Error::CloseGroupShortfall(_)
1676 ));
1677 }
1678
1679 fn chunk_peer_get_result(peer_seed: u8, distance_tail: u8) -> ChunkPeerGetResult {
1680 let mut xor_distance = [0; TEST_XORNAME_BYTE_LEN];
1681 xor_distance[TEST_DISTANCE_TAIL_INDEX] = distance_tail;
1682
1683 ChunkPeerGetResult {
1684 peer_id: PeerId::from_bytes([peer_seed; TEST_XORNAME_BYTE_LEN]),
1685 peer_addrs: Vec::new(),
1686 xor_distance,
1687 chunk_result: Ok(None),
1688 }
1689 }
1690
1691 #[test]
1692 fn authoritative_not_found_requires_unanimous_well_sampled_response() {
1693 assert!(is_authoritative_not_found(7, 7));
1696 assert!(is_authoritative_not_found(
1699 CLOSE_GROUP_MAJORITY,
1700 CLOSE_GROUP_MAJORITY
1701 ));
1702
1703 assert!(!is_authoritative_not_found(1, 1));
1708 assert!(!is_authoritative_not_found(3, 3));
1709 assert!(!is_authoritative_not_found(
1710 CLOSE_GROUP_MAJORITY - 1,
1711 CLOSE_GROUP_MAJORITY - 1
1712 ));
1713
1714 assert!(!is_authoritative_not_found(4, 7));
1717 assert!(!is_authoritative_not_found(6, 7));
1718
1719 assert!(!is_authoritative_not_found(0, 7));
1721
1722 assert!(!is_authoritative_not_found(0, 0));
1725 }
1726
1727 #[test]
1728 fn chunk_get_outcome_classifies_each_result_kind() {
1729 let chunk = DataChunk::new([0u8; 32], Bytes::from_static(b"x"));
1732 assert_eq!(
1733 chunk_get_outcome(&Ok(Some(chunk))),
1734 Outcome::Success,
1735 "found-chunk must be Success",
1736 );
1737
1738 assert_eq!(
1743 chunk_get_outcome(&Ok(None)),
1744 Outcome::Timeout,
1745 "Ok(None) must be Timeout — that's the controller's load-shedding signal",
1746 );
1747
1748 assert_eq!(
1750 chunk_get_outcome(&Err(Error::Timeout("t".into()))),
1751 Outcome::Timeout,
1752 );
1753 assert_eq!(
1754 chunk_get_outcome(&Err(Error::Network("n".into()))),
1755 Outcome::NetworkError,
1756 );
1757
1758 assert_eq!(
1761 chunk_get_outcome(&Err(Error::Protocol("p".into()))),
1762 Outcome::ApplicationError,
1763 );
1764 }
1765
1766 #[test]
1767 fn single_node_proof_uses_store_response_timeout() {
1768 let timeout =
1769 store_response_timeout_for_proof(&[PROOF_TAG_SINGLE_NODE], TEST_MERKLE_TIMEOUT_SECS);
1770
1771 assert_eq!(timeout, STORE_RESPONSE_TIMEOUT);
1772 }
1773
1774 #[test]
1775 fn unknown_proof_uses_store_response_timeout() {
1776 let timeout =
1777 store_response_timeout_for_proof(&[UNKNOWN_PROOF_TAG], TEST_MERKLE_TIMEOUT_SECS);
1778
1779 assert_eq!(timeout, STORE_RESPONSE_TIMEOUT);
1780 }
1781
1782 #[test]
1783 fn merkle_proof_uses_configured_store_timeout() {
1784 let timeout =
1785 store_response_timeout_for_proof(&[PROOF_TAG_MERKLE], TEST_MERKLE_TIMEOUT_SECS);
1786
1787 assert_eq!(timeout, Duration::from_secs(TEST_MERKLE_TIMEOUT_SECS));
1788 }
1789
1790 #[test]
1791 fn chunk_peer_get_results_sort_by_xor_distance() {
1792 let mut results = vec![
1793 chunk_peer_get_result(3, 3),
1794 chunk_peer_get_result(1, 1),
1795 chunk_peer_get_result(2, 2),
1796 ];
1797
1798 sort_chunk_peer_get_results(&mut results);
1799
1800 let ordered_distances = results
1801 .iter()
1802 .map(|result| result.xor_distance[TEST_DISTANCE_TAIL_INDEX])
1803 .collect::<Vec<_>>();
1804 assert_eq!(ordered_distances, vec![1, 2, 3]);
1805 }
1806
1807 #[test]
1808 fn diagnostic_peer_get_overall_timeout_allows_one_wave_plus_padding() {
1809 const PER_PEER_TIMEOUT_SECS: u64 = 10;
1810 const EXPECTED_WAVES_WITH_PADDING: u64 = 2;
1811 const TARGET_COUNT: usize = 7;
1812 const CONCURRENCY_LIMIT: usize = 7;
1813
1814 let timeout = diagnostic_peer_get_overall_timeout(
1815 Duration::from_secs(PER_PEER_TIMEOUT_SECS),
1816 TARGET_COUNT,
1817 CONCURRENCY_LIMIT,
1818 );
1819
1820 assert_eq!(
1821 timeout,
1822 Duration::from_secs(PER_PEER_TIMEOUT_SECS * EXPECTED_WAVES_WITH_PADDING)
1823 );
1824 }
1825
1826 #[test]
1827 fn diagnostic_peer_get_overall_timeout_scales_with_peer_count() {
1828 const PER_PEER_TIMEOUT_SECS: u64 = 10;
1829 const TARGET_COUNT: usize = 20;
1830 const CLOSE_GROUP_SIZE: usize = 7;
1831 const EXPECTED_WAVES_WITH_PADDING: u64 = 4;
1832
1833 let concurrency_limit = diagnostic_peer_get_concurrency(TARGET_COUNT, CLOSE_GROUP_SIZE);
1834 let timeout = diagnostic_peer_get_overall_timeout(
1835 Duration::from_secs(PER_PEER_TIMEOUT_SECS),
1836 TARGET_COUNT,
1837 concurrency_limit,
1838 );
1839
1840 assert_eq!(
1841 timeout,
1842 Duration::from_secs(PER_PEER_TIMEOUT_SECS * EXPECTED_WAVES_WITH_PADDING)
1843 );
1844 }
1845
1846 #[test]
1853 fn default_merkle_store_timeout_satisfies_storer_invariant() {
1854 use crate::data::client::ClientConfig;
1855 const STORER_CLOSENESS_LOOKUP_TIMEOUT_SECS: u64 = 240;
1856 const MIN_PADDING_SECS: u64 = 30;
1857 let config = ClientConfig::default();
1858 assert!(
1859 config.merkle_store_timeout_secs
1860 >= STORER_CLOSENESS_LOOKUP_TIMEOUT_SECS + MIN_PADDING_SECS,
1861 "merkle_store_timeout_secs ({}) must be >= storer CLOSENESS_LOOKUP_TIMEOUT ({}) + padding ({})",
1862 config.merkle_store_timeout_secs,
1863 STORER_CLOSENESS_LOOKUP_TIMEOUT_SECS,
1864 MIN_PADDING_SECS,
1865 );
1866 }
1867
1868 #[test]
1877 fn non_merkle_put_ignores_merkle_timeout_value() {
1878 let absurd_merkle_timeout = 9_999;
1879 for tag in [PROOF_TAG_SINGLE_NODE, UNKNOWN_PROOF_TAG] {
1880 let timeout = store_response_timeout_for_proof(&[tag], absurd_merkle_timeout);
1881 assert_eq!(
1882 timeout, STORE_RESPONSE_TIMEOUT,
1883 "non-merkle proof tag {tag:#x} should ignore merkle timeout {absurd_merkle_timeout}",
1884 );
1885 }
1886 }
1887}