1use crate::data::client::adaptive::{observe_op, Outcome};
8use crate::data::client::classify_error;
9use crate::data::client::file::UploadEvent;
10use crate::data::client::Client;
11use crate::data::error::{Error, Result};
12use ant_protocol::evm::{
13 Amount, MerklePaymentCandidateNode, MerklePaymentCandidatePool, MerklePaymentProof, MerkleTree,
14 MidpointProof, PoolCommitment, CANDIDATES_PER_POOL, MAX_LEAVES,
15};
16use ant_protocol::payment::commitment::{
17 commitment_hash, verify_commitment_signature, StorageCommitment, MAX_COMMITMENT_KEY_COUNT,
18 MAX_COMMITMENT_SIDECAR_BYTES,
19};
20use ant_protocol::payment::{
21 calculate_price, serialize_merkle_proof, verify_merkle_candidate_signature,
22};
23use ant_protocol::transport::PeerId;
24use ant_protocol::{
25 compute_address, send_and_await_chunk_response, ChunkMessage, ChunkMessageBody,
26 MerkleCandidateQuoteRequest, MerkleCandidateQuoteResponse,
27};
28use bytes::Bytes;
29use futures::stream::{self, FuturesUnordered, StreamExt};
30use rand::Rng;
31use std::collections::{HashMap, VecDeque};
32use std::time::Duration;
33use tokio::sync::mpsc;
34use tracing::{debug, info, warn};
35use xor_name::XorName;
36
37pub const DEFAULT_MERKLE_THRESHOLD: usize = 64;
39
40fn merkle_candidate_binding_is_valid(
50 peer_id: &PeerId,
51 candidate: &MerklePaymentCandidateNode,
52 commitment: &Option<Vec<u8>>,
53) -> std::result::Result<(), String> {
54 let count = candidate.committed_key_count;
55 let pin = candidate.commitment_pin;
56 match (count, pin.is_some()) {
57 (0, false) | (1.., true) => {}
58 (1.., false) => {
59 return Err(format!(
60 "committed_key_count={count} > 0 but commitment_pin is None (unauditable count)"
61 ));
62 }
63 (0, true) => {
64 return Err("committed_key_count=0 with a commitment_pin (incoherent baseline)".into());
65 }
66 }
67 if count > MAX_COMMITMENT_KEY_COUNT {
68 return Err(format!(
69 "committed_key_count={count} exceeds MAX_COMMITMENT_KEY_COUNT={MAX_COMMITMENT_KEY_COUNT}"
70 ));
71 }
72 let expected = calculate_price(count as usize);
73 if candidate.price != expected {
74 return Err(format!(
75 "price {} does not equal calculate_price(committed_key_count={count}) = {expected}",
76 candidate.price
77 ));
78 }
79
80 let Some(pin) = pin else {
81 return Ok(()); };
83 let Some(blob) = commitment else {
84 return Err("bound candidate did not ship its commitment; pin is unresolvable".into());
85 };
86 if blob.len() > MAX_COMMITMENT_SIDECAR_BYTES {
87 return Err(format!(
88 "shipped commitment is {} bytes, exceeds MAX_COMMITMENT_SIDECAR_BYTES={MAX_COMMITMENT_SIDECAR_BYTES}",
89 blob.len()
90 ));
91 }
92 let commitment: StorageCommitment = rmp_serde::from_slice(blob)
93 .map_err(|e| format!("shipped commitment did not deserialize: {e}"))?;
94 if compute_address(&commitment.sender_public_key) != *peer_id.as_bytes()
95 || commitment.sender_peer_id != *peer_id.as_bytes()
96 {
97 return Err("shipped commitment is not bound to the candidate peer".into());
98 }
99 if !verify_commitment_signature(&commitment) {
100 return Err("shipped commitment has an invalid signature".into());
101 }
102 if commitment_hash(&commitment) != Some(pin) {
103 return Err("shipped commitment does not hash to the candidate's pin".into());
104 }
105 if commitment.key_count != count {
106 return Err(format!(
107 "shipped commitment attests key_count={} but the candidate claims {count}",
108 commitment.key_count
109 ));
110 }
111 Ok(())
112}
113
114#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
116#[serde(rename_all = "snake_case")]
117pub enum PaymentMode {
118 #[default]
120 Auto,
121 Merkle,
123 Single,
125}
126
127#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
132pub struct MerkleBatchPaymentResult {
133 pub proofs: HashMap<[u8; 32], Vec<u8>>,
135 pub chunk_count: usize,
137 pub storage_cost_atto: String,
139 pub gas_cost_wei: u128,
141 #[serde(default)]
146 pub merkle_payment_timestamp: u64,
147}
148
149pub struct PreparedMerkleBatch {
154 pub depth: u8,
156 pub pool_commitments: Vec<PoolCommitment>,
158 pub merkle_payment_timestamp: u64,
160 candidate_pools: Vec<MerklePaymentCandidatePool>,
162 tree: MerkleTree,
164 addresses: Vec<[u8; 32]>,
166}
167
168#[derive(Debug, Clone, Default)]
170pub(crate) struct MerkleUploadPlan {
171 pub already_stored: Vec<[u8; 32]>,
173 pub to_upload: Vec<[u8; 32]>,
175 to_upload_total_bytes: u64,
177}
178
179impl MerkleUploadPlan {
180 #[must_use]
182 pub fn to_upload_avg_size(&self) -> u64 {
183 if self.to_upload.is_empty() {
184 return 0;
185 }
186
187 self.to_upload_total_bytes / self.to_upload.len() as u64
188 }
189}
190
191impl std::fmt::Debug for PreparedMerkleBatch {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 f.debug_struct("PreparedMerkleBatch")
194 .field("depth", &self.depth)
195 .field("pool_commitments", &self.pool_commitments.len())
196 .field("merkle_payment_timestamp", &self.merkle_payment_timestamp)
197 .field("candidate_pools", &self.candidate_pools.len())
198 .field("addresses", &self.addresses.len())
199 .finish()
200 }
201}
202
203pub(crate) fn chunk_contents_for_upload_addresses(
208 chunk_contents: Vec<Bytes>,
209 addresses: &[[u8; 32]],
210) -> Result<Vec<Bytes>> {
211 if addresses.is_empty() {
212 return Ok(Vec::new());
213 }
214
215 let mut needed_by_address: HashMap<[u8; 32], usize> = HashMap::new();
216 for address in addresses {
217 *needed_by_address.entry(*address).or_default() += 1;
218 }
219
220 let mut chunks_by_address: HashMap<[u8; 32], VecDeque<Bytes>> =
221 HashMap::with_capacity(needed_by_address.len());
222 let mut remaining = addresses.len();
223 for chunk in chunk_contents {
224 let address = compute_address(&chunk);
225 if let Some(needed) = needed_by_address.get_mut(&address) {
226 if *needed > 0 {
227 chunks_by_address
228 .entry(address)
229 .or_default()
230 .push_back(chunk);
231 *needed -= 1;
232 remaining -= 1;
233 if remaining == 0 {
234 break;
235 }
236 }
237 }
238 }
239
240 for (address, needed) in &needed_by_address {
241 if *needed == 0 {
242 continue;
243 }
244
245 if chunks_by_address.contains_key(address) {
246 return Err(Error::InvalidData(format!(
247 "missing duplicate chunk content for merkle address {}",
248 hex::encode(address)
249 )));
250 }
251
252 return Err(Error::InvalidData(format!(
253 "missing chunk content for merkle address {}",
254 hex::encode(address)
255 )));
256 }
257
258 let mut selected = Vec::with_capacity(addresses.len());
259 for address in addresses {
260 let chunks = chunks_by_address.get_mut(address).ok_or_else(|| {
261 Error::InvalidData(format!(
262 "missing chunk content for merkle address {}",
263 hex::encode(address)
264 ))
265 })?;
266 let chunk = chunks.pop_front().ok_or_else(|| {
267 Error::InvalidData(format!(
268 "missing duplicate chunk content for merkle address {}",
269 hex::encode(address)
270 ))
271 })?;
272 selected.push(chunk);
273 }
274
275 Ok(selected)
276}
277
278fn preflight_stored_status<T>(result: Result<T>) -> Result<bool> {
297 match result {
298 Ok(_) => Ok(false),
299 Err(Error::AlreadyStored) => Ok(true),
300 Err(e) if matches!(classify_error(&e), Outcome::Timeout | Outcome::NetworkError) => {
301 Ok(false)
302 }
303 Err(e) => Err(e),
304 }
305}
306
307#[must_use]
310pub fn should_use_merkle(chunk_count: usize, mode: PaymentMode) -> bool {
311 match mode {
312 PaymentMode::Auto => chunk_count >= DEFAULT_MERKLE_THRESHOLD,
313 PaymentMode::Merkle => chunk_count >= 2,
314 PaymentMode::Single => false,
315 }
316}
317
318impl Client {
319 #[must_use]
321 pub fn should_use_merkle(&self, chunk_count: usize, mode: PaymentMode) -> bool {
322 should_use_merkle(chunk_count, mode)
323 }
324
325 pub async fn pay_for_merkle_batch(
339 &self,
340 addresses: &[[u8; 32]],
341 data_type: u32,
342 data_size: u64,
343 ) -> Result<MerkleBatchPaymentResult> {
344 let chunk_count = addresses.len();
345 if chunk_count < 2 {
346 return Err(Error::Payment(
347 "Merkle batch payment requires at least 2 chunks".to_string(),
348 ));
349 }
350
351 if chunk_count > MAX_LEAVES {
352 return self
353 .pay_for_merkle_multi_batch(addresses, data_type, data_size)
354 .await;
355 }
356
357 self.pay_for_merkle_single_batch(addresses, data_type, data_size)
358 .await
359 }
360
361 pub(crate) async fn plan_merkle_upload(
370 &self,
371 chunks: Vec<([u8; 32], u64)>,
372 data_type: u32,
373 progress: Option<&mpsc::Sender<UploadEvent>>,
374 ) -> Result<MerkleUploadPlan> {
375 let total_chunks = chunks.len();
376 if total_chunks == 0 {
377 return Ok(MerkleUploadPlan::default());
378 }
379
380 info!("Checking {total_chunks} merkle chunks for existing storage before payment");
381
382 let quote_limiter = self.controller().quote.clone();
383 let quote_concurrency = quote_limiter.current().min(total_chunks.max(1));
384 let mut check_stream = stream::iter(chunks.into_iter().enumerate())
385 .map(|(index, (address, data_size))| {
386 let limiter = quote_limiter.clone();
387 async move {
388 let result = observe_op(
389 &limiter,
390 || async move {
391 self.chunk_already_stored_for_merkle(&address, data_type, data_size)
392 .await
393 },
394 classify_error,
395 )
396 .await;
397 (index, address, data_size, result)
398 }
399 })
400 .buffer_unordered(quote_concurrency);
401
402 let mut already_stored: Vec<(usize, [u8; 32])> = Vec::new();
403 let mut to_upload: Vec<(usize, [u8; 32], u64)> = Vec::new();
404 let mut checked = 0usize;
405
406 while let Some((index, address, data_size, result)) = check_stream.next().await {
407 let is_already_stored = result?;
408 checked += 1;
409
410 if let Some(tx) = progress {
411 let _ = tx.try_send(UploadEvent::ChunkQuoted {
412 quoted: checked,
413 total: total_chunks,
414 });
415 }
416
417 if is_already_stored {
418 debug!(
419 "Merkle preflight {checked}/{total_chunks}: chunk {} already stored",
420 hex::encode(address)
421 );
422 already_stored.push((index, address));
423 if let Some(tx) = progress {
424 let _ = tx.try_send(UploadEvent::ChunkStored {
425 stored: already_stored.len(),
426 total: total_chunks,
427 });
428 }
429 } else {
430 debug!(
431 "Merkle preflight {checked}/{total_chunks}: chunk {} needs upload",
432 hex::encode(address)
433 );
434 to_upload.push((index, address, data_size));
435 }
436 }
437
438 already_stored.sort_by_key(|(index, _)| *index);
439 to_upload.sort_by_key(|(index, _, _)| *index);
440
441 let to_upload_total_bytes = to_upload.iter().fold(0u64, |acc, (_, _, data_size)| {
442 acc.saturating_add(*data_size)
443 });
444
445 let already_stored = already_stored
446 .into_iter()
447 .map(|(_, address)| address)
448 .collect::<Vec<_>>();
449 let to_upload = to_upload
450 .into_iter()
451 .map(|(_, address, _)| address)
452 .collect::<Vec<_>>();
453
454 info!(
455 "Merkle preflight complete: {} already stored, {} need upload",
456 already_stored.len(),
457 to_upload.len()
458 );
459
460 Ok(MerkleUploadPlan {
461 already_stored,
462 to_upload,
463 to_upload_total_bytes,
464 })
465 }
466
467 async fn chunk_already_stored_for_merkle(
468 &self,
469 address: &[u8; 32],
470 data_type: u32,
471 data_size: u64,
472 ) -> Result<bool> {
473 let result = self
474 .get_store_quotes_with_fault_tolerance(address, data_size, data_type)
475 .await;
476 if let Err(e) = &result {
477 if matches!(classify_error(e), Outcome::Timeout | Outcome::NetworkError) {
478 debug!(
479 "Merkle preflight: could not determine stored status for {} ({e}); \
480 treating as not stored and queuing for upload",
481 hex::encode(address)
482 );
483 }
484 }
485 preflight_stored_status(result)
486 }
487
488 pub async fn prepare_merkle_batch_external(
494 &self,
495 addresses: &[[u8; 32]],
496 data_type: u32,
497 data_size: u64,
498 ) -> Result<PreparedMerkleBatch> {
499 let chunk_count = addresses.len();
500 let xornames: Vec<XorName> = addresses.iter().map(|a| XorName(*a)).collect();
501
502 debug!("Building merkle tree for {chunk_count} chunks");
503
504 let tree = MerkleTree::from_xornames(xornames)
506 .map_err(|e| Error::Payment(format!("Failed to build merkle tree: {e}")))?;
507
508 let depth = tree.depth();
509 let merkle_payment_timestamp = std::time::SystemTime::now()
510 .duration_since(std::time::UNIX_EPOCH)
511 .map_err(|e| Error::Payment(format!("System time error: {e}")))?
512 .as_secs();
513
514 debug!("Merkle tree: depth={depth}, leaves={chunk_count}, ts={merkle_payment_timestamp}");
515
516 let midpoint_proofs = tree
518 .reward_candidates(merkle_payment_timestamp)
519 .map_err(|e| Error::Payment(format!("Failed to generate reward candidates: {e}")))?;
520
521 debug!(
522 "Collecting candidate pools from {} midpoints (concurrent)",
523 midpoint_proofs.len()
524 );
525
526 let candidate_pools = self
532 .build_candidate_pools(
533 &midpoint_proofs,
534 data_type,
535 data_size,
536 merkle_payment_timestamp,
537 )
538 .await?;
539
540 let pool_commitments: Vec<PoolCommitment> = candidate_pools
542 .iter()
543 .map(MerklePaymentCandidatePool::to_commitment)
544 .collect();
545
546 Ok(PreparedMerkleBatch {
547 depth,
548 pool_commitments,
549 merkle_payment_timestamp,
550 candidate_pools,
551 tree,
552 addresses: addresses.to_vec(),
553 })
554 }
555
556 async fn pay_for_merkle_single_batch(
558 &self,
559 addresses: &[[u8; 32]],
560 data_type: u32,
561 data_size: u64,
562 ) -> Result<MerkleBatchPaymentResult> {
563 let wallet = self.require_wallet()?;
564 let prepared = self
565 .prepare_merkle_batch_external(addresses, data_type, data_size)
566 .await?;
567
568 info!(
569 "Submitting merkle batch payment on-chain (depth={})",
570 prepared.depth
571 );
572 let (winner_pool_hash, amount, gas_info) = wallet
573 .pay_for_merkle_tree(
574 prepared.depth,
575 prepared.pool_commitments.clone(),
576 prepared.merkle_payment_timestamp,
577 )
578 .await
579 .map_err(|e| Error::Payment(format!("Merkle batch payment failed: {e}")))?;
580
581 info!(
582 "Merkle payment succeeded: winner pool {}",
583 hex::encode(winner_pool_hash)
584 );
585
586 let mut result = finalize_merkle_batch(prepared, winner_pool_hash)?;
587 result.storage_cost_atto = amount.to_string();
588 result.gas_cost_wei = gas_info.gas_cost_wei;
589 Ok(result)
590 }
591
592 async fn pay_for_merkle_multi_batch(
594 &self,
595 addresses: &[[u8; 32]],
596 data_type: u32,
597 data_size: u64,
598 ) -> Result<MerkleBatchPaymentResult> {
599 let sub_batches: Vec<&[[u8; 32]]> = addresses.chunks(MAX_LEAVES).collect();
600 let total_sub_batches = sub_batches.len();
601 let mut all_proofs = HashMap::with_capacity(addresses.len());
602 let mut total_storage = Amount::ZERO;
603 let mut total_gas: u128 = 0;
604 let mut oldest_ts: u64 = 0;
608
609 for (i, chunk) in sub_batches.into_iter().enumerate() {
610 match self
611 .pay_for_merkle_single_batch(chunk, data_type, data_size)
612 .await
613 {
614 Ok(sub_result) => {
615 if let Ok(cost) = sub_result.storage_cost_atto.parse::<Amount>() {
616 total_storage += cost;
617 }
618 total_gas = total_gas.saturating_add(sub_result.gas_cost_wei);
619 if oldest_ts == 0
620 || (sub_result.merkle_payment_timestamp > 0
621 && sub_result.merkle_payment_timestamp < oldest_ts)
622 {
623 oldest_ts = sub_result.merkle_payment_timestamp;
624 }
625 all_proofs.extend(sub_result.proofs);
626 }
627 Err(e) => {
628 if all_proofs.is_empty() {
629 return Err(e);
631 }
632 warn!(
634 "Merkle sub-batch {}/{total_sub_batches} failed: {e}. \
635 Returning {} proofs from prior sub-batches",
636 i + 1,
637 all_proofs.len()
638 );
639 return Ok(MerkleBatchPaymentResult {
640 chunk_count: all_proofs.len(),
641 proofs: all_proofs,
642 storage_cost_atto: total_storage.to_string(),
643 gas_cost_wei: total_gas,
644 merkle_payment_timestamp: oldest_ts,
645 });
646 }
647 }
648 }
649
650 Ok(MerkleBatchPaymentResult {
651 chunk_count: addresses.len(),
652 proofs: all_proofs,
653 storage_cost_atto: total_storage.to_string(),
654 gas_cost_wei: total_gas,
655 merkle_payment_timestamp: oldest_ts,
656 })
657 }
658
659 async fn build_candidate_pools(
661 &self,
662 midpoint_proofs: &[MidpointProof],
663 data_type: u32,
664 data_size: u64,
665 merkle_payment_timestamp: u64,
666 ) -> Result<Vec<MerklePaymentCandidatePool>> {
667 let mut pool_futures = FuturesUnordered::new();
668
669 for midpoint_proof in midpoint_proofs {
670 let pool_address = midpoint_proof.address();
671 let mp = midpoint_proof.clone();
672 pool_futures.push(async move {
673 let candidate_nodes = self
674 .get_merkle_candidate_pool(
675 &pool_address.0,
676 data_type,
677 data_size,
678 merkle_payment_timestamp,
679 )
680 .await?;
681 Ok::<_, Error>(MerklePaymentCandidatePool {
682 midpoint_proof: mp,
683 candidate_nodes,
684 })
685 });
686 }
687
688 let mut pools = Vec::with_capacity(midpoint_proofs.len());
689 while let Some(result) = pool_futures.next().await {
690 pools.push(result?);
691 }
692
693 Ok(pools)
694 }
695
696 #[allow(clippy::too_many_lines)]
698 async fn get_merkle_candidate_pool(
699 &self,
700 address: &[u8; 32],
701 data_type: u32,
702 data_size: u64,
703 merkle_payment_timestamp: u64,
704 ) -> Result<[MerklePaymentCandidateNode; CANDIDATES_PER_POOL]> {
705 let node = self.network().node();
706 let timeout = Duration::from_secs(self.config().quote_timeout_secs);
707
708 let query_count = CANDIDATES_PER_POOL * 2;
710 let mut remote_peers = self
711 .network()
712 .find_closest_peers(address, query_count)
713 .await?;
714
715 if remote_peers.len() < CANDIDATES_PER_POOL {
719 let connected = self.network().connected_peers().await;
720 for peer in connected {
721 if !remote_peers.iter().any(|(id, _)| *id == peer) {
722 remote_peers.push((peer, vec![]));
723 }
724 }
725 }
726
727 if remote_peers.len() < CANDIDATES_PER_POOL {
728 return Err(Error::InsufficientPeers(format!(
729 "Found {} peers, need {CANDIDATES_PER_POOL} for merkle candidate pool. \
730 Use --no-merkle or a larger network.",
731 remote_peers.len()
732 )));
733 }
734
735 let mut candidate_futures = FuturesUnordered::new();
736
737 for (peer_id, peer_addrs) in &remote_peers {
738 let request_id = self.next_request_id();
739 let request = MerkleCandidateQuoteRequest {
740 address: *address,
741 data_type,
742 data_size,
743 merkle_payment_timestamp,
744 };
745 let message = ChunkMessage {
746 request_id,
747 body: ChunkMessageBody::MerkleCandidateQuoteRequest(request),
748 };
749
750 let message_bytes = match message.encode() {
751 Ok(bytes) => bytes,
752 Err(e) => {
753 warn!("Failed to encode merkle candidate request for {peer_id}: {e}");
754 continue;
755 }
756 };
757
758 let peer_id_clone = *peer_id;
759 let addrs_clone = peer_addrs.clone();
760 let node_clone = node.clone();
761
762 let fut = async move {
763 let result = send_and_await_chunk_response(
764 &node_clone,
765 &peer_id_clone,
766 message_bytes,
767 request_id,
768 timeout,
769 &addrs_clone,
770 |body| match body {
771 ChunkMessageBody::MerkleCandidateQuoteResponse(
772 MerkleCandidateQuoteResponse::Success {
773 candidate_node,
774 commitment,
775 },
776 ) => {
777 match rmp_serde::from_slice::<MerklePaymentCandidateNode>(
778 &candidate_node,
779 ) {
780 Ok(node) => Some(Ok((node, commitment))),
781 Err(e) => Some(Err(Error::Serialization(format!(
782 "Failed to deserialize candidate node from {peer_id_clone}: {e}"
783 )))),
784 }
785 }
786 ChunkMessageBody::MerkleCandidateQuoteResponse(
787 MerkleCandidateQuoteResponse::Error(e),
788 ) => Some(Err(Error::Protocol(format!(
789 "Merkle quote error from {peer_id_clone}: {e}"
790 )))),
791 _ => None,
792 },
793 |e| {
794 Error::Network(format!(
795 "Failed to send merkle candidate request to {peer_id_clone}: {e}"
796 ))
797 },
798 || {
799 Error::Timeout(format!(
800 "Timeout waiting for merkle candidate from {peer_id_clone}"
801 ))
802 },
803 )
804 .await;
805
806 (peer_id_clone, result)
807 };
808
809 candidate_futures.push(fut);
810 }
811
812 self.collect_validated_candidates(&mut candidate_futures, address, merkle_payment_timestamp)
813 .await
814 }
815
816 async fn collect_validated_candidates(
827 &self,
828 futures: &mut FuturesUnordered<
829 impl std::future::Future<
830 Output = (
831 PeerId,
832 std::result::Result<(MerklePaymentCandidateNode, Option<Vec<u8>>), Error>,
833 ),
834 >,
835 >,
836 target_address: &[u8; 32],
837 merkle_payment_timestamp: u64,
838 ) -> Result<[MerklePaymentCandidateNode; CANDIDATES_PER_POOL]> {
839 let mut valid: Vec<(PeerId, MerklePaymentCandidateNode)> = Vec::new();
840 let mut failures: Vec<String> = Vec::new();
841
842 while let Some((peer_id, result)) = futures.next().await {
843 match result {
844 Ok((candidate, commitment)) => {
845 if !verify_merkle_candidate_signature(&candidate) {
846 warn!("Invalid ML-DSA-65 signature from merkle candidate {peer_id}");
847 failures.push(format!("{peer_id}: invalid signature"));
848 continue;
849 }
850 if candidate.merkle_payment_timestamp != merkle_payment_timestamp {
851 warn!("Timestamp mismatch from merkle candidate {peer_id}");
852 failures.push(format!("{peer_id}: timestamp mismatch"));
853 continue;
854 }
855 let candidate_peer = PeerId::from_bytes(compute_address(&candidate.pub_key));
860 if candidate_peer != peer_id {
861 warn!(
862 "Dropping merkle candidate {peer_id} — pub_key derives {candidate_peer}, \
863 not the responding peer"
864 );
865 failures.push(format!("{peer_id}: candidate pub_key/peer mismatch"));
866 continue;
867 }
868 if let Err(detail) =
877 merkle_candidate_binding_is_valid(&candidate_peer, &candidate, &commitment)
878 {
879 warn!("Dropping merkle candidate {peer_id} — ADR-0004 binding invalid: {detail}");
880 failures.push(format!("{peer_id}: bad commitment binding ({detail})"));
881 continue;
882 }
883 valid.push((candidate_peer, candidate));
884 }
885 Err(e) => {
886 debug!("Failed to get merkle candidate from {peer_id}: {e}");
887 failures.push(format!("{peer_id}: {e}"));
888 }
889 }
890 }
891
892 if valid.len() < CANDIDATES_PER_POOL {
893 return Err(Error::InsufficientPeers(format!(
894 "Got {} merkle candidates, need {CANDIDATES_PER_POOL}. Failures: [{}]",
895 valid.len(),
896 failures.join("; ")
897 )));
898 }
899
900 let target_peer = PeerId::from_bytes(*target_address);
901 valid.sort_by_key(|(peer_id, _)| peer_id.xor_distance(&target_peer));
902
903 let candidates: Vec<MerklePaymentCandidateNode> = valid
904 .into_iter()
905 .take(CANDIDATES_PER_POOL)
906 .map(|(_, candidate)| candidate)
907 .collect();
908
909 let array: [MerklePaymentCandidateNode; CANDIDATES_PER_POOL] =
910 candidates.try_into().map_err(|_| {
911 Error::Payment("Failed to convert candidates to fixed array".to_string())
912 })?;
913 Ok(array)
914 }
915
916 pub(crate) async fn merkle_upload_chunks(
937 &self,
938 chunk_contents: Vec<Bytes>,
939 addresses: Vec<[u8; 32]>,
940 batch_result: &MerkleBatchPaymentResult,
941 progress: Option<&mpsc::Sender<UploadEvent>>,
942 stored_offset: usize,
943 total_chunks: usize,
944 ) -> Result<MerkleStoreOutcome> {
945 let store_limiter = self.controller().store.clone();
946 let batch_size = chunk_contents.len();
949 if batch_size != addresses.len() {
950 return Err(Error::InvalidData(format!(
951 "merkle upload has {batch_size} chunk contents but {} addresses",
952 addresses.len()
953 )));
954 }
955 let cap = || store_limiter.current().min(batch_size.max(1));
959
960 let bodies: std::collections::HashMap<[u8; 32], Bytes> =
965 addresses.iter().copied().zip(chunk_contents).collect();
966 let addrs = addresses;
967
968 let store_one = |addr: [u8; 32]| {
973 let limiter = store_limiter.clone();
974 let content = bodies.get(&addr).cloned();
975 let proof_bytes = batch_result.proofs.get(&addr).cloned();
976 async move {
977 let started = std::time::Instant::now();
978 let content = content.ok_or_else(|| {
979 Error::InvalidData(format!("missing chunk body for {}", hex::encode(addr)))
980 })?;
981 let proof = proof_bytes.ok_or_else(|| {
982 Error::Payment(format!(
983 "Missing merkle proof for chunk {}",
984 hex::encode(addr)
985 ))
986 })?;
987 let peers = self.put_target_peers(&addr).await?;
988 observe_op(
989 &limiter,
990 || async move { self.chunk_put_to_close_group(content, proof, &peers).await },
991 classify_error,
992 )
993 .await
994 .map(|_| started)
995 }
996 };
997
998 let outcome = merkle_store_with_retry(
999 addrs,
1000 cap,
1001 MERKLE_STORE_MAX_ATTEMPTS,
1002 MERKLE_RETRY_BACKOFF,
1003 progress,
1004 stored_offset,
1005 total_chunks,
1006 store_one,
1007 )
1008 .await?;
1009
1010 if let Some(e) = outcome.fatal {
1016 return Err(e);
1017 }
1018 Ok(outcome)
1019 }
1020}
1021
1022pub(crate) const MERKLE_STORE_MAX_ATTEMPTS: usize = 4;
1034
1035pub(crate) const MERKLE_RETRY_BACKOFF: Duration = Duration::from_secs(30);
1041
1042const MERKLE_RETRY_JITTER: f64 = 0.1;
1045
1046#[derive(Debug, Default)]
1049pub(crate) struct MerkleStoreOutcome {
1050 pub stored: usize,
1053 pub stored_addresses: Vec<[u8; 32]>,
1060 pub failed: usize,
1062 pub failed_addresses: Vec<([u8; 32], String)>,
1067 pub fatal: Option<Error>,
1074 pub stats: crate::data::client::batch::WaveAggregateStats,
1076}
1077
1078#[allow(clippy::too_many_arguments)]
1104pub(crate) async fn merkle_store_with_retry<F, Fut, C>(
1105 addrs: Vec<[u8; 32]>,
1106 cap: C,
1107 max_attempts: usize,
1108 backoff: Duration,
1109 progress: Option<&mpsc::Sender<UploadEvent>>,
1110 stored_offset: usize,
1111 total: usize,
1112 store_one: F,
1113) -> Result<MerkleStoreOutcome>
1114where
1115 F: Fn([u8; 32]) -> Fut,
1116 Fut: std::future::Future<Output = Result<std::time::Instant>>,
1117 C: Fn() -> usize,
1118{
1119 let attempts = max_attempts.max(1);
1120 let mut outcome = MerkleStoreOutcome {
1121 stored: stored_offset,
1122 ..MerkleStoreOutcome::default()
1123 };
1124 let mut pending = addrs;
1125
1126 for attempt in 0..attempts {
1127 let mut next_failed: Vec<([u8; 32], String)> = Vec::new();
1133
1134 let mut pending_iter = pending.into_iter();
1139 let mut in_flight = FuturesUnordered::new();
1140 loop {
1141 let slots = cap().max(1);
1142 while in_flight.len() < slots {
1143 match pending_iter.next() {
1144 Some(addr) => {
1145 let fut = store_one(addr);
1146 in_flight.push(async move { (addr, fut.await) });
1147 }
1148 None => break,
1149 }
1150 }
1151 let Some((addr, result)) = in_flight.next().await else {
1152 break;
1153 };
1154 outcome.stats.chunk_attempts_total =
1155 outcome.stats.chunk_attempts_total.saturating_add(1);
1156 match result {
1157 Ok(started) => {
1158 let duration_ms =
1159 u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX);
1160 outcome.stats.store_durations_ms.push(duration_ms);
1161 let idx = attempt.min(outcome.stats.retries_histogram.len().saturating_sub(1));
1162 outcome.stats.retries_histogram[idx] =
1163 outcome.stats.retries_histogram[idx].saturating_add(1);
1164 outcome.stored += 1;
1165 outcome.stored_addresses.push(addr);
1166 if let Some(tx) = progress {
1167 let _ = tx.try_send(UploadEvent::ChunkStored {
1168 stored: outcome.stored,
1169 total,
1170 });
1171 }
1172 }
1173 Err(
1180 e @ (Error::InsufficientPeers(_)
1181 | Error::CloseGroupShortfall(_)
1182 | Error::RemotePut { .. }),
1183 ) => {
1184 next_failed.push((addr, e.to_string()));
1185 }
1186 Err(e) => {
1187 next_failed.push((addr, e.to_string()));
1195 outcome.fatal = Some(e);
1196 break;
1197 }
1198 }
1199 }
1200
1201 if outcome.fatal.is_some() {
1202 outcome.failed = next_failed.len();
1203 outcome.failed_addresses = next_failed;
1204 return Ok(outcome);
1205 }
1206
1207 if next_failed.is_empty() {
1208 break;
1209 }
1210
1211 if attempt + 1 < attempts {
1212 warn!(
1213 failed = next_failed.len(),
1214 attempt = attempt + 1,
1215 "merkle chunks short of quorum, retrying after backoff"
1216 );
1217 pending = next_failed.into_iter().map(|(addr, _msg)| addr).collect();
1218 if backoff > Duration::ZERO {
1219 let wait = {
1224 let mut rng = rand::thread_rng();
1225 let factor = 1.0 + rng.gen_range(-MERKLE_RETRY_JITTER..=MERKLE_RETRY_JITTER);
1226 backoff.mul_f64(factor)
1227 };
1228 tokio::time::sleep(wait).await;
1229 }
1230 } else {
1231 outcome.failed = next_failed.len();
1232 outcome.failed_addresses = next_failed;
1233 break;
1234 }
1235 }
1236
1237 Ok(outcome)
1238}
1239
1240pub(crate) const DEFERRED_ROUND_DELAYS_SECS: [u64; 3] = [0, 15, 45];
1249
1250pub(crate) fn deferred_round_histogram_slot(round: usize, hist_len: usize) -> usize {
1257 (round + 1).min(hist_len.saturating_sub(1))
1258}
1259
1260#[derive(Debug, Default)]
1262pub(crate) struct DeferredRetryOutcome {
1263 pub stored: usize,
1267 pub stored_addresses: Vec<[u8; 32]>,
1270 pub failed: usize,
1272 pub failed_addresses: Vec<([u8; 32], String)>,
1276 pub fatal: Option<String>,
1280 pub stats: crate::data::client::batch::WaveAggregateStats,
1283}
1284
1285#[allow(clippy::too_many_arguments)]
1303pub(crate) async fn merkle_deferred_retry<CF, SF, Fut>(
1304 deferred: Vec<([u8; 32], String)>,
1305 round_delays_secs: &[u64],
1306 concurrency_for: CF,
1307 progress: Option<&mpsc::Sender<UploadEvent>>,
1308 stored_offset: usize,
1309 total: usize,
1310 store_one: SF,
1311) -> Result<DeferredRetryOutcome>
1312where
1313 CF: Fn(usize) -> usize,
1314 SF: Fn([u8; 32]) -> Fut,
1315 Fut: std::future::Future<Output = Result<std::time::Instant>>,
1316{
1317 let mut outcome = DeferredRetryOutcome {
1318 stored: stored_offset,
1319 ..DeferredRetryOutcome::default()
1320 };
1321 let mut remaining = deferred;
1322 let rounds = round_delays_secs.len();
1323
1324 for (round, &delay_secs) in round_delays_secs.iter().enumerate() {
1325 if remaining.is_empty() {
1326 break;
1327 }
1328 if delay_secs > 0 {
1329 tokio::time::sleep(Duration::from_secs(delay_secs)).await;
1330 }
1331 info!(
1332 "Deferred merkle retry round {}/{}: {} chunk(s) short of quorum",
1333 round + 1,
1334 rounds,
1335 remaining.len(),
1336 );
1337
1338 let slot = deferred_round_histogram_slot(round, outcome.stats.retries_histogram.len());
1342 let round_addrs: Vec<[u8; 32]> = std::mem::take(&mut remaining)
1343 .into_iter()
1344 .map(|(addr, _msg)| addr)
1345 .collect();
1346 let round_len = round_addrs.len();
1347 let cap = || concurrency_for(round_len);
1350
1351 let round_outcome = merkle_store_with_retry(
1352 round_addrs,
1353 cap,
1354 1,
1355 Duration::ZERO,
1356 progress,
1357 outcome.stored,
1358 total,
1359 &store_one,
1360 )
1361 .await?;
1362
1363 outcome.stored = round_outcome.stored;
1364 outcome
1365 .stored_addresses
1366 .extend(round_outcome.stored_addresses);
1367
1368 outcome.stats.chunk_attempts_total = outcome
1370 .stats
1371 .chunk_attempts_total
1372 .saturating_add(round_outcome.stats.chunk_attempts_total);
1373 outcome
1374 .stats
1375 .store_durations_ms
1376 .extend(round_outcome.stats.store_durations_ms);
1377 let landed: usize = round_outcome.stats.retries_histogram.iter().sum();
1378 outcome.stats.retries_histogram[slot] =
1379 outcome.stats.retries_histogram[slot].saturating_add(landed);
1380
1381 if let Some(fatal) = round_outcome.fatal {
1382 outcome.fatal = Some(fatal.to_string());
1387 outcome.failed = round_outcome.failed_addresses.len();
1388 outcome.failed_addresses = round_outcome.failed_addresses;
1389 return Ok(outcome);
1390 }
1391
1392 remaining = round_outcome.failed_addresses;
1394 }
1395
1396 outcome.failed = remaining.len();
1397 outcome.failed_addresses = remaining;
1398 Ok(outcome)
1399}
1400
1401pub fn finalize_merkle_batch(
1406 prepared: PreparedMerkleBatch,
1407 winner_pool_hash: [u8; 32],
1408) -> Result<MerkleBatchPaymentResult> {
1409 let chunk_count = prepared.addresses.len();
1410 let xornames: Vec<XorName> = prepared.addresses.iter().map(|a| XorName(*a)).collect();
1411
1412 let winner_pool = prepared
1414 .candidate_pools
1415 .iter()
1416 .find(|pool| pool.hash() == winner_pool_hash)
1417 .ok_or_else(|| {
1418 Error::Payment(format!(
1419 "Winner pool {} not found in candidate pools",
1420 hex::encode(winner_pool_hash)
1421 ))
1422 })?;
1423
1424 info!("Generating merkle proofs for {chunk_count} chunks");
1435 let mut proofs = HashMap::with_capacity(chunk_count);
1436
1437 for (i, xorname) in xornames.iter().enumerate() {
1438 let address_proof = prepared
1439 .tree
1440 .generate_address_proof(i, *xorname)
1441 .map_err(|e| {
1442 Error::Payment(format!(
1443 "Failed to generate address proof for chunk {i}: {e}"
1444 ))
1445 })?;
1446
1447 let merkle_proof = MerklePaymentProof::new(*xorname, address_proof, winner_pool.clone());
1448
1449 let tagged_bytes = serialize_merkle_proof(&merkle_proof)
1450 .map_err(|e| Error::Serialization(format!("Failed to serialize merkle proof: {e}")))?;
1451
1452 proofs.insert(prepared.addresses[i], tagged_bytes);
1453 }
1454
1455 info!("Merkle batch payment complete: {chunk_count} proofs generated");
1456
1457 Ok(MerkleBatchPaymentResult {
1458 proofs,
1459 chunk_count,
1460 storage_cost_atto: "0".to_string(),
1461 gas_cost_wei: 0,
1462 merkle_payment_timestamp: prepared.merkle_payment_timestamp,
1463 })
1464}
1465
1466#[cfg(test)]
1468mod send_assertions {
1469 use super::*;
1470 use crate::data::client::Client;
1471
1472 fn _assert_send<T: Send>(_: &T) {}
1473
1474 #[allow(
1475 dead_code,
1476 unreachable_code,
1477 unused_variables,
1478 clippy::diverging_sub_expression
1479 )]
1480 async fn _merkle_upload_chunks_is_send(client: &Client) {
1481 let batch_result: MerkleBatchPaymentResult = todo!();
1482 let fut = client.merkle_upload_chunks(Vec::new(), Vec::new(), &batch_result, None, 0, 0);
1483 _assert_send(&fut);
1484 }
1485}
1486
1487#[cfg(test)]
1488#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
1489mod tests {
1490 use super::*;
1491 use ant_protocol::evm::{Amount, MerkleTree, RewardsAddress, CANDIDATES_PER_POOL};
1492
1493 #[test]
1498 fn test_auto_below_threshold() {
1499 assert!(!should_use_merkle(1, PaymentMode::Auto));
1500 assert!(!should_use_merkle(10, PaymentMode::Auto));
1501 assert!(!should_use_merkle(63, PaymentMode::Auto));
1502 }
1503
1504 #[test]
1505 fn test_auto_at_and_above_threshold() {
1506 assert!(should_use_merkle(64, PaymentMode::Auto));
1507 assert!(should_use_merkle(65, PaymentMode::Auto));
1508 assert!(should_use_merkle(1000, PaymentMode::Auto));
1509 }
1510
1511 #[test]
1512 fn test_merkle_mode_forces_at_2() {
1513 assert!(!should_use_merkle(1, PaymentMode::Merkle));
1514 assert!(should_use_merkle(2, PaymentMode::Merkle));
1515 assert!(should_use_merkle(3, PaymentMode::Merkle));
1516 }
1517
1518 #[test]
1519 fn test_single_mode_always_false() {
1520 assert!(!should_use_merkle(0, PaymentMode::Single));
1521 assert!(!should_use_merkle(64, PaymentMode::Single));
1522 assert!(!should_use_merkle(1000, PaymentMode::Single));
1523 }
1524
1525 #[test]
1526 fn test_default_mode_is_auto() {
1527 assert_eq!(PaymentMode::default(), PaymentMode::Auto);
1528 }
1529
1530 #[test]
1531 fn test_threshold_value() {
1532 assert_eq!(DEFAULT_MERKLE_THRESHOLD, 64);
1533 }
1534
1535 #[test]
1540 fn test_preflight_quotes_gathered_means_not_stored() {
1541 assert!(matches!(preflight_stored_status(Ok(())), Ok(false)));
1542 }
1543
1544 #[test]
1545 fn test_preflight_already_stored_is_stored() {
1546 let r: Result<()> = Err(Error::AlreadyStored);
1547 assert!(matches!(preflight_stored_status(r), Ok(true)));
1548 }
1549
1550 #[test]
1554 fn test_preflight_transient_quote_failure_does_not_abort() {
1555 let insufficient: Result<()> =
1557 Err(Error::InsufficientPeers("Got 5 quotes, need 7".to_string()));
1558 assert!(
1559 matches!(preflight_stored_status(insufficient), Ok(false)),
1560 "insufficient-peers during preflight must degrade to not-stored, not error"
1561 );
1562
1563 let timeout: Result<()> = Err(Error::Timeout("Timeout waiting for quote".to_string()));
1564 assert!(matches!(preflight_stored_status(timeout), Ok(false)));
1565
1566 let network: Result<()> = Err(Error::Network("connection reset".to_string()));
1567 assert!(matches!(preflight_stored_status(network), Ok(false)));
1568 }
1569
1570 #[test]
1573 fn test_preflight_application_error_propagates() {
1574 let payment: Result<()> = Err(Error::Payment("bad payment".to_string()));
1575 assert!(matches!(
1576 preflight_stored_status(payment),
1577 Err(Error::Payment(_))
1578 ));
1579 }
1580
1581 #[test]
1582 fn chunk_contents_for_upload_addresses_preserves_requested_order() {
1583 let first = Bytes::from_static(b"first");
1584 let second = Bytes::from_static(b"second");
1585 let first_addr = compute_address(&first);
1586 let second_addr = compute_address(&second);
1587
1588 let selected = chunk_contents_for_upload_addresses(
1589 vec![first.clone(), second.clone()],
1590 &[second_addr, first_addr],
1591 )
1592 .unwrap();
1593
1594 assert_eq!(selected, vec![second, first]);
1595 }
1596
1597 #[test]
1598 fn chunk_contents_for_upload_addresses_preserves_duplicate_requests() {
1599 let repeated = Bytes::from_static(b"same-content");
1600 let other = Bytes::from_static(b"other-content");
1601 let repeated_addr = compute_address(&repeated);
1602
1603 let selected = chunk_contents_for_upload_addresses(
1604 vec![repeated.clone(), other, repeated.clone()],
1605 &[repeated_addr, repeated_addr],
1606 )
1607 .unwrap();
1608
1609 assert_eq!(selected, vec![repeated.clone(), repeated]);
1610 }
1611
1612 #[test]
1613 fn chunk_contents_for_upload_addresses_ignores_unrequested_duplicates() {
1614 let requested = Bytes::from_static(b"requested-content");
1615 let unrequested = Bytes::from_static(b"unrequested-content");
1616 let requested_addr = compute_address(&requested);
1617
1618 let selected = chunk_contents_for_upload_addresses(
1619 vec![
1620 unrequested.clone(),
1621 requested.clone(),
1622 unrequested.clone(),
1623 unrequested,
1624 ],
1625 &[requested_addr],
1626 )
1627 .unwrap();
1628
1629 assert_eq!(selected, vec![requested]);
1630 }
1631
1632 #[test]
1633 fn chunk_contents_for_upload_addresses_errors_for_missing_content() {
1634 let present = Bytes::from_static(b"present-content");
1635 let missing = Bytes::from_static(b"missing-content");
1636 let missing_addr = compute_address(&missing);
1637
1638 let result = chunk_contents_for_upload_addresses(vec![present], &[missing_addr]);
1639
1640 assert!(matches!(result, Err(Error::InvalidData(_))));
1641 }
1642
1643 fn make_test_addresses(count: usize) -> Vec<[u8; 32]> {
1648 (0..count)
1649 .map(|i| {
1650 let xn = XorName::from_content(&i.to_le_bytes());
1651 xn.0
1652 })
1653 .collect()
1654 }
1655
1656 #[test]
1657 fn test_tree_depth_for_known_sizes() {
1658 let cases = [(2, 1), (4, 2), (16, 4), (100, 7), (256, 8)];
1659 for (count, expected_depth) in cases {
1660 let addrs = make_test_addresses(count);
1661 let xornames: Vec<XorName> = addrs.iter().map(|a| XorName(*a)).collect();
1662 let tree = MerkleTree::from_xornames(xornames).unwrap();
1663 assert_eq!(
1664 tree.depth(),
1665 expected_depth,
1666 "depth mismatch for {count} leaves"
1667 );
1668 }
1669 }
1670
1671 #[test]
1672 fn test_proof_generation_and_verification_for_all_leaves() {
1673 let addrs = make_test_addresses(16);
1674 let xornames: Vec<XorName> = addrs.iter().map(|a| XorName(*a)).collect();
1675 let tree = MerkleTree::from_xornames(xornames.clone()).unwrap();
1676
1677 for (i, xn) in xornames.iter().enumerate() {
1678 let proof = tree.generate_address_proof(i, *xn).unwrap();
1679 assert!(proof.verify(), "proof for leaf {i} should verify");
1680 assert_eq!(proof.depth(), tree.depth() as usize);
1681 }
1682 }
1683
1684 #[test]
1685 fn test_proof_fails_for_wrong_address() {
1686 let addrs = make_test_addresses(8);
1687 let xornames: Vec<XorName> = addrs.iter().map(|a| XorName(*a)).collect();
1688 let tree = MerkleTree::from_xornames(xornames).unwrap();
1689
1690 let wrong = XorName::from_content(b"wrong");
1691 let proof = tree.generate_address_proof(0, wrong).unwrap();
1692 assert!(!proof.verify(), "proof with wrong address should fail");
1693 }
1694
1695 #[test]
1696 fn test_tree_too_few_leaves() {
1697 let xornames = vec![XorName::from_content(b"only_one")];
1698 let result = MerkleTree::from_xornames(xornames);
1699 assert!(result.is_err());
1700 }
1701
1702 #[test]
1703 fn test_tree_at_max_leaves() {
1704 let addrs = make_test_addresses(MAX_LEAVES);
1705 let xornames: Vec<XorName> = addrs.iter().map(|a| XorName(*a)).collect();
1706 let tree = MerkleTree::from_xornames(xornames).unwrap();
1707 assert_eq!(tree.leaf_count(), MAX_LEAVES);
1708 }
1709
1710 #[test]
1715 fn test_merkle_proof_serialize_deserialize_roundtrip() {
1716 use ant_protocol::evm::{Amount, MerklePaymentCandidateNode, RewardsAddress};
1717 use ant_protocol::payment::{deserialize_merkle_proof, serialize_merkle_proof};
1718
1719 let addrs = make_test_addresses(4);
1720 let xornames: Vec<XorName> = addrs.iter().map(|a| XorName(*a)).collect();
1721 let tree = MerkleTree::from_xornames(xornames.clone()).unwrap();
1722
1723 let timestamp = std::time::SystemTime::now()
1724 .duration_since(std::time::UNIX_EPOCH)
1725 .unwrap()
1726 .as_secs();
1727
1728 let candidates = tree.reward_candidates(timestamp).unwrap();
1729 let midpoint = candidates.first().unwrap().clone();
1730
1731 #[allow(clippy::cast_possible_truncation)]
1733 let candidate_nodes: [MerklePaymentCandidateNode; CANDIDATES_PER_POOL] =
1734 std::array::from_fn(|i| MerklePaymentCandidateNode {
1735 pub_key: vec![i as u8; 32],
1736 price: Amount::from(1024u64),
1737 reward_address: RewardsAddress::new([i as u8; 20]),
1738 merkle_payment_timestamp: timestamp,
1739 signature: vec![i as u8; 64],
1740 committed_key_count: 0,
1741 commitment_pin: None,
1742 });
1743
1744 let pool = MerklePaymentCandidatePool {
1745 midpoint_proof: midpoint,
1746 candidate_nodes,
1747 };
1748
1749 let address_proof = tree.generate_address_proof(0, xornames[0]).unwrap();
1750 let merkle_proof = MerklePaymentProof::new(xornames[0], address_proof, pool);
1751
1752 let tagged = serialize_merkle_proof(&merkle_proof).unwrap();
1753 assert_eq!(
1754 tagged.first().copied(),
1755 Some(0x02),
1756 "tag should be PROOF_TAG_MERKLE"
1757 );
1758
1759 let deserialized = deserialize_merkle_proof(&tagged).unwrap();
1760 assert_eq!(deserialized.address, merkle_proof.address);
1761 assert_eq!(
1762 deserialized.winner_pool.candidate_nodes.len(),
1763 CANDIDATES_PER_POOL
1764 );
1765 }
1766
1767 #[test]
1772 fn test_candidate_wrong_timestamp_rejected() {
1773 let candidate = MerklePaymentCandidateNode {
1775 pub_key: vec![0u8; 32],
1776 price: ant_protocol::evm::Amount::ZERO,
1777 reward_address: ant_protocol::evm::RewardsAddress::new([0u8; 20]),
1778 merkle_payment_timestamp: 1000,
1779 signature: vec![0u8; 64],
1780 committed_key_count: 0,
1781 commitment_pin: None,
1782 };
1783
1784 assert_ne!(candidate.merkle_payment_timestamp, 2000);
1786 }
1787
1788 fn make_dummy_candidate_nodes(
1793 timestamp: u64,
1794 ) -> [MerklePaymentCandidateNode; CANDIDATES_PER_POOL] {
1795 std::array::from_fn(|i| MerklePaymentCandidateNode {
1796 pub_key: vec![i as u8; 32],
1797 price: Amount::from(1024u64),
1798 reward_address: RewardsAddress::new([i as u8; 20]),
1799 merkle_payment_timestamp: timestamp,
1800 signature: vec![i as u8; 64],
1801 committed_key_count: 0,
1802 commitment_pin: None,
1803 })
1804 }
1805
1806 fn make_prepared_merkle_batch(count: usize) -> PreparedMerkleBatch {
1807 let addrs = make_test_addresses(count);
1808 let xornames: Vec<XorName> = addrs.iter().map(|a| XorName(*a)).collect();
1809 let tree = MerkleTree::from_xornames(xornames).unwrap();
1810
1811 let timestamp = std::time::SystemTime::now()
1812 .duration_since(std::time::UNIX_EPOCH)
1813 .unwrap()
1814 .as_secs();
1815
1816 let midpoints = tree.reward_candidates(timestamp).unwrap();
1817
1818 let candidate_pools: Vec<MerklePaymentCandidatePool> = midpoints
1819 .into_iter()
1820 .map(|mp| MerklePaymentCandidatePool {
1821 midpoint_proof: mp,
1822 candidate_nodes: make_dummy_candidate_nodes(timestamp),
1823 })
1824 .collect();
1825
1826 let pool_commitments = candidate_pools
1827 .iter()
1828 .map(MerklePaymentCandidatePool::to_commitment)
1829 .collect();
1830
1831 PreparedMerkleBatch {
1832 depth: tree.depth(),
1833 pool_commitments,
1834 merkle_payment_timestamp: timestamp,
1835 candidate_pools,
1836 tree,
1837 addresses: addrs,
1838 }
1839 }
1840
1841 #[test]
1842 fn test_finalize_merkle_batch_with_valid_winner() {
1843 let prepared = make_prepared_merkle_batch(4);
1844 let winner_hash = prepared.candidate_pools[0].hash();
1845
1846 let result = finalize_merkle_batch(prepared, winner_hash);
1847 assert!(
1848 result.is_ok(),
1849 "should succeed with valid winner: {result:?}"
1850 );
1851
1852 let batch = result.unwrap();
1853 assert_eq!(batch.chunk_count, 4);
1854 assert_eq!(batch.proofs.len(), 4);
1855
1856 for proof_bytes in batch.proofs.values() {
1858 assert!(!proof_bytes.is_empty());
1859 }
1860 }
1861
1862 #[test]
1870 fn test_finalize_merkle_batch_ships_no_commitment_sidecars() {
1871 use ant_protocol::payment::deserialize_merkle_proof;
1872
1873 let mut prepared = make_prepared_merkle_batch(4);
1874 for pool in &mut prepared.candidate_pools {
1877 for candidate in &mut pool.candidate_nodes {
1878 candidate.committed_key_count = 9_000;
1879 candidate.commitment_pin = Some([7u8; 32]);
1880 }
1881 }
1882 let winner_hash = prepared.candidate_pools[0].hash();
1883
1884 let batch = finalize_merkle_batch(prepared, winner_hash).unwrap();
1885 assert_eq!(batch.proofs.len(), 4);
1886 for proof_bytes in batch.proofs.values() {
1887 let proof = deserialize_merkle_proof(proof_bytes).unwrap();
1888 assert!(
1889 proof.commitment_sidecars.is_empty(),
1890 "per-chunk merkle proofs must not ship commitment sidecars"
1891 );
1892 }
1893 }
1894
1895 #[test]
1896 fn test_finalize_merkle_batch_with_invalid_winner() {
1897 let prepared = make_prepared_merkle_batch(4);
1898 let bad_hash = [0xFF; 32];
1899
1900 let result = finalize_merkle_batch(prepared, bad_hash);
1901 assert!(result.is_err());
1902 let err = result.unwrap_err().to_string();
1903 assert!(err.contains("not found in candidate pools"), "got: {err}");
1904 }
1905
1906 #[test]
1907 fn test_finalize_merkle_batch_proofs_are_deserializable() {
1908 use ant_protocol::payment::deserialize_merkle_proof;
1909
1910 let prepared = make_prepared_merkle_batch(8);
1911 let winner_hash = prepared.candidate_pools[0].hash();
1912
1913 let batch = finalize_merkle_batch(prepared, winner_hash).unwrap();
1914
1915 for (addr, proof_bytes) in &batch.proofs {
1916 let proof = deserialize_merkle_proof(proof_bytes);
1917 assert!(
1918 proof.is_ok(),
1919 "proof for {} should deserialize: {:?}",
1920 hex::encode(addr),
1921 proof.err()
1922 );
1923 }
1924 }
1925
1926 #[test]
1931 fn test_batch_split_calculation() {
1932 let addrs = make_test_addresses(MAX_LEAVES);
1934 assert_eq!(addrs.chunks(MAX_LEAVES).count(), 1);
1935
1936 let addrs = make_test_addresses(MAX_LEAVES + 1);
1938 assert_eq!(addrs.chunks(MAX_LEAVES).count(), 2);
1939
1940 let addrs = make_test_addresses(3 * MAX_LEAVES);
1942 assert_eq!(addrs.chunks(MAX_LEAVES).count(), 3);
1943 }
1944
1945 use std::sync::{Arc, Mutex};
1950
1951 fn make_addrs(count: usize) -> Vec<[u8; 32]> {
1954 make_test_addresses(count)
1955 }
1956
1957 #[tokio::test]
1961 async fn store_with_retry_collects_failures_instead_of_aborting() {
1962 let chunks = make_addrs(6);
1963 let failing: std::collections::HashSet<[u8; 32]> = chunks.iter().take(2).copied().collect();
1964 let failing_for_closure = failing.clone();
1965
1966 let store_one = move |addr: [u8; 32]| {
1967 let fail = failing_for_closure.contains(&addr);
1968 async move {
1969 if fail {
1970 Err(Error::InsufficientPeers("test shortfall".into()))
1971 } else {
1972 Ok(std::time::Instant::now())
1973 }
1974 }
1975 };
1976
1977 let outcome =
1978 merkle_store_with_retry(chunks, || 8, 1, Duration::ZERO, None, 0, 6, store_one)
1979 .await
1980 .expect("quorum shortfalls must not abort the batch");
1981
1982 assert_eq!(outcome.stored, 4);
1983 assert_eq!(outcome.failed, 2);
1984 assert_eq!(outcome.stats.retries_histogram[0], 4);
1986 assert_eq!(outcome.stats.chunk_attempts_total, 6);
1987 }
1988
1989 #[tokio::test]
1995 async fn store_with_retry_rereads_cap_per_slot() {
1996 let count = 6;
1997 let chunks = make_addrs(count);
1998 let cap_calls = Arc::new(Mutex::new(0usize));
1999 let cap_calls_for_closure = cap_calls.clone();
2000 let cap = move || {
2001 *cap_calls_for_closure.lock().expect("cap counter poisoned") += 1;
2002 2
2003 };
2004 let store_one = move |_addr: [u8; 32]| async move { Ok(std::time::Instant::now()) };
2005
2006 let outcome =
2007 merkle_store_with_retry(chunks, cap, 1, Duration::ZERO, None, 0, count, store_one)
2008 .await
2009 .expect("all stores succeed");
2010
2011 assert_eq!(outcome.stored, count);
2012 let calls = *cap_calls.lock().expect("cap counter poisoned");
2013 assert!(
2014 calls >= count,
2015 "cap must be re-read per drained slot (rolling), not snapshotted once — \
2016 expected >= {count} invocations, got {calls}",
2017 );
2018 }
2019
2020 #[tokio::test]
2027 async fn store_pass_has_no_barrier() {
2028 use std::sync::atomic::{AtomicUsize, Ordering};
2029 let count = 8;
2030 let addrs = make_addrs(count);
2031 let slow = addrs[0];
2032 let fast_completed = Arc::new(AtomicUsize::new(0));
2033 let release_slow = Arc::new(tokio::sync::Notify::new());
2034
2035 let store_one = move |addr: [u8; 32]| {
2036 let fast_completed = fast_completed.clone();
2037 let release_slow = release_slow.clone();
2038 async move {
2039 if addr == slow {
2040 release_slow.notified().await;
2044 } else if fast_completed.fetch_add(1, Ordering::SeqCst) + 1 == count - 1 {
2045 release_slow.notify_one();
2046 }
2047 Ok(std::time::Instant::now())
2048 }
2049 };
2050
2051 let outcome = tokio::time::timeout(
2052 Duration::from_secs(5),
2053 merkle_store_with_retry(addrs, || 8, 1, Duration::ZERO, None, 0, count, store_one),
2054 )
2055 .await
2056 .expect("store pass must not deadlock — a slow chunk must not block the others")
2057 .expect("all stores succeed");
2058
2059 assert_eq!(outcome.stored, count);
2060 }
2061
2062 #[tokio::test]
2067 async fn store_pass_keeps_at_most_cap_in_flight() {
2068 use std::sync::atomic::{AtomicUsize, Ordering};
2069 let count = 40;
2070 let cap = 4;
2071 let addrs = make_addrs(count);
2072 let in_flight = Arc::new(AtomicUsize::new(0));
2073 let max_in_flight = Arc::new(AtomicUsize::new(0));
2074 let max_in_flight_for_closure = max_in_flight.clone();
2075
2076 let store_one = move |_addr: [u8; 32]| {
2077 let in_flight = in_flight.clone();
2078 let max_in_flight = max_in_flight_for_closure.clone();
2079 async move {
2080 let now = in_flight.fetch_add(1, Ordering::SeqCst) + 1;
2081 max_in_flight.fetch_max(now, Ordering::SeqCst);
2082 tokio::task::yield_now().await;
2085 in_flight.fetch_sub(1, Ordering::SeqCst);
2086 Ok(std::time::Instant::now())
2087 }
2088 };
2089
2090 let outcome = merkle_store_with_retry(
2091 addrs,
2092 move || cap,
2093 1,
2094 Duration::ZERO,
2095 None,
2096 0,
2097 count,
2098 store_one,
2099 )
2100 .await
2101 .expect("all stores succeed");
2102
2103 assert_eq!(outcome.stored, count);
2104 let peak = max_in_flight.load(Ordering::SeqCst);
2105 assert!(
2106 peak <= cap,
2107 "at most `cap` bodies may be in flight (memory bound), got peak {peak} > cap {cap}",
2108 );
2109 assert!(
2110 peak > 1,
2111 "the pass must actually run concurrently, not serialize (peak {peak})",
2112 );
2113 }
2114
2115 #[tokio::test]
2120 async fn store_with_retry_treats_remote_put_as_recoverable() {
2121 let chunks = make_addrs(6);
2122 let failing: std::collections::HashSet<[u8; 32]> = chunks.iter().take(2).copied().collect();
2123 let failing_for_closure = failing.clone();
2124
2125 let store_one = move |addr: [u8; 32]| {
2126 let fail = failing_for_closure.contains(&addr);
2127 async move {
2128 if fail {
2129 Err(Error::RemotePut {
2130 address: hex::encode(addr),
2131 source: ant_protocol::ProtocolError::StorageFailed(
2132 "insufficient disk space".into(),
2133 ),
2134 })
2135 } else {
2136 Ok(std::time::Instant::now())
2137 }
2138 }
2139 };
2140
2141 let outcome =
2142 merkle_store_with_retry(chunks, || 8, 1, Duration::ZERO, None, 0, 6, store_one)
2143 .await
2144 .expect("remote app-rejections must not abort the batch");
2145
2146 assert_eq!(outcome.stored, 4);
2147 assert_eq!(outcome.failed, 2);
2148 }
2149
2150 #[tokio::test]
2154 async fn store_with_retry_reports_non_quorum_errors_as_fatal() {
2155 let chunks = make_addrs(3);
2156 let store_one = |_addr: [u8; 32]| async move {
2157 Err::<std::time::Instant, _>(Error::Payment("missing proof".into()))
2158 };
2159
2160 let outcome =
2161 merkle_store_with_retry(chunks, || 8, 3, Duration::ZERO, None, 0, 3, store_one)
2162 .await
2163 .expect("fatal is carried in the outcome, not returned as Err");
2164 assert!(matches!(outcome.fatal, Some(Error::Payment(_))));
2165 }
2166
2167 #[tokio::test]
2172 async fn store_with_retry_fatal_preserves_same_pass_successes() {
2173 let chunks = make_addrs(6);
2174 let bad = chunks[5];
2175 let store_one = move |addr: [u8; 32]| async move {
2176 if addr == bad {
2177 Err(Error::Payment("fatal".into()))
2178 } else {
2179 Ok(std::time::Instant::now())
2180 }
2181 };
2182
2183 let outcome =
2184 merkle_store_with_retry(chunks, || 1, 1, Duration::ZERO, None, 0, 6, store_one)
2185 .await
2186 .expect("fatal carried in outcome, not returned as Err");
2187 assert!(matches!(outcome.fatal, Some(Error::Payment(_))));
2188 assert_eq!(outcome.stored, 5);
2190 assert_eq!(outcome.stored_addresses.len(), 5);
2191 assert!(!outcome.stored_addresses.contains(&bad));
2192 assert!(outcome.failed_addresses.iter().any(|(a, _)| *a == bad));
2194 }
2195
2196 #[tokio::test]
2198 async fn store_with_retry_retries_only_the_failed_set() {
2199 let chunks = make_addrs(5);
2200 let total = chunks.len();
2201 let failing: std::collections::HashSet<[u8; 32]> = chunks.iter().take(2).copied().collect();
2202 let failing_for_closure = failing.clone();
2203
2204 let calls = Arc::new(Mutex::new(Vec::<[u8; 32]>::new()));
2206 let calls_for_closure = calls.clone();
2207
2208 let store_one = move |addr: [u8; 32]| {
2209 let calls = calls_for_closure.clone();
2210 let already_seen = calls.lock().unwrap().iter().filter(|&&a| a == addr).count();
2212 let fail = failing_for_closure.contains(&addr) && already_seen == 0;
2213 calls.lock().unwrap().push(addr);
2214 async move {
2215 if fail {
2216 Err(Error::InsufficientPeers("round-1 shortfall".into()))
2217 } else {
2218 Ok(std::time::Instant::now())
2219 }
2220 }
2221 };
2222
2223 let outcome =
2224 merkle_store_with_retry(chunks, || 8, 3, Duration::ZERO, None, 0, total, store_one)
2225 .await
2226 .expect("should converge after retry");
2227
2228 assert_eq!(outcome.stored, total);
2229 assert_eq!(outcome.failed, 0);
2230
2231 let calls = calls.lock().unwrap();
2235 assert_eq!(calls.len(), total + failing.len());
2236 let round_two: std::collections::HashSet<[u8; 32]> =
2237 calls[total..].iter().copied().collect();
2238 assert_eq!(round_two, failing);
2239 }
2240
2241 #[tokio::test]
2244 async fn store_with_retry_counts_retry_success_once_in_histogram() {
2245 let chunks = make_addrs(4);
2246 let total = chunks.len();
2247 let flaky_addr = chunks[0];
2248
2249 let attempts = Arc::new(Mutex::new(HashMap::<[u8; 32], usize>::new()));
2250 let attempts_for_closure = attempts.clone();
2251
2252 let store_one = move |addr: [u8; 32]| {
2253 let attempts = attempts_for_closure.clone();
2254 let n = {
2255 let mut m = attempts.lock().unwrap();
2256 let entry = m.entry(addr).or_insert(0);
2257 *entry += 1;
2258 *entry
2259 };
2260 let fail = addr == flaky_addr && n == 1;
2261 async move {
2262 if fail {
2263 Err(Error::InsufficientPeers("transient".into()))
2264 } else {
2265 Ok(std::time::Instant::now())
2266 }
2267 }
2268 };
2269
2270 let outcome =
2271 merkle_store_with_retry(chunks, || 8, 3, Duration::ZERO, None, 0, total, store_one)
2272 .await
2273 .expect("flaky chunk should recover on retry");
2274
2275 assert_eq!(outcome.stored, total);
2276 assert_eq!(outcome.failed, 0);
2277 assert_eq!(outcome.stats.retries_histogram[0], total - 1);
2279 assert_eq!(outcome.stats.retries_histogram[1], 1);
2280 assert_eq!(outcome.stats.chunk_attempts_total, total + 1);
2282 }
2283
2284 #[tokio::test]
2289 async fn store_with_retry_reports_all_failed_when_retries_exhausted() {
2290 let chunks = make_addrs(3);
2291 let total = chunks.len();
2292
2293 let store_one = |_addr: [u8; 32]| async move {
2294 Err::<std::time::Instant, _>(Error::InsufficientPeers("never converges".into()))
2295 };
2296
2297 let outcome = merkle_store_with_retry(
2298 chunks,
2299 || 8,
2300 MERKLE_STORE_MAX_ATTEMPTS,
2301 Duration::ZERO,
2302 None,
2303 0,
2304 total,
2305 store_one,
2306 )
2307 .await
2308 .expect("an exhausted retry budget is reported, not propagated as Err");
2309
2310 assert_eq!(outcome.stored, 0);
2311 assert_eq!(outcome.failed, total);
2312 assert_eq!(
2314 outcome.stats.chunk_attempts_total,
2315 total * MERKLE_STORE_MAX_ATTEMPTS
2316 );
2317 assert_eq!(outcome.stats.retries_histogram, [0; 4]);
2319 }
2320
2321 #[tokio::test]
2326 async fn store_with_retry_records_failed_addresses_when_exhausted() {
2327 let chunks = make_addrs(6);
2328 let failing: std::collections::HashSet<[u8; 32]> = chunks.iter().take(2).copied().collect();
2329 let failing_for_closure = failing.clone();
2330
2331 let store_one = move |addr: [u8; 32]| {
2332 let fail = failing_for_closure.contains(&addr);
2333 async move {
2334 if fail {
2335 Err(Error::InsufficientPeers("permanent shortfall".into()))
2336 } else {
2337 Ok(std::time::Instant::now())
2338 }
2339 }
2340 };
2341
2342 let outcome = merkle_store_with_retry(
2343 chunks,
2344 || 8,
2345 MERKLE_STORE_MAX_ATTEMPTS,
2346 Duration::ZERO,
2347 None,
2348 0,
2349 6,
2350 store_one,
2351 )
2352 .await
2353 .expect("quorum shortfalls must not abort the batch");
2354
2355 assert_eq!(outcome.stored, 4);
2356 assert_eq!(outcome.failed, 2);
2357 assert_eq!(outcome.failed_addresses.len(), 2);
2359 let reported: std::collections::HashSet<[u8; 32]> =
2360 outcome.failed_addresses.iter().map(|(a, _)| *a).collect();
2361 assert_eq!(reported, failing);
2362 for (_, msg) in &outcome.failed_addresses {
2364 assert!(msg.contains("permanent shortfall"));
2365 }
2366 }
2367
2368 #[tokio::test]
2371 async fn store_with_retry_failed_addresses_empty_on_full_success() {
2372 let chunks = make_addrs(4);
2373 let total = chunks.len();
2374 let store_one = |_addr: [u8; 32]| async move { Ok(std::time::Instant::now()) };
2375
2376 let outcome = merkle_store_with_retry(
2377 chunks,
2378 || 8,
2379 MERKLE_STORE_MAX_ATTEMPTS,
2380 Duration::ZERO,
2381 None,
2382 0,
2383 total,
2384 store_one,
2385 )
2386 .await
2387 .expect("all chunks store");
2388
2389 assert_eq!(outcome.stored, total);
2390 assert_eq!(outcome.failed, 0);
2391 assert!(outcome.failed_addresses.is_empty());
2392 }
2393
2394 #[test]
2401 fn deferred_round_histogram_slot_maps_and_clamps() {
2402 assert_eq!(deferred_round_histogram_slot(0, 4), 1);
2403 assert_eq!(deferred_round_histogram_slot(1, 4), 2);
2404 assert_eq!(deferred_round_histogram_slot(2, 4), 3);
2405 assert_eq!(deferred_round_histogram_slot(3, 4), 3);
2407 assert_eq!(deferred_round_histogram_slot(9, 4), 3);
2408 }
2409
2410 fn deferred_set(count: usize) -> Vec<([u8; 32], String)> {
2411 make_test_addresses(count)
2412 .into_iter()
2413 .map(|addr| (addr, "short of quorum".to_string()))
2414 .collect()
2415 }
2416
2417 #[tokio::test]
2421 async fn deferred_retry_succeeds_on_a_later_round() {
2422 let deferred = deferred_set(3);
2423 let attempts = Arc::new(Mutex::new(HashMap::<[u8; 32], usize>::new()));
2426 let attempts_for_closure = attempts.clone();
2427 let store_one = move |addr: [u8; 32]| {
2428 let attempts = attempts_for_closure.clone();
2429 async move {
2430 let n = {
2431 let mut map = attempts.lock().unwrap();
2432 let e = map.entry(addr).or_insert(0);
2433 *e += 1;
2434 *e
2435 };
2436 if n < 2 {
2437 Err(Error::InsufficientPeers("still short".into()))
2438 } else {
2439 Ok(std::time::Instant::now())
2440 }
2441 }
2442 };
2443
2444 let outcome = merkle_deferred_retry(
2445 deferred,
2446 &[0, 0, 0],
2447 |n: usize| n.max(1),
2448 None,
2449 0,
2450 3,
2451 store_one,
2452 )
2453 .await
2454 .expect("deferred retry must not abort on quorum shortfalls");
2455
2456 assert_eq!(outcome.stored, 3, "all three land by round 1");
2457 assert_eq!(outcome.stored_addresses.len(), 3);
2458 assert_eq!(outcome.failed, 0);
2459 assert!(outcome.failed_addresses.is_empty());
2460 assert!(outcome.fatal.is_none());
2461 assert_eq!(outcome.stats.retries_histogram[1], 0);
2463 assert_eq!(outcome.stats.retries_histogram[2], 3);
2464 assert_eq!(outcome.stats.chunk_attempts_total, 6);
2466 }
2467
2468 #[tokio::test]
2471 async fn deferred_retry_leftovers_become_failed() {
2472 let deferred = deferred_set(2);
2473 let store_one = |_addr: [u8; 32]| async move {
2474 Err::<std::time::Instant, _>(Error::InsufficientPeers("always short".into()))
2475 };
2476
2477 let outcome = merkle_deferred_retry(
2478 deferred,
2479 &[0, 0, 0],
2480 |n: usize| n.max(1),
2481 None,
2482 0,
2483 2,
2484 store_one,
2485 )
2486 .await
2487 .expect("exhausted retries report failures, not an error");
2488
2489 assert_eq!(outcome.stored, 0);
2490 assert!(outcome.stored_addresses.is_empty());
2491 assert_eq!(outcome.failed, 2);
2492 assert_eq!(outcome.failed_addresses.len(), 2);
2493 assert!(outcome.fatal.is_none());
2494 assert_eq!(outcome.stats.chunk_attempts_total, 6);
2496 }
2497
2498 #[tokio::test]
2503 async fn deferred_retry_fatal_error_preserves_prior_progress() {
2504 let addrs = make_test_addresses(2);
2505 let good = addrs[0];
2506 let bad = addrs[1];
2507 let deferred = vec![(good, "short".to_string()), (bad, "short".to_string())];
2508
2509 let attempts = Arc::new(Mutex::new(HashMap::<[u8; 32], usize>::new()));
2512 let attempts_for_closure = attempts.clone();
2513 let store_one = move |addr: [u8; 32]| {
2514 let attempts = attempts_for_closure.clone();
2515 async move {
2516 let n = {
2517 let mut map = attempts.lock().unwrap();
2518 let e = map.entry(addr).or_insert(0);
2519 *e += 1;
2520 *e
2521 };
2522 if addr == good {
2523 Ok(std::time::Instant::now())
2524 } else if n == 1 {
2525 Err(Error::InsufficientPeers("short".into()))
2526 } else {
2527 Err(Error::Payment("fatal on retry".into()))
2528 }
2529 }
2530 };
2531
2532 let outcome = merkle_deferred_retry(
2533 deferred,
2534 &[0, 0, 0],
2535 |n: usize| n.max(1),
2536 None,
2537 0,
2538 2,
2539 store_one,
2540 )
2541 .await
2542 .expect("a fatal round error is reported via `fatal`, not as Err");
2543
2544 assert!(outcome.fatal.is_some(), "fatal error must be captured");
2545 assert_eq!(outcome.stored, 1, "round-0 success preserved");
2546 assert_eq!(outcome.stored_addresses, vec![good]);
2547 assert_eq!(outcome.failed, 1);
2548 assert_eq!(outcome.failed_addresses.len(), 1);
2549 assert_eq!(outcome.failed_addresses[0].0, bad);
2550 }
2551
2552 #[tokio::test]
2554 async fn deferred_retry_empty_set_is_a_noop() {
2555 let store_one = |_addr: [u8; 32]| async move {
2556 Err::<std::time::Instant, _>(Error::InsufficientPeers("unused".into()))
2557 };
2558
2559 let outcome = merkle_deferred_retry(
2560 Vec::new(),
2561 &DEFERRED_ROUND_DELAYS_SECS,
2562 |n: usize| n.max(1),
2563 None,
2564 7,
2565 7,
2566 store_one,
2567 )
2568 .await
2569 .expect("empty deferred set is a no-op");
2570
2571 assert_eq!(outcome.stored, 7, "stored_offset carried through unchanged");
2572 assert_eq!(outcome.failed, 0);
2573 assert!(outcome.stored_addresses.is_empty());
2574 assert!(outcome.failed_addresses.is_empty());
2575 assert!(outcome.fatal.is_none());
2576 }
2577}