1use crate::data::client::adaptive::{observe_op, Outcome};
8use crate::data::client::classify_error;
9use crate::data::client::file::UploadEvent;
10use crate::data::client::quote::settlement_refusal_error;
11use crate::data::client::Client;
12use crate::data::error::{Error, Result};
13use ant_protocol::evm::{
14 Amount, MerklePaymentCandidateNode, MerklePaymentCandidatePool, MerklePaymentProof, MerkleTree,
15 MidpointProof, PoolCommitment, CANDIDATES_PER_POOL, MAX_LEAVES,
16};
17use ant_protocol::payment::commitment::{
18 commitment_hash, verify_commitment_signature, StorageCommitment, MAX_COMMITMENT_KEY_COUNT,
19 MAX_COMMITMENT_SIDECAR_BYTES,
20};
21use ant_protocol::payment::{
22 calculate_price, serialize_merkle_proof, verify_merkle_candidate_signature,
23};
24use ant_protocol::transport::PeerId;
25use ant_protocol::{
26 compute_address, send_and_await_chunk_response, ChunkMessage, ChunkMessageBody,
27 MerkleCandidateQuoteRequest, MerkleCandidateQuoteRequestV2, MerkleCandidateQuoteResponse,
28 ProtocolError,
29};
30use bytes::Bytes;
31use futures::stream::{self, FuturesUnordered, StreamExt};
32use rand::Rng;
33use std::collections::{HashMap, VecDeque};
34use std::sync::Arc;
35use std::time::Duration;
36use tokio::sync::mpsc;
37use tracing::{debug, info, warn};
38use xor_name::XorName;
39
40pub const DEFAULT_MERKLE_THRESHOLD: usize = 64;
42
43use crate::data::client::payment::SINGLE_NODE_PAYMENT_MULTIPLIER as MERKLE_PAYMENT_MULTIPLIER;
65
66fn merkle_candidate_binding_is_valid(
76 peer_id: &PeerId,
77 candidate: &MerklePaymentCandidateNode,
78 commitment: &Option<Vec<u8>>,
79) -> std::result::Result<(), String> {
80 let count = candidate.committed_key_count;
81 let pin = candidate.commitment_pin;
82 match (count, pin.is_some()) {
83 (0, false) | (1.., true) => {}
84 (1.., false) => {
85 return Err(format!(
86 "committed_key_count={count} > 0 but commitment_pin is None (unauditable count)"
87 ));
88 }
89 (0, true) => {
90 return Err("committed_key_count=0 with a commitment_pin (incoherent baseline)".into());
91 }
92 }
93 if count > MAX_COMMITMENT_KEY_COUNT {
94 return Err(format!(
95 "committed_key_count={count} exceeds MAX_COMMITMENT_KEY_COUNT={MAX_COMMITMENT_KEY_COUNT}"
96 ));
97 }
98 let expected = calculate_price(count as usize);
99 if candidate.price != expected {
100 return Err(format!(
101 "price {} does not equal calculate_price(committed_key_count={count}) = {expected}",
102 candidate.price
103 ));
104 }
105
106 let Some(pin) = pin else {
107 return Ok(()); };
109 let Some(blob) = commitment else {
110 return Err("bound candidate did not ship its commitment; pin is unresolvable".into());
111 };
112 if blob.len() > MAX_COMMITMENT_SIDECAR_BYTES {
113 return Err(format!(
114 "shipped commitment is {} bytes, exceeds MAX_COMMITMENT_SIDECAR_BYTES={MAX_COMMITMENT_SIDECAR_BYTES}",
115 blob.len()
116 ));
117 }
118 let commitment: StorageCommitment = rmp_serde::from_slice(blob)
119 .map_err(|e| format!("shipped commitment did not deserialize: {e}"))?;
120 if compute_address(&commitment.sender_public_key) != *peer_id.as_bytes()
121 || commitment.sender_peer_id != *peer_id.as_bytes()
122 {
123 return Err("shipped commitment is not bound to the candidate peer".into());
124 }
125 if !verify_commitment_signature(&commitment) {
126 return Err("shipped commitment has an invalid signature".into());
127 }
128 if commitment_hash(&commitment) != Some(pin) {
129 return Err("shipped commitment does not hash to the candidate's pin".into());
130 }
131 if commitment.key_count != count {
132 return Err(format!(
133 "shipped commitment attests key_count={} but the candidate claims {count}",
134 commitment.key_count
135 ));
136 }
137 Ok(())
138}
139
140fn pool_commitment_with_payment_multiplier(
153 pool: &MerklePaymentCandidatePool,
154) -> Result<PoolCommitment> {
155 let mut commitment = pool.to_commitment();
156 let multiplier = Amount::from(MERKLE_PAYMENT_MULTIPLIER);
157 for candidate in &mut commitment.candidates {
158 candidate.price = candidate.price.checked_mul(multiplier).ok_or_else(|| {
159 Error::Payment(format!(
160 "Merkle candidate amount overflow applying {MERKLE_PAYMENT_MULTIPLIER}x to price {}",
161 candidate.price
162 ))
163 })?;
164 }
165 Ok(commitment)
166}
167
168#[derive(Default)]
186struct PoolVerdict {
187 refusal: Option<Error>,
188 first_failure: Option<Error>,
189}
190
191impl PoolVerdict {
192 fn note(&mut self, e: Error) {
193 match e {
194 e @ Error::ClientUpdateRequired(_) => {
195 if self.refusal.is_none() {
196 self.refusal = Some(e);
197 }
198 }
199 e => {
200 if self.first_failure.is_none() {
201 self.first_failure = Some(e);
202 }
203 }
204 }
205 }
206
207 fn into_error(self) -> Option<Error> {
208 self.refusal.or(self.first_failure)
209 }
210}
211
212fn map_merkle_candidate_response(
213 peer_id: PeerId,
214 body: ChunkMessageBody,
215) -> Option<Result<(MerklePaymentCandidateNode, Option<Vec<u8>>)>> {
216 match body {
217 ChunkMessageBody::MerkleCandidateQuoteResponse(MerkleCandidateQuoteResponse::Success {
218 candidate_node,
219 commitment,
220 }) => match rmp_serde::from_slice::<MerklePaymentCandidateNode>(&candidate_node) {
221 Ok(node) => Some(Ok((node, commitment))),
222 Err(e) => Some(Err(Error::Serialization(format!(
223 "Failed to deserialize candidate node from {peer_id}: {e}"
224 )))),
225 },
226 ChunkMessageBody::MerkleCandidateQuoteResponse(MerkleCandidateQuoteResponse::Error(
233 ProtocolError::ClientUpdateRequired {
234 client_settlement_version,
235 min_settlement_version,
236 },
237 )) => Some(Err(settlement_refusal_error(
238 &peer_id,
239 client_settlement_version,
240 min_settlement_version,
241 ))),
242 ChunkMessageBody::MerkleCandidateQuoteResponse(MerkleCandidateQuoteResponse::Error(
243 behind @ ProtocolError::StorerUpdateRequired { .. },
244 )) => Some(Err(Error::StorerUpdateRequired(behind.to_string()))),
245 ChunkMessageBody::MerkleCandidateQuoteResponse(MerkleCandidateQuoteResponse::Error(e)) => {
246 Some(Err(Error::Protocol(format!(
247 "Merkle quote error from {peer_id}: {e}"
248 ))))
249 }
250 _ => None,
251 }
252}
253
254const _: () = crate::data::client::UNVERSIONED_RETRY_REQUIRES_MIN_V1;
288
289const fn is_version_unaware(error: &Error) -> bool {
290 matches!(error, Error::Network(_) | Error::Timeout(_))
291}
292
293#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
295#[serde(rename_all = "snake_case")]
296pub enum PaymentMode {
297 #[default]
299 Auto,
300 Merkle,
302 Single,
304}
305
306#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
311pub struct MerkleBatchPaymentResult {
312 pub proofs: HashMap<[u8; 32], Vec<u8>>,
314 pub chunk_count: usize,
316 pub storage_cost_atto: String,
318 pub gas_cost_wei: u128,
320 #[serde(default)]
325 pub merkle_payment_timestamp: u64,
326}
327
328pub struct PreparedMerkleBatch {
333 pub depth: u8,
335 pub pool_commitments: Vec<PoolCommitment>,
337 pub merkle_payment_timestamp: u64,
339 candidate_pools: Vec<MerklePaymentCandidatePool>,
341 tree: MerkleTree,
343 addresses: Vec<[u8; 32]>,
345}
346
347#[derive(Debug, Clone, Default)]
349pub(crate) struct MerkleUploadPlan {
350 pub already_stored: Vec<[u8; 32]>,
352 pub to_upload: Vec<[u8; 32]>,
354 to_upload_total_bytes: u64,
356}
357
358impl MerkleUploadPlan {
359 #[must_use]
361 pub fn to_upload_avg_size(&self) -> u64 {
362 if self.to_upload.is_empty() {
363 return 0;
364 }
365
366 self.to_upload_total_bytes / self.to_upload.len() as u64
367 }
368}
369
370impl std::fmt::Debug for PreparedMerkleBatch {
371 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372 f.debug_struct("PreparedMerkleBatch")
373 .field("depth", &self.depth)
374 .field("pool_commitments", &self.pool_commitments.len())
375 .field("merkle_payment_timestamp", &self.merkle_payment_timestamp)
376 .field("candidate_pools", &self.candidate_pools.len())
377 .field("addresses", &self.addresses.len())
378 .finish()
379 }
380}
381
382pub(crate) fn chunk_contents_for_upload_addresses(
387 chunk_contents: Vec<Bytes>,
388 addresses: &[[u8; 32]],
389) -> Result<Vec<Bytes>> {
390 if addresses.is_empty() {
391 return Ok(Vec::new());
392 }
393
394 let mut needed_by_address: HashMap<[u8; 32], usize> = HashMap::new();
395 for address in addresses {
396 *needed_by_address.entry(*address).or_default() += 1;
397 }
398
399 let mut chunks_by_address: HashMap<[u8; 32], VecDeque<Bytes>> =
400 HashMap::with_capacity(needed_by_address.len());
401 let mut remaining = addresses.len();
402 for chunk in chunk_contents {
403 let address = compute_address(&chunk);
404 if let Some(needed) = needed_by_address.get_mut(&address) {
405 if *needed > 0 {
406 chunks_by_address
407 .entry(address)
408 .or_default()
409 .push_back(chunk);
410 *needed -= 1;
411 remaining -= 1;
412 if remaining == 0 {
413 break;
414 }
415 }
416 }
417 }
418
419 for (address, needed) in &needed_by_address {
420 if *needed == 0 {
421 continue;
422 }
423
424 if chunks_by_address.contains_key(address) {
425 return Err(Error::InvalidData(format!(
426 "missing duplicate chunk content for merkle address {}",
427 hex::encode(address)
428 )));
429 }
430
431 return Err(Error::InvalidData(format!(
432 "missing chunk content for merkle address {}",
433 hex::encode(address)
434 )));
435 }
436
437 let mut selected = Vec::with_capacity(addresses.len());
438 for address in addresses {
439 let chunks = chunks_by_address.get_mut(address).ok_or_else(|| {
440 Error::InvalidData(format!(
441 "missing chunk content for merkle address {}",
442 hex::encode(address)
443 ))
444 })?;
445 let chunk = chunks.pop_front().ok_or_else(|| {
446 Error::InvalidData(format!(
447 "missing duplicate chunk content for merkle address {}",
448 hex::encode(address)
449 ))
450 })?;
451 selected.push(chunk);
452 }
453
454 Ok(selected)
455}
456
457fn preflight_stored_status<T>(result: Result<T>) -> Result<bool> {
476 match result {
477 Ok(_) => Ok(false),
478 Err(Error::AlreadyStored) => Ok(true),
479 Err(e) if matches!(classify_error(&e), Outcome::Timeout | Outcome::NetworkError) => {
480 Ok(false)
481 }
482 Err(e) => Err(e),
483 }
484}
485
486#[must_use]
504pub fn merkle_batch_sizes(total: usize) -> Vec<usize> {
505 merkle_batch_sizes_with_cap(total, MAX_LEAVES)
506}
507
508#[must_use]
518pub fn merkle_batch_sizes_with_cap(total: usize, cap: usize) -> Vec<usize> {
519 if total < 2 {
520 return Vec::new();
521 }
522 let cap = cap.clamp(3, MAX_LEAVES);
523
524 let mut sizes = Vec::with_capacity(total.div_ceil(cap));
525 let mut remaining = total;
526 while remaining > cap {
527 let take = if remaining - cap == 1 { cap - 1 } else { cap };
530 sizes.push(take);
531 remaining -= take;
532 }
533 sizes.push(remaining);
534 sizes
535}
536
537#[must_use]
542pub fn merkle_batch_partitions(addresses: &[[u8; 32]]) -> Vec<&[[u8; 32]]> {
543 merkle_batch_partitions_with_cap(addresses, MAX_LEAVES)
544}
545
546#[must_use]
549pub fn merkle_batch_partitions_with_cap(addresses: &[[u8; 32]], cap: usize) -> Vec<&[[u8; 32]]> {
550 let mut partitions = Vec::new();
551 let mut rest = addresses;
552 for size in merkle_batch_sizes_with_cap(addresses.len(), cap) {
553 let (batch, tail) = rest.split_at(size);
554 partitions.push(batch);
555 rest = tail;
556 }
557 partitions
558}
559
560#[must_use]
569pub(crate) fn merge_merkle_batch_results(
570 results: Vec<MerkleBatchPaymentResult>,
571) -> MerkleBatchPaymentResult {
572 let mut merged = MerkleBatchPaymentResult {
573 proofs: HashMap::new(),
574 chunk_count: 0,
575 storage_cost_atto: "0".to_string(),
576 gas_cost_wei: 0,
577 merkle_payment_timestamp: 0,
578 };
579 let mut total_storage = Amount::ZERO;
580 for result in results {
581 merged.proofs.extend(result.proofs);
582 merged.chunk_count += result.chunk_count;
583 if let Ok(cost) = result.storage_cost_atto.parse::<Amount>() {
584 total_storage += cost;
585 }
586 merged.gas_cost_wei = merged.gas_cost_wei.saturating_add(result.gas_cost_wei);
587 if merged.merkle_payment_timestamp == 0
588 || (result.merkle_payment_timestamp > 0
589 && result.merkle_payment_timestamp < merged.merkle_payment_timestamp)
590 {
591 merged.merkle_payment_timestamp = result.merkle_payment_timestamp;
592 }
593 }
594 merged.storage_cost_atto = total_storage.to_string();
595 merged
596}
597
598fn padded_leaf_count(batch_size: usize) -> u64 {
603 let padded = batch_size
606 .max(2)
607 .checked_next_power_of_two()
608 .unwrap_or(usize::MAX);
609 u64::try_from(padded).unwrap_or(u64::MAX)
610}
611
612#[must_use]
621pub fn merkle_billable_leaves(chunk_count: u64) -> u64 {
622 let total = usize::try_from(chunk_count).unwrap_or(usize::MAX);
623 let batches = merkle_batch_sizes(total);
624 if batches.is_empty() {
625 return if total == 0 { 0 } else { 2 };
628 }
629
630 batches
631 .into_iter()
632 .map(padded_leaf_count)
633 .fold(0u64, u64::saturating_add)
634}
635
636fn ensure_single_merkle_tree_batch(address_count: usize) -> Result<()> {
645 if address_count > MAX_LEAVES {
646 return Err(Error::MerkleBatchTooLarge {
647 addresses: address_count,
648 max_leaves: MAX_LEAVES,
649 });
650 }
651 Ok(())
652}
653
654#[must_use]
657pub fn should_use_merkle(chunk_count: usize, mode: PaymentMode) -> bool {
658 match mode {
659 PaymentMode::Auto => chunk_count >= DEFAULT_MERKLE_THRESHOLD,
660 PaymentMode::Merkle => chunk_count >= 2,
661 PaymentMode::Single => false,
662 }
663}
664
665impl Client {
666 #[must_use]
668 pub fn should_use_merkle(&self, chunk_count: usize, mode: PaymentMode) -> bool {
669 should_use_merkle(chunk_count, mode)
670 }
671
672 pub async fn pay_for_merkle_batch(
687 &self,
688 addresses: &[[u8; 32]],
689 data_type: u32,
690 data_size: u64,
691 ) -> Result<MerkleBatchPaymentResult> {
692 if let Some(refusal) = self.corroborated_settlement_refusal() {
697 return Err(Error::ClientUpdateRequired(refusal));
698 }
699 let chunk_count = addresses.len();
700 if chunk_count < 2 {
701 return Err(Error::Payment(
702 "Merkle batch payment requires at least 2 chunks".to_string(),
703 ));
704 }
705
706 if chunk_count > MAX_LEAVES {
707 return self
708 .pay_for_merkle_multi_batch(addresses, data_type, data_size)
709 .await;
710 }
711
712 self.pay_for_merkle_single_batch(addresses, data_type, data_size)
713 .await
714 }
715
716 pub(crate) async fn plan_merkle_upload(
725 &self,
726 chunks: Vec<([u8; 32], u64)>,
727 data_type: u32,
728 progress: Option<&mpsc::Sender<UploadEvent>>,
729 ) -> Result<MerkleUploadPlan> {
730 let total_chunks = chunks.len();
731 if total_chunks == 0 {
732 return Ok(MerkleUploadPlan::default());
733 }
734
735 info!("Checking {total_chunks} merkle chunks for existing storage before payment");
736
737 let quote_limiter = self.controller().quote.clone();
738 let quote_concurrency = quote_limiter.current().min(total_chunks.max(1));
739 let mut check_stream = stream::iter(chunks.into_iter().enumerate())
740 .map(|(index, (address, data_size))| {
741 let limiter = quote_limiter.clone();
742 async move {
743 let result = observe_op(
744 &limiter,
745 || async move {
746 self.chunk_already_stored_for_merkle(&address, data_type, data_size)
747 .await
748 },
749 classify_error,
750 )
751 .await;
752 (index, address, data_size, result)
753 }
754 })
755 .buffer_unordered(quote_concurrency);
756
757 let mut already_stored: Vec<(usize, [u8; 32])> = Vec::new();
758 let mut to_upload: Vec<(usize, [u8; 32], u64)> = Vec::new();
759 let mut checked = 0usize;
760
761 while let Some((index, address, data_size, result)) = check_stream.next().await {
762 let is_already_stored = result?;
763 checked += 1;
764
765 if let Some(tx) = progress {
766 let _ = tx.try_send(UploadEvent::ChunkQuoted {
767 quoted: checked,
768 total: total_chunks,
769 });
770 }
771
772 if is_already_stored {
773 debug!(
774 "Merkle preflight {checked}/{total_chunks}: chunk {} already stored",
775 hex::encode(address)
776 );
777 already_stored.push((index, address));
778 if let Some(tx) = progress {
779 let _ = tx.try_send(UploadEvent::ChunkStored {
780 stored: already_stored.len(),
781 total: total_chunks,
782 });
783 }
784 } else {
785 debug!(
786 "Merkle preflight {checked}/{total_chunks}: chunk {} needs upload",
787 hex::encode(address)
788 );
789 to_upload.push((index, address, data_size));
790 }
791 }
792
793 already_stored.sort_by_key(|(index, _)| *index);
794 to_upload.sort_by_key(|(index, _, _)| *index);
795
796 let to_upload_total_bytes = to_upload.iter().fold(0u64, |acc, (_, _, data_size)| {
797 acc.saturating_add(*data_size)
798 });
799
800 let already_stored = already_stored
801 .into_iter()
802 .map(|(_, address)| address)
803 .collect::<Vec<_>>();
804 let to_upload = to_upload
805 .into_iter()
806 .map(|(_, address, _)| address)
807 .collect::<Vec<_>>();
808
809 info!(
810 "Merkle preflight complete: {} already stored, {} need upload",
811 already_stored.len(),
812 to_upload.len()
813 );
814
815 Ok(MerkleUploadPlan {
816 already_stored,
817 to_upload,
818 to_upload_total_bytes,
819 })
820 }
821
822 async fn chunk_already_stored_for_merkle(
823 &self,
824 address: &[u8; 32],
825 data_type: u32,
826 data_size: u64,
827 ) -> Result<bool> {
828 let result = self
829 .get_store_quotes_with_fault_tolerance(address, data_size, data_type)
830 .await;
831 if let Err(e) = &result {
832 if matches!(classify_error(e), Outcome::Timeout | Outcome::NetworkError) {
833 debug!(
834 "Merkle preflight: could not determine stored status for {} ({e}); \
835 treating as not stored and queuing for upload",
836 hex::encode(address)
837 );
838 }
839 }
840 preflight_stored_status(result)
841 }
842
843 pub async fn prepare_merkle_batches_external(
864 &self,
865 addresses: &[[u8; 32]],
866 data_type: u32,
867 data_size: u64,
868 cap: usize,
869 ) -> Result<Vec<PreparedMerkleBatch>> {
870 if addresses.len() < 2 {
871 return Err(Error::Payment(
872 "Merkle batch payment requires at least 2 chunks".to_string(),
873 ));
874 }
875 let partitions = merkle_batch_partitions_with_cap(addresses, cap);
876 let total = partitions.len();
877 let mut batches = Vec::with_capacity(total);
878 for (i, partition) in partitions.into_iter().enumerate() {
879 debug!(
880 "Preparing external merkle sub-batch {}/{total} ({} chunks)",
881 i + 1,
882 partition.len()
883 );
884 batches.push(
885 self.prepare_merkle_batch_external(partition, data_type, data_size)
886 .await?,
887 );
888 }
889 Ok(batches)
890 }
891
892 pub async fn prepare_merkle_batch_external(
908 &self,
909 addresses: &[[u8; 32]],
910 data_type: u32,
911 data_size: u64,
912 ) -> Result<PreparedMerkleBatch> {
913 if let Some(refusal) = self.corroborated_settlement_refusal() {
918 return Err(Error::ClientUpdateRequired(refusal));
919 }
920 ensure_single_merkle_tree_batch(addresses.len())?;
921
922 let chunk_count = addresses.len();
923 let xornames: Vec<XorName> = addresses.iter().map(|a| XorName(*a)).collect();
924
925 debug!("Building merkle tree for {chunk_count} chunks");
926
927 let tree = MerkleTree::from_xornames(xornames)
929 .map_err(|e| Error::Payment(format!("Failed to build merkle tree: {e}")))?;
930
931 let depth = tree.depth();
932 let merkle_payment_timestamp = std::time::SystemTime::now()
933 .duration_since(std::time::UNIX_EPOCH)
934 .map_err(|e| Error::Payment(format!("System time error: {e}")))?
935 .as_secs();
936
937 debug!("Merkle tree: depth={depth}, leaves={chunk_count}, ts={merkle_payment_timestamp}");
938
939 let midpoint_proofs = tree
941 .reward_candidates(merkle_payment_timestamp)
942 .map_err(|e| Error::Payment(format!("Failed to generate reward candidates: {e}")))?;
943
944 debug!(
945 "Collecting candidate pools from {} midpoints (concurrent)",
946 midpoint_proofs.len()
947 );
948
949 let candidate_pools = self
955 .build_candidate_pools(
956 &midpoint_proofs,
957 data_type,
958 data_size,
959 merkle_payment_timestamp,
960 )
961 .await?;
962
963 let pool_commitments: Vec<PoolCommitment> = candidate_pools
968 .iter()
969 .map(pool_commitment_with_payment_multiplier)
970 .collect::<Result<Vec<_>>>()?;
971
972 Ok(PreparedMerkleBatch {
973 depth,
974 pool_commitments,
975 merkle_payment_timestamp,
976 candidate_pools,
977 tree,
978 addresses: addresses.to_vec(),
979 })
980 }
981
982 async fn pay_for_merkle_single_batch(
984 &self,
985 addresses: &[[u8; 32]],
986 data_type: u32,
987 data_size: u64,
988 ) -> Result<MerkleBatchPaymentResult> {
989 let wallet = self.require_wallet()?;
990 let prepared = self
991 .prepare_merkle_batch_external(addresses, data_type, data_size)
992 .await?;
993
994 info!(
995 "Submitting merkle batch payment on-chain (depth={})",
996 prepared.depth
997 );
998 let (winner_pool_hash, amount, gas_info) = wallet
999 .pay_for_merkle_tree(
1000 prepared.depth,
1001 prepared.pool_commitments.clone(),
1002 prepared.merkle_payment_timestamp,
1003 )
1004 .await
1005 .map_err(|e| Error::Payment(format!("Merkle batch payment failed: {e}")))?;
1006
1007 info!(
1008 "Merkle payment succeeded: winner pool {}",
1009 hex::encode(winner_pool_hash)
1010 );
1011
1012 let mut result = finalize_merkle_batch(prepared, winner_pool_hash)?;
1013 result.storage_cost_atto = amount.to_string();
1014 result.gas_cost_wei = gas_info.gas_cost_wei;
1015 Ok(result)
1016 }
1017
1018 async fn pay_for_merkle_multi_batch(
1020 &self,
1021 addresses: &[[u8; 32]],
1022 data_type: u32,
1023 data_size: u64,
1024 ) -> Result<MerkleBatchPaymentResult> {
1025 let sub_batches = merkle_batch_partitions(addresses);
1030 let total_sub_batches = sub_batches.len();
1031 let mut all_proofs = HashMap::with_capacity(addresses.len());
1032 let mut total_storage = Amount::ZERO;
1033 let mut total_gas: u128 = 0;
1034 let mut oldest_ts: u64 = 0;
1038
1039 for (i, chunk) in sub_batches.into_iter().enumerate() {
1040 match self
1041 .pay_for_merkle_single_batch(chunk, data_type, data_size)
1042 .await
1043 {
1044 Ok(sub_result) => {
1045 if let Ok(cost) = sub_result.storage_cost_atto.parse::<Amount>() {
1046 total_storage += cost;
1047 }
1048 total_gas = total_gas.saturating_add(sub_result.gas_cost_wei);
1049 if oldest_ts == 0
1050 || (sub_result.merkle_payment_timestamp > 0
1051 && sub_result.merkle_payment_timestamp < oldest_ts)
1052 {
1053 oldest_ts = sub_result.merkle_payment_timestamp;
1054 }
1055 all_proofs.extend(sub_result.proofs);
1056 }
1057 Err(e) => {
1058 if all_proofs.is_empty() {
1059 return Err(e);
1061 }
1062 if matches!(e, Error::ClientUpdateRequired(_)) {
1069 warn!(
1081 "Merkle sub-batch {}/{total_sub_batches}: storers refused this \
1082 client's settlement version. Returning {} proofs from \
1083 already-paid sub-batches so that spend is not stranded; the \
1084 refusal is latched and will stop the next payment.",
1085 i + 1,
1086 all_proofs.len()
1087 );
1088 }
1089 warn!(
1091 "Merkle sub-batch {}/{total_sub_batches} failed: {e}. \
1092 Returning {} proofs from prior sub-batches",
1093 i + 1,
1094 all_proofs.len()
1095 );
1096 return Ok(MerkleBatchPaymentResult {
1097 chunk_count: all_proofs.len(),
1098 proofs: all_proofs,
1099 storage_cost_atto: total_storage.to_string(),
1100 gas_cost_wei: total_gas,
1101 merkle_payment_timestamp: oldest_ts,
1102 });
1103 }
1104 }
1105 }
1106
1107 Ok(MerkleBatchPaymentResult {
1108 chunk_count: addresses.len(),
1109 proofs: all_proofs,
1110 storage_cost_atto: total_storage.to_string(),
1111 gas_cost_wei: total_gas,
1112 merkle_payment_timestamp: oldest_ts,
1113 })
1114 }
1115
1116 async fn build_candidate_pools(
1118 &self,
1119 midpoint_proofs: &[MidpointProof],
1120 data_type: u32,
1121 data_size: u64,
1122 merkle_payment_timestamp: u64,
1123 ) -> Result<Vec<MerklePaymentCandidatePool>> {
1124 let mut pool_futures = FuturesUnordered::new();
1125
1126 for midpoint_proof in midpoint_proofs {
1127 let pool_address = midpoint_proof.address();
1128 let mp = midpoint_proof.clone();
1129 pool_futures.push(async move {
1130 let candidate_nodes = self
1131 .get_merkle_candidate_pool(
1132 &pool_address.0,
1133 data_type,
1134 data_size,
1135 merkle_payment_timestamp,
1136 )
1137 .await?;
1138 Ok::<_, Error>(MerklePaymentCandidatePool {
1139 midpoint_proof: mp,
1140 candidate_nodes,
1141 })
1142 });
1143 }
1144
1145 let mut pools = Vec::with_capacity(midpoint_proofs.len());
1157 let mut verdict = PoolVerdict::default();
1158 while let Some(result) = pool_futures.next().await {
1159 match result {
1160 Ok(pool) => pools.push(pool),
1161 Err(e) => verdict.note(e),
1162 }
1163 }
1164 if let Some(e) = verdict.into_error() {
1165 return Err(e);
1166 }
1167
1168 Ok(pools)
1169 }
1170
1171 #[allow(clippy::too_many_lines)]
1173 async fn get_merkle_candidate_pool(
1174 &self,
1175 address: &[u8; 32],
1176 data_type: u32,
1177 data_size: u64,
1178 merkle_payment_timestamp: u64,
1179 ) -> Result<[MerklePaymentCandidateNode; CANDIDATES_PER_POOL]> {
1180 let node = self.network().node();
1181 let timeout = Duration::from_secs(self.config().quote_timeout_secs);
1182
1183 let query_count = CANDIDATES_PER_POOL * 2;
1185 let mut remote_peers = self
1186 .network()
1187 .find_closest_peers(address, query_count)
1188 .await?;
1189
1190 if remote_peers.len() < CANDIDATES_PER_POOL {
1194 let connected = self.network().connected_peers().await;
1195 for peer in connected {
1196 if !remote_peers.iter().any(|(id, _)| *id == peer) {
1197 remote_peers.push((peer, vec![]));
1198 }
1199 }
1200 }
1201
1202 if remote_peers.len() < CANDIDATES_PER_POOL {
1203 return Err(Error::InsufficientPeers(format!(
1204 "Found {} peers, need {CANDIDATES_PER_POOL} for merkle candidate pool. \
1205 Use --no-merkle or a larger network.",
1206 remote_peers.len()
1207 )));
1208 }
1209
1210 let mut candidate_futures = FuturesUnordered::new();
1211
1212 let unversioned_peers = self.unversioned_quote_peers();
1213 let versioned_capable = self.versioned_quote_capable_handle();
1214
1215 for (peer_id, peer_addrs) in &remote_peers {
1216 let request_id = self.next_request_id();
1217 let known_legacy = !versioned_capable
1229 .lock()
1230 .is_ok_and(|peers| peers.contains(peer_id))
1231 && unversioned_peers
1232 .lock()
1233 .is_ok_and(|peers| peers.contains(peer_id));
1234
1235 let legacy_request = MerkleCandidateQuoteRequest {
1236 address: *address,
1237 data_type,
1238 data_size,
1239 merkle_payment_timestamp,
1240 };
1241
1242 let message = ChunkMessage {
1245 request_id,
1246 body: if known_legacy {
1247 ChunkMessageBody::MerkleCandidateQuoteRequest(legacy_request.clone())
1248 } else {
1249 ChunkMessageBody::MerkleCandidateQuoteRequestV2(
1250 MerkleCandidateQuoteRequestV2::new(
1251 *address,
1252 data_size,
1253 merkle_payment_timestamp,
1254 ),
1255 )
1256 },
1257 };
1258
1259 let message_bytes = match message.encode() {
1260 Ok(bytes) => bytes,
1261 Err(e) => {
1262 warn!("Failed to encode merkle candidate request for {peer_id}: {e}");
1263 continue;
1264 }
1265 };
1266
1267 let legacy_request_id = self.next_request_id();
1276 let legacy_message_bytes = match (ChunkMessage {
1277 request_id: legacy_request_id,
1278 body: ChunkMessageBody::MerkleCandidateQuoteRequest(legacy_request),
1279 })
1280 .encode()
1281 {
1282 Ok(bytes) => bytes,
1283 Err(e) => {
1284 warn!("Failed to encode legacy merkle candidate request for {peer_id}: {e}");
1285 continue;
1286 }
1287 };
1288
1289 let peer_id_clone = *peer_id;
1290 let addrs_clone = peer_addrs.clone();
1291 let node_clone = node.clone();
1292 let peers_handle = Arc::clone(&unversioned_peers);
1293 let capable_handle = Arc::clone(&versioned_capable);
1294
1295 let fut = async move {
1296 let attempt_timeout = if known_legacy {
1301 timeout
1302 } else {
1303 timeout.min(crate::data::client::VERSIONED_QUOTE_PROBE_CEILING)
1304 };
1305
1306 let result = send_and_await_chunk_response(
1307 &node_clone,
1308 &peer_id_clone,
1309 message_bytes,
1310 request_id,
1311 attempt_timeout,
1312 &addrs_clone,
1313 |body| map_merkle_candidate_response(peer_id_clone, body),
1314 |e| {
1315 Error::Network(format!(
1316 "Failed to send merkle candidate request to {peer_id_clone}: {e}"
1317 ))
1318 },
1319 || {
1320 Error::Timeout(format!(
1321 "Timeout waiting for merkle candidate from {peer_id_clone}"
1322 ))
1323 },
1324 )
1325 .await;
1326
1327 let answered = match &result {
1333 Ok(_) => true,
1334 Err(e) => !is_version_unaware(e),
1335 };
1336 if !known_legacy && answered {
1337 if let Ok(mut peers) = capable_handle.lock() {
1338 peers.insert(peer_id_clone);
1339 }
1340 }
1341
1342 let result = match result {
1348 Err(ref e) if is_version_unaware(e) && !known_legacy => {
1349 let ever_answered = capable_handle
1356 .lock()
1357 .is_ok_and(|peers| peers.contains(&peer_id_clone));
1358 if matches!(e, Error::Timeout(_)) && !ever_answered {
1359 if let Ok(mut peers) = peers_handle.lock() {
1360 peers.insert(peer_id_clone);
1361 }
1362 }
1363 debug!(
1364 "Peer {peer_id_clone} did not answer a versioned merkle quote; \
1365 retrying in the legacy shape"
1366 );
1367 send_and_await_chunk_response(
1368 &node_clone,
1369 &peer_id_clone,
1370 legacy_message_bytes,
1371 legacy_request_id,
1372 timeout,
1373 &addrs_clone,
1374 |body| map_merkle_candidate_response(peer_id_clone, body),
1375 |e| {
1376 Error::Network(format!(
1377 "Failed to send merkle candidate request to {peer_id_clone}: {e}"
1378 ))
1379 },
1380 || {
1381 Error::Timeout(format!(
1382 "Timeout waiting for merkle candidate from {peer_id_clone}"
1383 ))
1384 },
1385 )
1386 .await
1387 }
1388 other => other,
1389 };
1390
1391 (peer_id_clone, result)
1392 };
1393
1394 candidate_futures.push(fut);
1395 }
1396
1397 self.collect_validated_candidates(&mut candidate_futures, address, merkle_payment_timestamp)
1398 .await
1399 }
1400
1401 async fn collect_validated_candidates(
1412 &self,
1413 futures: &mut FuturesUnordered<
1414 impl std::future::Future<
1415 Output = (
1416 PeerId,
1417 std::result::Result<(MerklePaymentCandidateNode, Option<Vec<u8>>), Error>,
1418 ),
1419 >,
1420 >,
1421 target_address: &[u8; 32],
1422 merkle_payment_timestamp: u64,
1423 ) -> Result<[MerklePaymentCandidateNode; CANDIDATES_PER_POOL]> {
1424 let mut valid: Vec<(PeerId, MerklePaymentCandidateNode)> = Vec::new();
1425 let mut failures: Vec<String> = Vec::new();
1426
1427 while let Some((peer_id, result)) = futures.next().await {
1428 match result {
1429 Ok((candidate, commitment)) => {
1430 if !verify_merkle_candidate_signature(&candidate) {
1431 warn!("Invalid ML-DSA-65 signature from merkle candidate {peer_id}");
1432 failures.push(format!("{peer_id}: invalid signature"));
1433 continue;
1434 }
1435 if candidate.merkle_payment_timestamp != merkle_payment_timestamp {
1436 warn!("Timestamp mismatch from merkle candidate {peer_id}");
1437 failures.push(format!("{peer_id}: timestamp mismatch"));
1438 continue;
1439 }
1440 let candidate_peer = PeerId::from_bytes(compute_address(&candidate.pub_key));
1445 if candidate_peer != peer_id {
1446 warn!(
1447 "Dropping merkle candidate {peer_id} — pub_key derives {candidate_peer}, \
1448 not the responding peer"
1449 );
1450 failures.push(format!("{peer_id}: candidate pub_key/peer mismatch"));
1451 continue;
1452 }
1453 if let Err(detail) =
1462 merkle_candidate_binding_is_valid(&candidate_peer, &candidate, &commitment)
1463 {
1464 warn!("Dropping merkle candidate {peer_id} — ADR-0004 binding invalid: {detail}");
1465 failures.push(format!("{peer_id}: bad commitment binding ({detail})"));
1466 continue;
1467 }
1468 valid.push((candidate_peer, candidate));
1469 }
1470 Err(e @ Error::ClientUpdateRequired(_)) => {
1479 if let Some(corroborated) =
1480 self.note_settlement_refusal(peer_id, &e.to_string())
1481 {
1482 let corroborators = self.settlement_refusals().corroborating_peers();
1487 warn!(
1488 "Settlement refusal corroborated by {} distinct peers [{}]; aborting before payment",
1489 corroborators.len(),
1490 corroborators.join(", ")
1491 );
1492 return Err(Error::ClientUpdateRequired(corroborated));
1493 }
1494 warn!("Merkle candidate {peer_id} refused this client's settlement version; awaiting corroboration");
1495 failures.push(format!("{peer_id}: {e}"));
1496 }
1497 Err(e) => {
1498 debug!("Failed to get merkle candidate from {peer_id}: {e}");
1499 failures.push(format!("{peer_id}: {e}"));
1500 }
1501 }
1502 }
1503
1504 if valid.len() < CANDIDATES_PER_POOL {
1505 return Err(Error::InsufficientPeers(format!(
1506 "Got {} merkle candidates, need {CANDIDATES_PER_POOL}. Failures: [{}]",
1507 valid.len(),
1508 failures.join("; ")
1509 )));
1510 }
1511
1512 let target_peer = PeerId::from_bytes(*target_address);
1513 valid.sort_by_key(|(peer_id, _)| peer_id.xor_distance(&target_peer));
1514
1515 let candidates: Vec<MerklePaymentCandidateNode> = valid
1516 .into_iter()
1517 .take(CANDIDATES_PER_POOL)
1518 .map(|(_, candidate)| candidate)
1519 .collect();
1520
1521 let array: [MerklePaymentCandidateNode; CANDIDATES_PER_POOL] =
1522 candidates.try_into().map_err(|_| {
1523 Error::Payment("Failed to convert candidates to fixed array".to_string())
1524 })?;
1525 Ok(array)
1526 }
1527
1528 pub(crate) async fn merkle_upload_chunks(
1549 &self,
1550 chunk_contents: Vec<Bytes>,
1551 addresses: Vec<[u8; 32]>,
1552 batch_result: &MerkleBatchPaymentResult,
1553 progress: Option<&mpsc::Sender<UploadEvent>>,
1554 stored_offset: usize,
1555 total_chunks: usize,
1556 ) -> Result<MerkleStoreOutcome> {
1557 let store_limiter = self.controller().store.clone();
1558 let batch_size = chunk_contents.len();
1561 if batch_size != addresses.len() {
1562 return Err(Error::InvalidData(format!(
1563 "merkle upload has {batch_size} chunk contents but {} addresses",
1564 addresses.len()
1565 )));
1566 }
1567 let cap = || store_limiter.current().min(batch_size.max(1));
1571
1572 let bodies: std::collections::HashMap<[u8; 32], Bytes> =
1577 addresses.iter().copied().zip(chunk_contents).collect();
1578 let addrs = addresses;
1579
1580 let store_one = |addr: [u8; 32]| {
1585 let limiter = store_limiter.clone();
1586 let content = bodies.get(&addr).cloned();
1587 let proof_bytes = batch_result.proofs.get(&addr).cloned();
1588 async move {
1589 let started = std::time::Instant::now();
1590 let content = content.ok_or_else(|| {
1591 Error::InvalidData(format!("missing chunk body for {}", hex::encode(addr)))
1592 })?;
1593 let proof = proof_bytes.ok_or_else(|| {
1594 Error::Payment(format!(
1595 "Missing merkle proof for chunk {}",
1596 hex::encode(addr)
1597 ))
1598 })?;
1599 let peers = self.put_target_peers(&addr).await?;
1600 observe_op(
1601 &limiter,
1602 || async move { self.chunk_put_to_close_group(content, proof, &peers).await },
1603 classify_error,
1604 )
1605 .await
1606 .map(|_| started)
1607 }
1608 };
1609
1610 let outcome = merkle_store_with_retry(
1611 addrs,
1612 cap,
1613 MERKLE_STORE_MAX_ATTEMPTS,
1614 MERKLE_RETRY_BACKOFF,
1615 progress,
1616 stored_offset,
1617 total_chunks,
1618 store_one,
1619 )
1620 .await?;
1621
1622 if let Some(e) = outcome.fatal {
1630 return Err(e);
1631 }
1632 Ok(outcome)
1633 }
1634}
1635
1636pub(crate) const MERKLE_STORE_MAX_ATTEMPTS: usize = 4;
1648
1649pub(crate) const MERKLE_RETRY_BACKOFF: Duration = Duration::from_secs(30);
1655
1656const MERKLE_RETRY_JITTER: f64 = 0.1;
1659
1660#[derive(Debug, Default)]
1663pub(crate) struct MerkleStoreOutcome {
1664 pub stored: usize,
1667 pub stored_addresses: Vec<[u8; 32]>,
1674 pub failed: usize,
1676 pub failed_addresses: Vec<([u8; 32], String)>,
1681 pub fatal: Option<Error>,
1688 pub stats: crate::data::client::batch::WaveAggregateStats,
1690}
1691
1692#[allow(clippy::too_many_arguments)]
1718pub(crate) async fn merkle_store_with_retry<F, Fut, C>(
1719 addrs: Vec<[u8; 32]>,
1720 cap: C,
1721 max_attempts: usize,
1722 backoff: Duration,
1723 progress: Option<&mpsc::Sender<UploadEvent>>,
1724 stored_offset: usize,
1725 total: usize,
1726 store_one: F,
1727) -> Result<MerkleStoreOutcome>
1728where
1729 F: Fn([u8; 32]) -> Fut,
1730 Fut: std::future::Future<Output = Result<std::time::Instant>>,
1731 C: Fn() -> usize,
1732{
1733 let attempts = max_attempts.max(1);
1734 let mut outcome = MerkleStoreOutcome {
1735 stored: stored_offset,
1736 ..MerkleStoreOutcome::default()
1737 };
1738 let mut pending = addrs;
1739
1740 for attempt in 0..attempts {
1741 let mut next_failed: Vec<([u8; 32], String)> = Vec::new();
1747
1748 let mut pending_iter = pending.into_iter();
1753 let mut in_flight = FuturesUnordered::new();
1754 loop {
1755 let slots = cap().max(1);
1756 while in_flight.len() < slots {
1757 match pending_iter.next() {
1758 Some(addr) => {
1759 let fut = store_one(addr);
1760 in_flight.push(async move { (addr, fut.await) });
1761 }
1762 None => break,
1763 }
1764 }
1765 let Some((addr, result)) = in_flight.next().await else {
1766 break;
1767 };
1768 outcome.stats.chunk_attempts_total =
1769 outcome.stats.chunk_attempts_total.saturating_add(1);
1770 match result {
1771 Ok(started) => {
1772 let duration_ms =
1773 u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX);
1774 outcome.stats.store_durations_ms.push(duration_ms);
1775 let idx = attempt.min(outcome.stats.retries_histogram.len().saturating_sub(1));
1776 outcome.stats.retries_histogram[idx] =
1777 outcome.stats.retries_histogram[idx].saturating_add(1);
1778 outcome.stored += 1;
1779 outcome.stored_addresses.push(addr);
1780 if let Some(tx) = progress {
1781 let _ = tx.try_send(UploadEvent::ChunkStored {
1782 stored: outcome.stored,
1783 total,
1784 });
1785 }
1786 }
1787 Err(
1794 e @ (Error::InsufficientPeers(_)
1795 | Error::CloseGroupShortfall(_)
1796 | Error::RemotePut { .. }),
1797 ) => {
1798 next_failed.push((addr, e.to_string()));
1799 }
1800 Err(e) => {
1801 next_failed.push((addr, e.to_string()));
1809 outcome.fatal = Some(e);
1810 break;
1811 }
1812 }
1813 }
1814
1815 if outcome.fatal.is_some() {
1816 outcome.failed = next_failed.len();
1817 outcome.failed_addresses = next_failed;
1818 return Ok(outcome);
1819 }
1820
1821 if next_failed.is_empty() {
1822 break;
1823 }
1824
1825 if attempt + 1 < attempts {
1826 warn!(
1827 failed = next_failed.len(),
1828 attempt = attempt + 1,
1829 "merkle chunks short of quorum, retrying after backoff"
1830 );
1831 pending = next_failed.into_iter().map(|(addr, _msg)| addr).collect();
1832 if backoff > Duration::ZERO {
1833 let wait = {
1838 let mut rng = rand::thread_rng();
1839 let factor = 1.0 + rng.gen_range(-MERKLE_RETRY_JITTER..=MERKLE_RETRY_JITTER);
1840 backoff.mul_f64(factor)
1841 };
1842 tokio::time::sleep(wait).await;
1843 }
1844 } else {
1845 outcome.failed = next_failed.len();
1846 outcome.failed_addresses = next_failed;
1847 break;
1848 }
1849 }
1850
1851 Ok(outcome)
1852}
1853
1854pub(crate) const DEFERRED_ROUND_DELAYS_SECS: [u64; 3] = [0, 15, 45];
1863
1864pub(crate) fn deferred_round_histogram_slot(round: usize, hist_len: usize) -> usize {
1871 (round + 1).min(hist_len.saturating_sub(1))
1872}
1873
1874#[derive(Debug, Default)]
1876pub(crate) struct DeferredRetryOutcome {
1877 pub stored: usize,
1881 pub stored_addresses: Vec<[u8; 32]>,
1884 pub failed: usize,
1886 pub failed_addresses: Vec<([u8; 32], String)>,
1890 pub fatal: Option<String>,
1894 pub stats: crate::data::client::batch::WaveAggregateStats,
1897}
1898
1899#[allow(clippy::too_many_arguments)]
1917pub(crate) async fn merkle_deferred_retry<CF, SF, Fut>(
1918 deferred: Vec<([u8; 32], String)>,
1919 round_delays_secs: &[u64],
1920 concurrency_for: CF,
1921 progress: Option<&mpsc::Sender<UploadEvent>>,
1922 stored_offset: usize,
1923 total: usize,
1924 store_one: SF,
1925) -> Result<DeferredRetryOutcome>
1926where
1927 CF: Fn(usize) -> usize,
1928 SF: Fn([u8; 32]) -> Fut,
1929 Fut: std::future::Future<Output = Result<std::time::Instant>>,
1930{
1931 let mut outcome = DeferredRetryOutcome {
1932 stored: stored_offset,
1933 ..DeferredRetryOutcome::default()
1934 };
1935 let mut remaining = deferred;
1936 let rounds = round_delays_secs.len();
1937
1938 for (round, &delay_secs) in round_delays_secs.iter().enumerate() {
1939 if remaining.is_empty() {
1940 break;
1941 }
1942 if delay_secs > 0 {
1943 tokio::time::sleep(Duration::from_secs(delay_secs)).await;
1944 }
1945 info!(
1946 "Deferred merkle retry round {}/{}: {} chunk(s) short of quorum",
1947 round + 1,
1948 rounds,
1949 remaining.len(),
1950 );
1951
1952 let slot = deferred_round_histogram_slot(round, outcome.stats.retries_histogram.len());
1956 let round_addrs: Vec<[u8; 32]> = std::mem::take(&mut remaining)
1957 .into_iter()
1958 .map(|(addr, _msg)| addr)
1959 .collect();
1960 let round_len = round_addrs.len();
1961 let cap = || concurrency_for(round_len);
1964
1965 let round_outcome = merkle_store_with_retry(
1966 round_addrs,
1967 cap,
1968 1,
1969 Duration::ZERO,
1970 progress,
1971 outcome.stored,
1972 total,
1973 &store_one,
1974 )
1975 .await?;
1976
1977 outcome.stored = round_outcome.stored;
1978 outcome
1979 .stored_addresses
1980 .extend(round_outcome.stored_addresses);
1981
1982 outcome.stats.chunk_attempts_total = outcome
1984 .stats
1985 .chunk_attempts_total
1986 .saturating_add(round_outcome.stats.chunk_attempts_total);
1987 outcome
1988 .stats
1989 .store_durations_ms
1990 .extend(round_outcome.stats.store_durations_ms);
1991 let landed: usize = round_outcome.stats.retries_histogram.iter().sum();
1992 outcome.stats.retries_histogram[slot] =
1993 outcome.stats.retries_histogram[slot].saturating_add(landed);
1994
1995 if let Some(fatal) = round_outcome.fatal {
1996 outcome.fatal = Some(fatal.to_string());
2001 outcome.failed = round_outcome.failed_addresses.len();
2002 outcome.failed_addresses = round_outcome.failed_addresses;
2003 return Ok(outcome);
2004 }
2005
2006 remaining = round_outcome.failed_addresses;
2008 }
2009
2010 outcome.failed = remaining.len();
2011 outcome.failed_addresses = remaining;
2012 Ok(outcome)
2013}
2014
2015pub fn finalize_merkle_batch(
2020 prepared: PreparedMerkleBatch,
2021 winner_pool_hash: [u8; 32],
2022) -> Result<MerkleBatchPaymentResult> {
2023 let chunk_count = prepared.addresses.len();
2024 let xornames: Vec<XorName> = prepared.addresses.iter().map(|a| XorName(*a)).collect();
2025
2026 let winner_pool = prepared
2028 .candidate_pools
2029 .iter()
2030 .find(|pool| pool.hash() == winner_pool_hash)
2031 .ok_or_else(|| {
2032 Error::Payment(format!(
2033 "Winner pool {} not found in candidate pools",
2034 hex::encode(winner_pool_hash)
2035 ))
2036 })?;
2037
2038 info!("Generating merkle proofs for {chunk_count} chunks");
2049 let mut proofs = HashMap::with_capacity(chunk_count);
2050
2051 for (i, xorname) in xornames.iter().enumerate() {
2052 let address_proof = prepared
2053 .tree
2054 .generate_address_proof(i, *xorname)
2055 .map_err(|e| {
2056 Error::Payment(format!(
2057 "Failed to generate address proof for chunk {i}: {e}"
2058 ))
2059 })?;
2060
2061 let merkle_proof = MerklePaymentProof::new(*xorname, address_proof, winner_pool.clone());
2062
2063 let tagged_bytes = serialize_merkle_proof(&merkle_proof)
2064 .map_err(|e| Error::Serialization(format!("Failed to serialize merkle proof: {e}")))?;
2065
2066 proofs.insert(prepared.addresses[i], tagged_bytes);
2067 }
2068
2069 info!("Merkle batch payment complete: {chunk_count} proofs generated");
2070
2071 Ok(MerkleBatchPaymentResult {
2072 proofs,
2073 chunk_count,
2074 storage_cost_atto: "0".to_string(),
2075 gas_cost_wei: 0,
2076 merkle_payment_timestamp: prepared.merkle_payment_timestamp,
2077 })
2078}
2079
2080#[cfg(test)]
2082mod send_assertions {
2083 use super::*;
2084 use crate::data::client::Client;
2085
2086 fn _assert_send<T: Send>(_: &T) {}
2087
2088 #[allow(
2089 dead_code,
2090 unreachable_code,
2091 unused_variables,
2092 clippy::diverging_sub_expression
2093 )]
2094 async fn _merkle_upload_chunks_is_send(client: &Client) {
2095 let batch_result: MerkleBatchPaymentResult = todo!();
2096 let fut = client.merkle_upload_chunks(Vec::new(), Vec::new(), &batch_result, None, 0, 0);
2097 _assert_send(&fut);
2098 }
2099}
2100
2101#[cfg(test)]
2104#[allow(clippy::unwrap_used, clippy::expect_used)]
2105pub(crate) mod test_support {
2106 use super::*;
2107 use ant_protocol::evm::RewardsAddress;
2108
2109 pub(crate) fn make_test_addresses(count: usize) -> Vec<[u8; 32]> {
2110 (0..count)
2111 .map(|i| {
2112 let xn = XorName::from_content(&i.to_le_bytes());
2113 xn.0
2114 })
2115 .collect()
2116 }
2117
2118 pub(crate) fn make_dummy_candidate_nodes(
2119 timestamp: u64,
2120 ) -> [MerklePaymentCandidateNode; CANDIDATES_PER_POOL] {
2121 std::array::from_fn(|i| MerklePaymentCandidateNode {
2122 pub_key: vec![i as u8; 32],
2123 price: Amount::from(1024u64),
2124 reward_address: RewardsAddress::new([i as u8; 20]),
2125 merkle_payment_timestamp: timestamp,
2126 signature: vec![i as u8; 64],
2127 committed_key_count: 0,
2128 commitment_pin: None,
2129 })
2130 }
2131
2132 pub(crate) fn make_prepared_merkle_batch(count: usize) -> PreparedMerkleBatch {
2133 let addrs = make_test_addresses(count);
2134 let xornames: Vec<XorName> = addrs.iter().map(|a| XorName(*a)).collect();
2135 let tree = MerkleTree::from_xornames(xornames).unwrap();
2136
2137 let timestamp = std::time::SystemTime::now()
2138 .duration_since(std::time::UNIX_EPOCH)
2139 .unwrap()
2140 .as_secs();
2141
2142 let midpoints = tree.reward_candidates(timestamp).unwrap();
2143
2144 let candidate_pools: Vec<MerklePaymentCandidatePool> = midpoints
2145 .into_iter()
2146 .map(|mp| MerklePaymentCandidatePool {
2147 midpoint_proof: mp,
2148 candidate_nodes: make_dummy_candidate_nodes(timestamp),
2149 })
2150 .collect();
2151
2152 let pool_commitments = candidate_pools
2153 .iter()
2154 .map(pool_commitment_with_payment_multiplier)
2155 .collect::<Result<Vec<_>>>()
2156 .unwrap();
2157
2158 PreparedMerkleBatch {
2159 depth: tree.depth(),
2160 pool_commitments,
2161 merkle_payment_timestamp: timestamp,
2162 candidate_pools,
2163 tree,
2164 addresses: addrs,
2165 }
2166 }
2167
2168 pub(crate) fn winner_hash_for(batch: &PreparedMerkleBatch) -> [u8; 32] {
2173 batch.candidate_pools[0].hash()
2174 }
2175}
2176
2177#[cfg(test)]
2178#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
2179mod tests {
2180 use super::test_support::*;
2181 use super::*;
2182 use ant_protocol::evm::{Amount, MerkleTree, RewardsAddress, CANDIDATES_PER_POOL};
2183
2184 #[test]
2189 fn test_auto_below_threshold() {
2190 assert!(!should_use_merkle(1, PaymentMode::Auto));
2191 assert!(!should_use_merkle(10, PaymentMode::Auto));
2192 assert!(!should_use_merkle(63, PaymentMode::Auto));
2193 }
2194
2195 #[test]
2196 fn test_auto_at_and_above_threshold() {
2197 assert!(should_use_merkle(64, PaymentMode::Auto));
2198 assert!(should_use_merkle(65, PaymentMode::Auto));
2199 assert!(should_use_merkle(1000, PaymentMode::Auto));
2200 }
2201
2202 #[test]
2203 fn test_merkle_mode_forces_at_2() {
2204 assert!(!should_use_merkle(1, PaymentMode::Merkle));
2205 assert!(should_use_merkle(2, PaymentMode::Merkle));
2206 assert!(should_use_merkle(3, PaymentMode::Merkle));
2207 }
2208
2209 #[test]
2210 fn test_single_mode_always_false() {
2211 assert!(!should_use_merkle(0, PaymentMode::Single));
2212 assert!(!should_use_merkle(64, PaymentMode::Single));
2213 assert!(!should_use_merkle(1000, PaymentMode::Single));
2214 }
2215
2216 #[test]
2217 fn test_default_mode_is_auto() {
2218 assert_eq!(PaymentMode::default(), PaymentMode::Auto);
2219 }
2220
2221 #[test]
2222 fn test_threshold_value() {
2223 assert_eq!(DEFAULT_MERKLE_THRESHOLD, 64);
2224 }
2225
2226 #[test]
2231 fn test_preflight_quotes_gathered_means_not_stored() {
2232 assert!(matches!(preflight_stored_status(Ok(())), Ok(false)));
2233 }
2234
2235 #[test]
2236 fn test_preflight_already_stored_is_stored() {
2237 let r: Result<()> = Err(Error::AlreadyStored);
2238 assert!(matches!(preflight_stored_status(r), Ok(true)));
2239 }
2240
2241 #[test]
2245 fn test_preflight_transient_quote_failure_does_not_abort() {
2246 let insufficient: Result<()> =
2248 Err(Error::InsufficientPeers("Got 5 quotes, need 7".to_string()));
2249 assert!(
2250 matches!(preflight_stored_status(insufficient), Ok(false)),
2251 "insufficient-peers during preflight must degrade to not-stored, not error"
2252 );
2253
2254 let timeout: Result<()> = Err(Error::Timeout("Timeout waiting for quote".to_string()));
2255 assert!(matches!(preflight_stored_status(timeout), Ok(false)));
2256
2257 let network: Result<()> = Err(Error::Network("connection reset".to_string()));
2258 assert!(matches!(preflight_stored_status(network), Ok(false)));
2259 }
2260
2261 #[test]
2264 fn test_preflight_application_error_propagates() {
2265 let payment: Result<()> = Err(Error::Payment("bad payment".to_string()));
2266 assert!(matches!(
2267 preflight_stored_status(payment),
2268 Err(Error::Payment(_))
2269 ));
2270 }
2271
2272 #[test]
2273 fn chunk_contents_for_upload_addresses_preserves_requested_order() {
2274 let first = Bytes::from_static(b"first");
2275 let second = Bytes::from_static(b"second");
2276 let first_addr = compute_address(&first);
2277 let second_addr = compute_address(&second);
2278
2279 let selected = chunk_contents_for_upload_addresses(
2280 vec![first.clone(), second.clone()],
2281 &[second_addr, first_addr],
2282 )
2283 .unwrap();
2284
2285 assert_eq!(selected, vec![second, first]);
2286 }
2287
2288 #[test]
2289 fn chunk_contents_for_upload_addresses_preserves_duplicate_requests() {
2290 let repeated = Bytes::from_static(b"same-content");
2291 let other = Bytes::from_static(b"other-content");
2292 let repeated_addr = compute_address(&repeated);
2293
2294 let selected = chunk_contents_for_upload_addresses(
2295 vec![repeated.clone(), other, repeated.clone()],
2296 &[repeated_addr, repeated_addr],
2297 )
2298 .unwrap();
2299
2300 assert_eq!(selected, vec![repeated.clone(), repeated]);
2301 }
2302
2303 #[test]
2304 fn chunk_contents_for_upload_addresses_ignores_unrequested_duplicates() {
2305 let requested = Bytes::from_static(b"requested-content");
2306 let unrequested = Bytes::from_static(b"unrequested-content");
2307 let requested_addr = compute_address(&requested);
2308
2309 let selected = chunk_contents_for_upload_addresses(
2310 vec![
2311 unrequested.clone(),
2312 requested.clone(),
2313 unrequested.clone(),
2314 unrequested,
2315 ],
2316 &[requested_addr],
2317 )
2318 .unwrap();
2319
2320 assert_eq!(selected, vec![requested]);
2321 }
2322
2323 #[test]
2324 fn chunk_contents_for_upload_addresses_errors_for_missing_content() {
2325 let present = Bytes::from_static(b"present-content");
2326 let missing = Bytes::from_static(b"missing-content");
2327 let missing_addr = compute_address(&missing);
2328
2329 let result = chunk_contents_for_upload_addresses(vec![present], &[missing_addr]);
2330
2331 assert!(matches!(result, Err(Error::InvalidData(_))));
2332 }
2333
2334 #[test]
2339 fn test_tree_depth_for_known_sizes() {
2340 let cases = [(2, 1), (4, 2), (16, 4), (100, 7), (256, 8)];
2341 for (count, expected_depth) in cases {
2342 let addrs = make_test_addresses(count);
2343 let xornames: Vec<XorName> = addrs.iter().map(|a| XorName(*a)).collect();
2344 let tree = MerkleTree::from_xornames(xornames).unwrap();
2345 assert_eq!(
2346 tree.depth(),
2347 expected_depth,
2348 "depth mismatch for {count} leaves"
2349 );
2350 }
2351 }
2352
2353 #[test]
2354 fn test_proof_generation_and_verification_for_all_leaves() {
2355 let addrs = make_test_addresses(16);
2356 let xornames: Vec<XorName> = addrs.iter().map(|a| XorName(*a)).collect();
2357 let tree = MerkleTree::from_xornames(xornames.clone()).unwrap();
2358
2359 for (i, xn) in xornames.iter().enumerate() {
2360 let proof = tree.generate_address_proof(i, *xn).unwrap();
2361 assert!(proof.verify(), "proof for leaf {i} should verify");
2362 assert_eq!(proof.depth(), tree.depth() as usize);
2363 }
2364 }
2365
2366 #[test]
2367 fn test_proof_fails_for_wrong_address() {
2368 let addrs = make_test_addresses(8);
2369 let xornames: Vec<XorName> = addrs.iter().map(|a| XorName(*a)).collect();
2370 let tree = MerkleTree::from_xornames(xornames).unwrap();
2371
2372 let wrong = XorName::from_content(b"wrong");
2373 let proof = tree.generate_address_proof(0, wrong).unwrap();
2374 assert!(!proof.verify(), "proof with wrong address should fail");
2375 }
2376
2377 #[test]
2378 fn test_tree_too_few_leaves() {
2379 let xornames = vec![XorName::from_content(b"only_one")];
2380 let result = MerkleTree::from_xornames(xornames);
2381 assert!(result.is_err());
2382 }
2383
2384 #[test]
2385 fn test_tree_at_max_leaves() {
2386 let addrs = make_test_addresses(MAX_LEAVES);
2387 let xornames: Vec<XorName> = addrs.iter().map(|a| XorName(*a)).collect();
2388 let tree = MerkleTree::from_xornames(xornames).unwrap();
2389 assert_eq!(tree.leaf_count(), MAX_LEAVES);
2390 }
2391
2392 #[test]
2397 fn test_merkle_proof_serialize_deserialize_roundtrip() {
2398 use ant_protocol::evm::{Amount, MerklePaymentCandidateNode, RewardsAddress};
2399 use ant_protocol::payment::{deserialize_merkle_proof, serialize_merkle_proof};
2400
2401 let addrs = make_test_addresses(4);
2402 let xornames: Vec<XorName> = addrs.iter().map(|a| XorName(*a)).collect();
2403 let tree = MerkleTree::from_xornames(xornames.clone()).unwrap();
2404
2405 let timestamp = std::time::SystemTime::now()
2406 .duration_since(std::time::UNIX_EPOCH)
2407 .unwrap()
2408 .as_secs();
2409
2410 let candidates = tree.reward_candidates(timestamp).unwrap();
2411 let midpoint = candidates.first().unwrap().clone();
2412
2413 #[allow(clippy::cast_possible_truncation)]
2415 let candidate_nodes: [MerklePaymentCandidateNode; CANDIDATES_PER_POOL] =
2416 std::array::from_fn(|i| MerklePaymentCandidateNode {
2417 pub_key: vec![i as u8; 32],
2418 price: Amount::from(1024u64),
2419 reward_address: RewardsAddress::new([i as u8; 20]),
2420 merkle_payment_timestamp: timestamp,
2421 signature: vec![i as u8; 64],
2422 committed_key_count: 0,
2423 commitment_pin: None,
2424 });
2425
2426 let pool = MerklePaymentCandidatePool {
2427 midpoint_proof: midpoint,
2428 candidate_nodes,
2429 };
2430
2431 let address_proof = tree.generate_address_proof(0, xornames[0]).unwrap();
2432 let merkle_proof = MerklePaymentProof::new(xornames[0], address_proof, pool);
2433
2434 let tagged = serialize_merkle_proof(&merkle_proof).unwrap();
2435 assert_eq!(
2436 tagged.first().copied(),
2437 Some(0x02),
2438 "tag should be PROOF_TAG_MERKLE"
2439 );
2440
2441 let deserialized = deserialize_merkle_proof(&tagged).unwrap();
2442 assert_eq!(deserialized.address, merkle_proof.address);
2443 assert_eq!(
2444 deserialized.winner_pool.candidate_nodes.len(),
2445 CANDIDATES_PER_POOL
2446 );
2447 }
2448
2449 #[test]
2454 fn test_candidate_wrong_timestamp_rejected() {
2455 let candidate = MerklePaymentCandidateNode {
2457 pub_key: vec![0u8; 32],
2458 price: ant_protocol::evm::Amount::ZERO,
2459 reward_address: ant_protocol::evm::RewardsAddress::new([0u8; 20]),
2460 merkle_payment_timestamp: 1000,
2461 signature: vec![0u8; 64],
2462 committed_key_count: 0,
2463 commitment_pin: None,
2464 };
2465
2466 assert_ne!(candidate.merkle_payment_timestamp, 2000);
2468 }
2469
2470 fn pool_with_varied_prices(timestamp: u64) -> MerklePaymentCandidatePool {
2477 let addrs = make_test_addresses(4);
2478 let xornames: Vec<XorName> = addrs.iter().map(|a| XorName(*a)).collect();
2479 let tree = MerkleTree::from_xornames(xornames).unwrap();
2480 let midpoint = tree
2481 .reward_candidates(timestamp)
2482 .unwrap()
2483 .into_iter()
2484 .next()
2485 .unwrap();
2486
2487 let candidate_nodes = std::array::from_fn(|i| MerklePaymentCandidateNode {
2488 pub_key: vec![i as u8; 32],
2489 price: Amount::from((i as u64 + 1) * 100),
2491 reward_address: RewardsAddress::new([i as u8; 20]),
2492 merkle_payment_timestamp: timestamp,
2493 signature: vec![i as u8; 64],
2494 committed_key_count: 0,
2495 commitment_pin: None,
2496 });
2497
2498 MerklePaymentCandidatePool {
2499 midpoint_proof: midpoint,
2500 candidate_nodes,
2501 }
2502 }
2503
2504 fn median16(mut amounts: Vec<Amount>) -> Amount {
2506 amounts.sort_unstable();
2507 *amounts.get(amounts.len() / 2).unwrap()
2508 }
2509
2510 #[test]
2511 fn pool_commitment_applies_payment_multiplier_to_every_candidate() {
2512 let pool = pool_with_varied_prices(1_700_000_000);
2513 let commitment = pool_commitment_with_payment_multiplier(&pool).unwrap();
2514
2515 for (candidate, signed) in commitment
2516 .candidates
2517 .iter()
2518 .zip(pool.candidate_nodes.iter())
2519 {
2520 assert_eq!(
2521 candidate.price,
2522 signed.price * Amount::from(MERKLE_PAYMENT_MULTIPLIER),
2523 "on-chain payable amount must be {MERKLE_PAYMENT_MULTIPLIER}x the quoted price"
2524 );
2525 }
2526 }
2527
2528 #[test]
2529 fn pool_commitment_multiplier_leaves_signed_prices_and_pool_hash_untouched() {
2530 let pool = pool_with_varied_prices(1_700_000_000);
2531 let before: Vec<Amount> = pool.candidate_nodes.iter().map(|c| c.price).collect();
2532
2533 let commitment = pool_commitment_with_payment_multiplier(&pool).unwrap();
2534
2535 let after: Vec<Amount> = pool.candidate_nodes.iter().map(|c| c.price).collect();
2536 assert_eq!(before, after, "signed candidate prices must not change");
2537 assert_eq!(
2538 commitment.pool_hash,
2539 pool.hash(),
2540 "pool hash is the storer's on-chain lookup key and must be \
2541 computed over the signed 1x prices"
2542 );
2543 }
2544
2545 #[test]
2554 fn merkle_settlement_per_padded_leaf_is_the_multiplied_pool_median() {
2555 let pool = pool_with_varied_prices(1_700_000_000);
2556 let commitment = pool_commitment_with_payment_multiplier(&pool).unwrap();
2557
2558 let quoted_median = median16(pool.candidate_nodes.iter().map(|c| c.price).collect());
2559 let per_chunk = median16(commitment.candidates.iter().map(|c| c.price).collect());
2560
2561 assert_eq!(quoted_median, Amount::from(900u64));
2562 assert_eq!(
2563 per_chunk,
2564 quoted_median * Amount::from(MERKLE_PAYMENT_MULTIPLIER),
2565 "merkle per-chunk settlement must equal the single-node \
2566 {MERKLE_PAYMENT_MULTIPLIER}x median, not the bare quoted price"
2567 );
2568 }
2569
2570 #[test]
2571 fn test_finalize_merkle_batch_with_valid_winner() {
2572 let prepared = make_prepared_merkle_batch(4);
2573 let winner_hash = prepared.candidate_pools[0].hash();
2574
2575 let result = finalize_merkle_batch(prepared, winner_hash);
2576 assert!(
2577 result.is_ok(),
2578 "should succeed with valid winner: {result:?}"
2579 );
2580
2581 let batch = result.unwrap();
2582 assert_eq!(batch.chunk_count, 4);
2583 assert_eq!(batch.proofs.len(), 4);
2584
2585 for proof_bytes in batch.proofs.values() {
2587 assert!(!proof_bytes.is_empty());
2588 }
2589 }
2590
2591 #[test]
2599 fn test_finalize_merkle_batch_ships_no_commitment_sidecars() {
2600 use ant_protocol::payment::deserialize_merkle_proof;
2601
2602 let mut prepared = make_prepared_merkle_batch(4);
2603 for pool in &mut prepared.candidate_pools {
2606 for candidate in &mut pool.candidate_nodes {
2607 candidate.committed_key_count = 9_000;
2608 candidate.commitment_pin = Some([7u8; 32]);
2609 }
2610 }
2611 let winner_hash = prepared.candidate_pools[0].hash();
2612
2613 let batch = finalize_merkle_batch(prepared, winner_hash).unwrap();
2614 assert_eq!(batch.proofs.len(), 4);
2615 for proof_bytes in batch.proofs.values() {
2616 let proof = deserialize_merkle_proof(proof_bytes).unwrap();
2617 assert!(
2618 proof.commitment_sidecars.is_empty(),
2619 "per-chunk merkle proofs must not ship commitment sidecars"
2620 );
2621 }
2622 }
2623
2624 #[test]
2625 fn test_finalize_merkle_batch_with_invalid_winner() {
2626 let prepared = make_prepared_merkle_batch(4);
2627 let bad_hash = [0xFF; 32];
2628
2629 let result = finalize_merkle_batch(prepared, bad_hash);
2630 assert!(result.is_err());
2631 let err = result.unwrap_err().to_string();
2632 assert!(err.contains("not found in candidate pools"), "got: {err}");
2633 }
2634
2635 #[test]
2636 fn test_finalize_merkle_batch_proofs_are_deserializable() {
2637 use ant_protocol::payment::deserialize_merkle_proof;
2638
2639 let prepared = make_prepared_merkle_batch(8);
2640 let winner_hash = prepared.candidate_pools[0].hash();
2641
2642 let batch = finalize_merkle_batch(prepared, winner_hash).unwrap();
2643
2644 for (addr, proof_bytes) in &batch.proofs {
2645 let proof = deserialize_merkle_proof(proof_bytes);
2646 assert!(
2647 proof.is_ok(),
2648 "proof for {} should deserialize: {:?}",
2649 hex::encode(addr),
2650 proof.err()
2651 );
2652 }
2653 }
2654
2655 const PARTITION_CASES: [(usize, &[usize]); 10] = [
2664 (2, &[2]),
2665 (64, &[64]),
2666 (65, &[65]),
2667 (255, &[255]),
2668 (256, &[256]),
2669 (257, &[255, 2]),
2670 (300, &[256, 44]),
2671 (512, &[256, 256]),
2672 (513, &[256, 255, 2]),
2673 (769, &[256, 256, 255, 2]),
2674 ];
2675
2676 #[test]
2677 fn merkle_batch_sizes_rebalance_singleton_remainders() {
2678 for (total, expected) in PARTITION_CASES {
2679 assert_eq!(
2680 merkle_batch_sizes(total),
2681 expected,
2682 "{total} addresses must partition as {expected:?}"
2683 );
2684 }
2685 }
2686
2687 #[test]
2691 fn merkle_batch_sizes_with_cap_partitions_and_clamps() {
2692 assert_eq!(merkle_batch_sizes_with_cap(6, 3), vec![3, 3]);
2694 assert_eq!(merkle_batch_sizes_with_cap(7, 3), vec![3, 2, 2]);
2695 assert_eq!(merkle_batch_sizes_with_cap(4, 3), vec![2, 2]);
2696 assert_eq!(merkle_batch_sizes_with_cap(5, 2), vec![3, 2]);
2698 assert_eq!(
2700 merkle_batch_sizes_with_cap(MAX_LEAVES + 1, MAX_LEAVES * 4),
2701 vec![MAX_LEAVES - 1, 2]
2702 );
2703 for total in 2..200usize {
2706 let sizes = merkle_batch_sizes_with_cap(total, 3);
2707 assert_eq!(sizes.iter().sum::<usize>(), total, "cover for {total}");
2708 assert!(
2709 sizes.iter().all(|&s| (2..=3).contains(&s)),
2710 "unpayable part for {total}: {sizes:?}"
2711 );
2712 }
2713 }
2714
2715 #[test]
2719 fn merge_merkle_batch_results_unions_proofs_and_keeps_oldest_timestamp() {
2720 let a = MerkleBatchPaymentResult {
2721 proofs: [([1u8; 32], vec![1u8])].into_iter().collect(),
2722 chunk_count: 1,
2723 storage_cost_atto: "100".into(),
2724 gas_cost_wei: 7,
2725 merkle_payment_timestamp: 2_000,
2726 };
2727 let b = MerkleBatchPaymentResult {
2728 proofs: [([2u8; 32], vec![2u8]), ([3u8; 32], vec![3u8])]
2729 .into_iter()
2730 .collect(),
2731 chunk_count: 2,
2732 storage_cost_atto: "50".into(),
2733 gas_cost_wei: 5,
2734 merkle_payment_timestamp: 1_500,
2735 };
2736 let merged = merge_merkle_batch_results(vec![a, b]);
2737 assert_eq!(merged.proofs.len(), 3);
2738 assert_eq!(merged.chunk_count, 3);
2739 assert_eq!(merged.storage_cost_atto, "150");
2740 assert_eq!(merged.gas_cost_wei, 12);
2741 assert_eq!(merged.merkle_payment_timestamp, 1_500);
2742 }
2743
2744 #[test]
2748 fn merkle_batch_sizes_are_always_buildable_trees() {
2749 for total in 2..=(4 * MAX_LEAVES + 3) {
2750 let sizes = merkle_batch_sizes(total);
2751 assert!(!sizes.is_empty(), "{total} addresses must produce batches");
2752 assert_eq!(
2753 sizes.iter().sum::<usize>(),
2754 total,
2755 "{total} addresses: partition must cover every address"
2756 );
2757 for size in sizes {
2758 assert!(
2759 (2..=MAX_LEAVES).contains(&size),
2760 "{total} addresses produced a batch of {size}, outside 2..={MAX_LEAVES}"
2761 );
2762 }
2763 }
2764 }
2765
2766 #[test]
2767 fn merkle_batch_sizes_below_two_have_no_payable_partition() {
2768 assert!(merkle_batch_sizes(0).is_empty());
2769 assert!(merkle_batch_sizes(1).is_empty());
2770 }
2771
2772 #[test]
2773 fn merkle_batch_partitions_preserve_order_and_use_each_address_once() {
2774 for (total, _) in PARTITION_CASES {
2775 let addrs = make_test_addresses(total);
2776 let partitions = merkle_batch_partitions(&addrs);
2777
2778 let flattened: Vec<[u8; 32]> = partitions.concat();
2779 assert_eq!(
2780 flattened, addrs,
2781 "{total} addresses: partitions must concatenate back to the input in order"
2782 );
2783
2784 let unique: std::collections::HashSet<[u8; 32]> = flattened.iter().copied().collect();
2785 assert_eq!(
2786 unique.len(),
2787 total,
2788 "{total} addresses: no address may be duplicated or synthesised"
2789 );
2790 }
2791 }
2792
2793 #[test]
2797 fn post_preflight_plan_of_257_partitions_into_payable_batches() {
2798 let plan = MerkleUploadPlan {
2799 already_stored: make_test_addresses(3),
2800 to_upload: make_test_addresses(257),
2801 to_upload_total_bytes: 257 * 1024,
2802 };
2803 assert_eq!(plan.to_upload.len(), 257);
2804
2805 let partitions = merkle_batch_partitions(&plan.to_upload);
2806 let sizes: Vec<usize> = partitions.iter().map(|batch| batch.len()).collect();
2807 assert_eq!(sizes, vec![255, 2]);
2808 for batch in partitions {
2809 let xornames: Vec<XorName> = batch.iter().map(|a| XorName(*a)).collect();
2810 assert!(
2811 MerkleTree::from_xornames(xornames).is_ok(),
2812 "every partition of a 257-chunk plan must build a tree"
2813 );
2814 }
2815 }
2816
2817 #[test]
2821 fn no_partition_pays_before_a_singleton_tree_failure() {
2822 for total in [257usize, 513, 769] {
2823 let addrs = make_test_addresses(total);
2824 for batch in merkle_batch_partitions(&addrs) {
2825 let xornames: Vec<XorName> = batch.iter().map(|a| XorName(*a)).collect();
2826 assert!(
2827 MerkleTree::from_xornames(xornames).is_ok(),
2828 "{total} addresses: batch of {} is unpayable",
2829 batch.len()
2830 );
2831 }
2832 }
2833 }
2834
2835 #[test]
2836 fn merkle_billable_leaves_sum_the_padded_partitions() {
2837 for (total, expected) in PARTITION_CASES {
2838 let padded: u64 = expected
2839 .iter()
2840 .map(|size| size.next_power_of_two() as u64)
2841 .sum();
2842 assert_eq!(
2843 merkle_billable_leaves(total as u64),
2844 padded,
2845 "{total} chunks must bill for the padded partition {expected:?}"
2846 );
2847 }
2848
2849 assert_eq!(merkle_billable_leaves(65), 128);
2851 assert_eq!(merkle_billable_leaves(257), 256 + 2);
2852 assert_eq!(merkle_billable_leaves(300), 256 + 64);
2853 assert_eq!(merkle_billable_leaves(0), 0);
2856 assert_eq!(merkle_billable_leaves(1), 2);
2857 }
2858
2859 #[test]
2860 fn merkle_billable_leaves_never_under_quote() {
2861 for chunks in 1..2000u64 {
2862 assert!(
2863 merkle_billable_leaves(chunks) >= chunks,
2864 "{chunks} chunks must never be billed as fewer leaves"
2865 );
2866 }
2867 }
2868
2869 #[test]
2873 fn external_preparation_refuses_more_than_one_tree_of_addresses() {
2874 assert!(ensure_single_merkle_tree_batch(2).is_ok());
2875 assert!(ensure_single_merkle_tree_batch(MAX_LEAVES).is_ok());
2876
2877 for oversized in [MAX_LEAVES + 1, 300, 513] {
2878 match ensure_single_merkle_tree_batch(oversized) {
2879 Err(Error::MerkleBatchTooLarge {
2880 addresses,
2881 max_leaves,
2882 }) => {
2883 assert_eq!(addresses, oversized);
2884 assert_eq!(max_leaves, MAX_LEAVES);
2885 }
2886 other => panic!("{oversized} addresses should be refused, got {other:?}"),
2887 }
2888 }
2889 }
2890
2891 use std::sync::{Arc, Mutex};
2896
2897 fn make_addrs(count: usize) -> Vec<[u8; 32]> {
2900 make_test_addresses(count)
2901 }
2902
2903 #[tokio::test]
2907 async fn store_with_retry_collects_failures_instead_of_aborting() {
2908 let chunks = make_addrs(6);
2909 let failing: std::collections::HashSet<[u8; 32]> = chunks.iter().take(2).copied().collect();
2910 let failing_for_closure = failing.clone();
2911
2912 let store_one = move |addr: [u8; 32]| {
2913 let fail = failing_for_closure.contains(&addr);
2914 async move {
2915 if fail {
2916 Err(Error::InsufficientPeers("test shortfall".into()))
2917 } else {
2918 Ok(std::time::Instant::now())
2919 }
2920 }
2921 };
2922
2923 let outcome =
2924 merkle_store_with_retry(chunks, || 8, 1, Duration::ZERO, None, 0, 6, store_one)
2925 .await
2926 .expect("quorum shortfalls must not abort the batch");
2927
2928 assert_eq!(outcome.stored, 4);
2929 assert_eq!(outcome.failed, 2);
2930 assert_eq!(outcome.stats.retries_histogram[0], 4);
2932 assert_eq!(outcome.stats.chunk_attempts_total, 6);
2933 }
2934
2935 #[tokio::test]
2943 async fn quorum_shortfall_survives_deferred_retries_with_exact_accounting() {
2944 let chunks = make_addrs(5);
2945 let short: std::collections::HashSet<[u8; 32]> = chunks.iter().take(2).copied().collect();
2946 let short_for_closure = short.clone();
2947 let store_one = move |addr: [u8; 32]| {
2948 let fail = short_for_closure.contains(&addr);
2949 async move {
2950 if fail {
2951 Err(Error::InsufficientPeers("still short of quorum".into()))
2952 } else {
2953 Ok(std::time::Instant::now())
2954 }
2955 }
2956 };
2957
2958 let pass = merkle_store_with_retry(
2961 chunks.clone(),
2962 || 8,
2963 1,
2964 Duration::ZERO,
2965 None,
2966 0,
2967 5,
2968 &store_one,
2969 )
2970 .await
2971 .expect("quorum shortfalls must not abort the pass");
2972 assert!(pass.fatal.is_none());
2973 assert_eq!(pass.stored, 3);
2974 assert_eq!(pass.failed, 2);
2975
2976 let dr = merkle_deferred_retry(
2979 pass.failed_addresses.clone(),
2980 &[0, 0, 0],
2981 |n: usize| n.max(1),
2982 None,
2983 pass.stored,
2984 5,
2985 &store_one,
2986 )
2987 .await
2988 .expect("deferred shortfalls must not abort");
2989
2990 assert!(dr.fatal.is_none());
2991 assert_eq!(
2992 dr.stored + dr.failed,
2993 5,
2994 "stored + failed must account for every chunk"
2995 );
2996 assert_eq!(dr.stored, 3, "paid-and-stored chunks must stay counted");
2997 assert_eq!(dr.failed, 2);
2998 let failed_set: std::collections::HashSet<[u8; 32]> =
2999 dr.failed_addresses.iter().map(|(a, _)| *a).collect();
3000 assert_eq!(
3001 failed_set, short,
3002 "failed set must be exactly the shortfall chunks"
3003 );
3004 }
3005
3006 #[tokio::test]
3012 async fn store_with_retry_rereads_cap_per_slot() {
3013 let count = 6;
3014 let chunks = make_addrs(count);
3015 let cap_calls = Arc::new(Mutex::new(0usize));
3016 let cap_calls_for_closure = cap_calls.clone();
3017 let cap = move || {
3018 *cap_calls_for_closure.lock().expect("cap counter poisoned") += 1;
3019 2
3020 };
3021 let store_one = move |_addr: [u8; 32]| async move { Ok(std::time::Instant::now()) };
3022
3023 let outcome =
3024 merkle_store_with_retry(chunks, cap, 1, Duration::ZERO, None, 0, count, store_one)
3025 .await
3026 .expect("all stores succeed");
3027
3028 assert_eq!(outcome.stored, count);
3029 let calls = *cap_calls.lock().expect("cap counter poisoned");
3030 assert!(
3031 calls >= count,
3032 "cap must be re-read per drained slot (rolling), not snapshotted once — \
3033 expected >= {count} invocations, got {calls}",
3034 );
3035 }
3036
3037 #[tokio::test]
3044 async fn store_pass_has_no_barrier() {
3045 use std::sync::atomic::{AtomicUsize, Ordering};
3046 let count = 8;
3047 let addrs = make_addrs(count);
3048 let slow = addrs[0];
3049 let fast_completed = Arc::new(AtomicUsize::new(0));
3050 let release_slow = Arc::new(tokio::sync::Notify::new());
3051
3052 let store_one = move |addr: [u8; 32]| {
3053 let fast_completed = fast_completed.clone();
3054 let release_slow = release_slow.clone();
3055 async move {
3056 if addr == slow {
3057 release_slow.notified().await;
3061 } else if fast_completed.fetch_add(1, Ordering::SeqCst) + 1 == count - 1 {
3062 release_slow.notify_one();
3063 }
3064 Ok(std::time::Instant::now())
3065 }
3066 };
3067
3068 let outcome = tokio::time::timeout(
3069 Duration::from_secs(5),
3070 merkle_store_with_retry(addrs, || 8, 1, Duration::ZERO, None, 0, count, store_one),
3071 )
3072 .await
3073 .expect("store pass must not deadlock — a slow chunk must not block the others")
3074 .expect("all stores succeed");
3075
3076 assert_eq!(outcome.stored, count);
3077 }
3078
3079 #[tokio::test]
3084 async fn store_pass_keeps_at_most_cap_in_flight() {
3085 use std::sync::atomic::{AtomicUsize, Ordering};
3086 let count = 40;
3087 let cap = 4;
3088 let addrs = make_addrs(count);
3089 let in_flight = Arc::new(AtomicUsize::new(0));
3090 let max_in_flight = Arc::new(AtomicUsize::new(0));
3091 let max_in_flight_for_closure = max_in_flight.clone();
3092
3093 let store_one = move |_addr: [u8; 32]| {
3094 let in_flight = in_flight.clone();
3095 let max_in_flight = max_in_flight_for_closure.clone();
3096 async move {
3097 let now = in_flight.fetch_add(1, Ordering::SeqCst) + 1;
3098 max_in_flight.fetch_max(now, Ordering::SeqCst);
3099 tokio::task::yield_now().await;
3102 in_flight.fetch_sub(1, Ordering::SeqCst);
3103 Ok(std::time::Instant::now())
3104 }
3105 };
3106
3107 let outcome = merkle_store_with_retry(
3108 addrs,
3109 move || cap,
3110 1,
3111 Duration::ZERO,
3112 None,
3113 0,
3114 count,
3115 store_one,
3116 )
3117 .await
3118 .expect("all stores succeed");
3119
3120 assert_eq!(outcome.stored, count);
3121 let peak = max_in_flight.load(Ordering::SeqCst);
3122 assert!(
3123 peak <= cap,
3124 "at most `cap` bodies may be in flight (memory bound), got peak {peak} > cap {cap}",
3125 );
3126 assert!(
3127 peak > 1,
3128 "the pass must actually run concurrently, not serialize (peak {peak})",
3129 );
3130 }
3131
3132 #[tokio::test]
3137 async fn store_with_retry_treats_remote_put_as_recoverable() {
3138 let chunks = make_addrs(6);
3139 let failing: std::collections::HashSet<[u8; 32]> = chunks.iter().take(2).copied().collect();
3140 let failing_for_closure = failing.clone();
3141
3142 let store_one = move |addr: [u8; 32]| {
3143 let fail = failing_for_closure.contains(&addr);
3144 async move {
3145 if fail {
3146 Err(Error::RemotePut {
3147 address: hex::encode(addr),
3148 source: ant_protocol::ProtocolError::StorageFailed(
3149 "insufficient disk space".into(),
3150 ),
3151 })
3152 } else {
3153 Ok(std::time::Instant::now())
3154 }
3155 }
3156 };
3157
3158 let outcome =
3159 merkle_store_with_retry(chunks, || 8, 1, Duration::ZERO, None, 0, 6, store_one)
3160 .await
3161 .expect("remote app-rejections must not abort the batch");
3162
3163 assert_eq!(outcome.stored, 4);
3164 assert_eq!(outcome.failed, 2);
3165 }
3166
3167 #[tokio::test]
3171 async fn store_with_retry_reports_non_quorum_errors_as_fatal() {
3172 let chunks = make_addrs(3);
3173 let store_one = |_addr: [u8; 32]| async move {
3174 Err::<std::time::Instant, _>(Error::Payment("missing proof".into()))
3175 };
3176
3177 let outcome =
3178 merkle_store_with_retry(chunks, || 8, 3, Duration::ZERO, None, 0, 3, store_one)
3179 .await
3180 .expect("fatal is carried in the outcome, not returned as Err");
3181 assert!(matches!(outcome.fatal, Some(Error::Payment(_))));
3182 }
3183
3184 #[tokio::test]
3189 async fn store_with_retry_fatal_preserves_same_pass_successes() {
3190 let chunks = make_addrs(6);
3191 let bad = chunks[5];
3192 let store_one = move |addr: [u8; 32]| async move {
3193 if addr == bad {
3194 Err(Error::Payment("fatal".into()))
3195 } else {
3196 Ok(std::time::Instant::now())
3197 }
3198 };
3199
3200 let outcome =
3201 merkle_store_with_retry(chunks, || 1, 1, Duration::ZERO, None, 0, 6, store_one)
3202 .await
3203 .expect("fatal carried in outcome, not returned as Err");
3204 assert!(matches!(outcome.fatal, Some(Error::Payment(_))));
3205 assert_eq!(outcome.stored, 5);
3207 assert_eq!(outcome.stored_addresses.len(), 5);
3208 assert!(!outcome.stored_addresses.contains(&bad));
3209 assert!(outcome.failed_addresses.iter().any(|(a, _)| *a == bad));
3211 }
3212
3213 #[tokio::test]
3215 async fn store_with_retry_retries_only_the_failed_set() {
3216 let chunks = make_addrs(5);
3217 let total = chunks.len();
3218 let failing: std::collections::HashSet<[u8; 32]> = chunks.iter().take(2).copied().collect();
3219 let failing_for_closure = failing.clone();
3220
3221 let calls = Arc::new(Mutex::new(Vec::<[u8; 32]>::new()));
3223 let calls_for_closure = calls.clone();
3224
3225 let store_one = move |addr: [u8; 32]| {
3226 let calls = calls_for_closure.clone();
3227 let already_seen = calls.lock().unwrap().iter().filter(|&&a| a == addr).count();
3229 let fail = failing_for_closure.contains(&addr) && already_seen == 0;
3230 calls.lock().unwrap().push(addr);
3231 async move {
3232 if fail {
3233 Err(Error::InsufficientPeers("round-1 shortfall".into()))
3234 } else {
3235 Ok(std::time::Instant::now())
3236 }
3237 }
3238 };
3239
3240 let outcome =
3241 merkle_store_with_retry(chunks, || 8, 3, Duration::ZERO, None, 0, total, store_one)
3242 .await
3243 .expect("should converge after retry");
3244
3245 assert_eq!(outcome.stored, total);
3246 assert_eq!(outcome.failed, 0);
3247
3248 let calls = calls.lock().unwrap();
3252 assert_eq!(calls.len(), total + failing.len());
3253 let round_two: std::collections::HashSet<[u8; 32]> =
3254 calls[total..].iter().copied().collect();
3255 assert_eq!(round_two, failing);
3256 }
3257
3258 #[tokio::test]
3261 async fn store_with_retry_counts_retry_success_once_in_histogram() {
3262 let chunks = make_addrs(4);
3263 let total = chunks.len();
3264 let flaky_addr = chunks[0];
3265
3266 let attempts = Arc::new(Mutex::new(HashMap::<[u8; 32], usize>::new()));
3267 let attempts_for_closure = attempts.clone();
3268
3269 let store_one = move |addr: [u8; 32]| {
3270 let attempts = attempts_for_closure.clone();
3271 let n = {
3272 let mut m = attempts.lock().unwrap();
3273 let entry = m.entry(addr).or_insert(0);
3274 *entry += 1;
3275 *entry
3276 };
3277 let fail = addr == flaky_addr && n == 1;
3278 async move {
3279 if fail {
3280 Err(Error::InsufficientPeers("transient".into()))
3281 } else {
3282 Ok(std::time::Instant::now())
3283 }
3284 }
3285 };
3286
3287 let outcome =
3288 merkle_store_with_retry(chunks, || 8, 3, Duration::ZERO, None, 0, total, store_one)
3289 .await
3290 .expect("flaky chunk should recover on retry");
3291
3292 assert_eq!(outcome.stored, total);
3293 assert_eq!(outcome.failed, 0);
3294 assert_eq!(outcome.stats.retries_histogram[0], total - 1);
3296 assert_eq!(outcome.stats.retries_histogram[1], 1);
3297 assert_eq!(outcome.stats.chunk_attempts_total, total + 1);
3299 }
3300
3301 #[tokio::test]
3306 async fn store_with_retry_reports_all_failed_when_retries_exhausted() {
3307 let chunks = make_addrs(3);
3308 let total = chunks.len();
3309
3310 let store_one = |_addr: [u8; 32]| async move {
3311 Err::<std::time::Instant, _>(Error::InsufficientPeers("never converges".into()))
3312 };
3313
3314 let outcome = merkle_store_with_retry(
3315 chunks,
3316 || 8,
3317 MERKLE_STORE_MAX_ATTEMPTS,
3318 Duration::ZERO,
3319 None,
3320 0,
3321 total,
3322 store_one,
3323 )
3324 .await
3325 .expect("an exhausted retry budget is reported, not propagated as Err");
3326
3327 assert_eq!(outcome.stored, 0);
3328 assert_eq!(outcome.failed, total);
3329 assert_eq!(
3331 outcome.stats.chunk_attempts_total,
3332 total * MERKLE_STORE_MAX_ATTEMPTS
3333 );
3334 assert_eq!(outcome.stats.retries_histogram, [0; 4]);
3336 }
3337
3338 #[tokio::test]
3343 async fn store_with_retry_records_failed_addresses_when_exhausted() {
3344 let chunks = make_addrs(6);
3345 let failing: std::collections::HashSet<[u8; 32]> = chunks.iter().take(2).copied().collect();
3346 let failing_for_closure = failing.clone();
3347
3348 let store_one = move |addr: [u8; 32]| {
3349 let fail = failing_for_closure.contains(&addr);
3350 async move {
3351 if fail {
3352 Err(Error::InsufficientPeers("permanent shortfall".into()))
3353 } else {
3354 Ok(std::time::Instant::now())
3355 }
3356 }
3357 };
3358
3359 let outcome = merkle_store_with_retry(
3360 chunks,
3361 || 8,
3362 MERKLE_STORE_MAX_ATTEMPTS,
3363 Duration::ZERO,
3364 None,
3365 0,
3366 6,
3367 store_one,
3368 )
3369 .await
3370 .expect("quorum shortfalls must not abort the batch");
3371
3372 assert_eq!(outcome.stored, 4);
3373 assert_eq!(outcome.failed, 2);
3374 assert_eq!(outcome.failed_addresses.len(), 2);
3376 let reported: std::collections::HashSet<[u8; 32]> =
3377 outcome.failed_addresses.iter().map(|(a, _)| *a).collect();
3378 assert_eq!(reported, failing);
3379 for (_, msg) in &outcome.failed_addresses {
3381 assert!(msg.contains("permanent shortfall"));
3382 }
3383 }
3384
3385 #[tokio::test]
3388 async fn store_with_retry_failed_addresses_empty_on_full_success() {
3389 let chunks = make_addrs(4);
3390 let total = chunks.len();
3391 let store_one = |_addr: [u8; 32]| async move { Ok(std::time::Instant::now()) };
3392
3393 let outcome = merkle_store_with_retry(
3394 chunks,
3395 || 8,
3396 MERKLE_STORE_MAX_ATTEMPTS,
3397 Duration::ZERO,
3398 None,
3399 0,
3400 total,
3401 store_one,
3402 )
3403 .await
3404 .expect("all chunks store");
3405
3406 assert_eq!(outcome.stored, total);
3407 assert_eq!(outcome.failed, 0);
3408 assert!(outcome.failed_addresses.is_empty());
3409 }
3410
3411 #[test]
3418 fn deferred_round_histogram_slot_maps_and_clamps() {
3419 assert_eq!(deferred_round_histogram_slot(0, 4), 1);
3420 assert_eq!(deferred_round_histogram_slot(1, 4), 2);
3421 assert_eq!(deferred_round_histogram_slot(2, 4), 3);
3422 assert_eq!(deferred_round_histogram_slot(3, 4), 3);
3424 assert_eq!(deferred_round_histogram_slot(9, 4), 3);
3425 }
3426
3427 fn deferred_set(count: usize) -> Vec<([u8; 32], String)> {
3428 make_test_addresses(count)
3429 .into_iter()
3430 .map(|addr| (addr, "short of quorum".to_string()))
3431 .collect()
3432 }
3433
3434 #[tokio::test]
3438 async fn deferred_retry_succeeds_on_a_later_round() {
3439 let deferred = deferred_set(3);
3440 let attempts = Arc::new(Mutex::new(HashMap::<[u8; 32], usize>::new()));
3443 let attempts_for_closure = attempts.clone();
3444 let store_one = move |addr: [u8; 32]| {
3445 let attempts = attempts_for_closure.clone();
3446 async move {
3447 let n = {
3448 let mut map = attempts.lock().unwrap();
3449 let e = map.entry(addr).or_insert(0);
3450 *e += 1;
3451 *e
3452 };
3453 if n < 2 {
3454 Err(Error::InsufficientPeers("still short".into()))
3455 } else {
3456 Ok(std::time::Instant::now())
3457 }
3458 }
3459 };
3460
3461 let outcome = merkle_deferred_retry(
3462 deferred,
3463 &[0, 0, 0],
3464 |n: usize| n.max(1),
3465 None,
3466 0,
3467 3,
3468 store_one,
3469 )
3470 .await
3471 .expect("deferred retry must not abort on quorum shortfalls");
3472
3473 assert_eq!(outcome.stored, 3, "all three land by round 1");
3474 assert_eq!(outcome.stored_addresses.len(), 3);
3475 assert_eq!(outcome.failed, 0);
3476 assert!(outcome.failed_addresses.is_empty());
3477 assert!(outcome.fatal.is_none());
3478 assert_eq!(outcome.stats.retries_histogram[1], 0);
3480 assert_eq!(outcome.stats.retries_histogram[2], 3);
3481 assert_eq!(outcome.stats.chunk_attempts_total, 6);
3483 }
3484
3485 #[tokio::test]
3488 async fn deferred_retry_leftovers_become_failed() {
3489 let deferred = deferred_set(2);
3490 let store_one = |_addr: [u8; 32]| async move {
3491 Err::<std::time::Instant, _>(Error::InsufficientPeers("always short".into()))
3492 };
3493
3494 let outcome = merkle_deferred_retry(
3495 deferred,
3496 &[0, 0, 0],
3497 |n: usize| n.max(1),
3498 None,
3499 0,
3500 2,
3501 store_one,
3502 )
3503 .await
3504 .expect("exhausted retries report failures, not an error");
3505
3506 assert_eq!(outcome.stored, 0);
3507 assert!(outcome.stored_addresses.is_empty());
3508 assert_eq!(outcome.failed, 2);
3509 assert_eq!(outcome.failed_addresses.len(), 2);
3510 assert!(outcome.fatal.is_none());
3511 assert_eq!(outcome.stats.chunk_attempts_total, 6);
3513 }
3514
3515 #[tokio::test]
3520 async fn deferred_retry_fatal_error_preserves_prior_progress() {
3521 let addrs = make_test_addresses(2);
3522 let good = addrs[0];
3523 let bad = addrs[1];
3524 let deferred = vec![(good, "short".to_string()), (bad, "short".to_string())];
3525
3526 let attempts = Arc::new(Mutex::new(HashMap::<[u8; 32], usize>::new()));
3529 let attempts_for_closure = attempts.clone();
3530 let store_one = move |addr: [u8; 32]| {
3531 let attempts = attempts_for_closure.clone();
3532 async move {
3533 let n = {
3534 let mut map = attempts.lock().unwrap();
3535 let e = map.entry(addr).or_insert(0);
3536 *e += 1;
3537 *e
3538 };
3539 if addr == good {
3540 Ok(std::time::Instant::now())
3541 } else if n == 1 {
3542 Err(Error::InsufficientPeers("short".into()))
3543 } else {
3544 Err(Error::Payment("fatal on retry".into()))
3545 }
3546 }
3547 };
3548
3549 let outcome = merkle_deferred_retry(
3550 deferred,
3551 &[0, 0, 0],
3552 |n: usize| n.max(1),
3553 None,
3554 0,
3555 2,
3556 store_one,
3557 )
3558 .await
3559 .expect("a fatal round error is reported via `fatal`, not as Err");
3560
3561 assert!(outcome.fatal.is_some(), "fatal error must be captured");
3562 assert_eq!(outcome.stored, 1, "round-0 success preserved");
3563 assert_eq!(outcome.stored_addresses, vec![good]);
3564 assert_eq!(outcome.failed, 1);
3565 assert_eq!(outcome.failed_addresses.len(), 1);
3566 assert_eq!(outcome.failed_addresses[0].0, bad);
3567 }
3568
3569 #[tokio::test]
3571 async fn deferred_retry_empty_set_is_a_noop() {
3572 let store_one = |_addr: [u8; 32]| async move {
3573 Err::<std::time::Instant, _>(Error::InsufficientPeers("unused".into()))
3574 };
3575
3576 let outcome = merkle_deferred_retry(
3577 Vec::new(),
3578 &DEFERRED_ROUND_DELAYS_SECS,
3579 |n: usize| n.max(1),
3580 None,
3581 7,
3582 7,
3583 store_one,
3584 )
3585 .await
3586 .expect("empty deferred set is a no-op");
3587
3588 assert_eq!(outcome.stored, 7, "stored_offset carried through unchanged");
3589 assert_eq!(outcome.failed, 0);
3590 assert!(outcome.stored_addresses.is_empty());
3591 assert!(outcome.failed_addresses.is_empty());
3592 assert!(outcome.fatal.is_none());
3593 }
3594
3595 #[test]
3605 fn a_merkle_refusal_that_does_not_describe_this_client_is_ignored() {
3606 use ant_protocol::CURRENT_SETTLEMENT_VERSION;
3607
3608 let peer_id = PeerId::from_bytes([0x71; 32]);
3609 let refusal = |client: u32, min: u32| {
3610 map_merkle_candidate_response(
3611 peer_id,
3612 ChunkMessageBody::MerkleCandidateQuoteResponse(
3613 MerkleCandidateQuoteResponse::Error(ProtocolError::ClientUpdateRequired {
3614 client_settlement_version: client,
3615 min_settlement_version: min,
3616 }),
3617 ),
3618 )
3619 };
3620
3621 let wrong_echo = refusal(
3624 CURRENT_SETTLEMENT_VERSION.saturating_add(7),
3625 CURRENT_SETTLEMENT_VERSION.saturating_add(8),
3626 );
3627 assert!(
3628 matches!(wrong_echo, Some(Err(Error::Protocol(_)))),
3629 "{wrong_echo:?}"
3630 );
3631
3632 let no_gap = refusal(CURRENT_SETTLEMENT_VERSION, CURRENT_SETTLEMENT_VERSION);
3635 assert!(
3636 matches!(no_gap, Some(Err(Error::Protocol(_)))),
3637 "{no_gap:?}"
3638 );
3639
3640 match refusal(
3642 CURRENT_SETTLEMENT_VERSION,
3643 CURRENT_SETTLEMENT_VERSION.saturating_add(1),
3644 ) {
3645 Some(Err(Error::ClientUpdateRequired(msg))) => {
3646 assert!(msg.contains("ant update"), "{msg}");
3647 }
3648 other => panic!("expected ClientUpdateRequired, got {other:?}"),
3649 }
3650 }
3651
3652 #[test]
3657 fn a_refusal_outranks_an_ordinary_pool_failure() {
3658 let refusal = || Error::ClientUpdateRequired("run ant update".to_string());
3659 let ordinary = || Error::InsufficientPeers("need 16, got 2".to_string());
3660
3661 let mut verdict = PoolVerdict::default();
3663 verdict.note(ordinary());
3664 verdict.note(refusal());
3665 assert!(
3666 matches!(verdict.into_error(), Some(Error::ClientUpdateRequired(_))),
3667 "a later refusal must still outrank an earlier failure"
3668 );
3669
3670 let mut verdict = PoolVerdict::default();
3672 verdict.note(refusal());
3673 verdict.note(ordinary());
3674 assert!(
3675 matches!(verdict.into_error(), Some(Error::ClientUpdateRequired(_))),
3676 "an earlier refusal must not be displaced by a later failure"
3677 );
3678
3679 let mut verdict = PoolVerdict::default();
3681 verdict.note(ordinary());
3682 verdict.note(Error::Protocol("second".to_string()));
3683 assert!(
3684 matches!(verdict.into_error(), Some(Error::InsufficientPeers(_))),
3685 "the first ordinary failure is the one reported"
3686 );
3687
3688 assert!(PoolVerdict::default().into_error().is_none());
3690 }
3691}