Skip to main content

ant_core/data/client/
merkle.rs

1//! Merkle batch payment support for the Autonomi client.
2//!
3//! When uploading batches of 64+ chunks, merkle payments reduce gas costs
4//! by paying for the entire batch in a single on-chain transaction instead
5//! of one transaction per chunk.
6
7use 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
40/// Default threshold: use merkle payments when chunk count >= this value.
41pub const DEFAULT_MERKLE_THRESHOLD: usize = 64;
42
43/// Payment multiplier applied to a quoted price before settlement.
44///
45/// Deliberately the **same constant** the single-node path uses rather than a
46/// second copy of `3`: the whole defect this fixes was the two paths disagreeing
47/// about the multiplier, so they now read it from one place. (The storer's
48/// `PAID_QUOTE_PAYMENT_MULTIPLIER` and `ant_protocol`'s single-node builder are
49/// still separate literals; folding all of them into one `ant-protocol`
50/// constant is a follow-up.)
51///
52/// The single-node path pays the median-priced issuer 3× its quoted price, so
53/// the network receives the same revenue as paying three members of the close
54/// group while costing one transaction's gas. The merkle path never applied it:
55/// it submitted the raw quoted price as the on-chain payable amount, so the
56/// contract's `median16(amount) × 2^depth` came to **1×** the median per padded
57/// leaf — a third of what the same chunk earns on the single-node path, for
58/// identical storage and replication.
59///
60/// The on-chain field is `CandidateNode.amount`, the sum the vault pays out,
61/// not the quote itself; the signed candidate keeps its 1× quoted `price` and
62/// the pool hash is unchanged, so every proof still verifies against the
63/// quotes the nodes actually signed.
64use crate::data::client::payment::SINGLE_NODE_PAYMENT_MULTIPLIER as MERKLE_PAYMENT_MULTIPLIER;
65
66/// ADR-0004 resolve-before-pay gate for a merkle candidate — the merkle-path
67/// equivalent of the single-node `quote_commitment_binding_is_valid`. Runs the
68/// FULL binding check (shape, cap, exact price, and for bound candidates the
69/// commitment parse, peer-binding, signature, `hash == pin`, and
70/// `count == key_count`) before the candidate is allowed into a pool the client
71/// may pay. `peer_id` is derived from the candidate's `pub_key`
72/// (`BLAKE3(pub_key)`), matching the storer.
73///
74/// Returns `Ok(())` if the binding fully resolves, else `Err(detail)`.
75fn 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(()); // baseline candidate pins nothing
108    };
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
140/// Build the on-chain [`PoolCommitment`] for a candidate pool, applying
141/// [`MERKLE_PAYMENT_MULTIPLIER`] to every candidate's payable amount.
142///
143/// The contract derives what it pays out from the amounts submitted here
144/// (`total = median16(amount) × 2^depth`, split evenly across `depth`
145/// winners), so multiplying here — and only here — brings the merkle path to
146/// the same per-chunk revenue as the single-node path.
147///
148/// The pool hash is deliberately left as `pool.hash()`, computed over the
149/// candidates' **signed** 1× prices. It is the key the storer resolves the
150/// on-chain payment record under, and it commits to the quotes the nodes
151/// actually signed; multiplying the payable amount must not disturb it.
152fn 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/// Turn a merkle candidate quote response into the candidate it carries, or
169/// the error that explains why there is none.
170///
171/// Shared by the versioned request and its legacy retry so the two cannot
172/// interpret the same response differently.
173///
174/// `ClientUpdateRequired` is lifted out of the generic protocol-error case
175/// deliberately. It is the one rejection that is about this client rather than
176/// about this request, it already carries wording aimed at the person running
177/// the upload, and it must not be retried in a shape that would get a quote
178/// anyway.
179/// Which failure a fully drained set of candidate pools should report.
180///
181/// Pools are drained rather than short-circuited, so more than one can fail.
182/// A refusal outranks an ordinary failure: if one pool runs out of peers while
183/// another declares this client unable to settle, the second is the answer that
184/// keeps the caller from falling back to a payment path and spending.
185#[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        // Validated, not taken at face value. A refusal is unauthenticated and
227        // two of them latch this client for its whole run, so the merkle path
228        // applies the same coherence check as the single-node one: the echoed
229        // version must be ours and the stated minimum must genuinely exceed it.
230        // Without it, two faulty or hostile candidates could deny every upload
231        // with a refusal that does not describe this client at all.
232        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
254/// Should this failure be retried as an unversioned request?
255///
256/// A storer built before the settlement version existed cannot decode the
257/// versioned request, so it never replies and the send fails at the transport
258/// layer. Any response this client *recognises*, refusal included, means the
259/// storer understood us, and retrying that in an older shape would defeat the
260/// gate.
261///
262/// A reply that decodes but carries a body the mapper does not recognise is
263/// not distinguished: the wait continues and ends in a timeout, so it takes
264/// the fallback too. That is no more than a peer could achieve by saying
265/// nothing, which is why it is bounded by the cutover guard rather than
266/// special-cased here.
267///
268/// # This is a downgrade path, and it is only safe because nothing can be
269/// refused yet
270///
271/// Silence is **not** proof that a peer cannot parse the request. A dropped
272/// response, packet loss, an overloaded peer, or one deliberately discarding
273/// versioned requests all look identical from here. So this predicate can be
274/// provoked, and a peer that provokes it gets asked again without a version.
275///
276/// That is harmless only while no client can be refused on version grounds,
277/// which is exactly the case while `MIN_SUPPORTED_SETTLEMENT_VERSION` is the
278/// first declarable version. The moment the minimum is raised, this becomes a
279/// way to obtain a quote the gate meant to withhold, and then to burn a
280/// payment against it.
281///
282/// The guard below turns "remember to delete the fallback before raising the
283/// minimum" from a comment into a build failure. It lives in
284/// [`crate::data::client::UNVERSIONED_RETRY_REQUIRES_MIN_V1`] and is referenced
285/// from every fallback site, including the independent single-node one in
286/// `quote.rs`, so deleting one path cannot silently leave the other unguarded.
287const _: () = 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/// Payment mode for uploads.
294#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
295#[serde(rename_all = "snake_case")]
296pub enum PaymentMode {
297    /// Automatically choose: merkle for batches >= threshold, single otherwise.
298    #[default]
299    Auto,
300    /// Force merkle batch payment regardless of batch size (min 2 chunks).
301    Merkle,
302    /// Force single-node payment (one tx per chunk).
303    Single,
304}
305
306/// Result of a merkle batch payment.
307///
308/// Serializable so it can be persisted across runs for resume after a
309/// partial-upload failure. See `crate::data::client::cached_merkle`.
310#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
311pub struct MerkleBatchPaymentResult {
312    /// Map of `XorName` to serialized tagged proof bytes (ready to use in PUT requests).
313    pub proofs: HashMap<[u8; 32], Vec<u8>>,
314    /// Number of chunks in the batch.
315    pub chunk_count: usize,
316    /// Total storage cost in atto (token smallest unit).
317    pub storage_cost_atto: String,
318    /// Total gas cost in wei.
319    pub gas_cost_wei: u128,
320    /// Unix timestamp (seconds) used for the on-chain merkle payment.
321    /// Persisted so resume can check whether the on-chain payment has
322    /// aged out beyond the merkle expiration window and the cached
323    /// receipt must be discarded.
324    #[serde(default)]
325    pub merkle_payment_timestamp: u64,
326}
327
328/// Prepared merkle batch ready for external payment.
329///
330/// Contains everything needed to submit the on-chain merkle payment
331/// and then finalize proof generation without a wallet.
332pub struct PreparedMerkleBatch {
333    /// Merkle tree depth (needed for the on-chain call).
334    pub depth: u8,
335    /// Pool commitments for the on-chain call.
336    pub pool_commitments: Vec<PoolCommitment>,
337    /// Timestamp used for the merkle payment.
338    pub merkle_payment_timestamp: u64,
339    /// Internal: candidate pools (needed for proof generation after payment).
340    candidate_pools: Vec<MerklePaymentCandidatePool>,
341    /// Internal: the merkle tree (needed for proof generation).
342    tree: MerkleTree,
343    /// Internal: chunk addresses in order.
344    addresses: Vec<[u8; 32]>,
345}
346
347/// Result of checking a merkle upload batch before payment.
348#[derive(Debug, Clone, Default)]
349pub(crate) struct MerkleUploadPlan {
350    /// Chunks already confirmed by their close group.
351    pub already_stored: Vec<[u8; 32]>,
352    /// Chunks that still need payment and storage.
353    pub to_upload: Vec<[u8; 32]>,
354    /// Total byte size of chunks in `to_upload`.
355    to_upload_total_bytes: u64,
356}
357
358impl MerkleUploadPlan {
359    /// Average byte size of chunks that still need upload.
360    #[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
382/// Select chunk contents that correspond to `addresses`, preserving address order.
383///
384/// Extra chunk contents are ignored; missing contents for any requested address
385/// are treated as corrupted upload state.
386pub(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
457/// Map a per-chunk quote-collection result to its merkle-preflight stored
458/// status, degrading gracefully on transient failures.
459///
460/// The preflight only needs to answer "is this chunk already on the network?"
461/// so it can be skipped before payment — it is an optimisation, never a gate.
462/// Therefore:
463/// - `Ok(_)` (quotes gathered) → `Ok(false)`: chunk is not stored, upload it.
464/// - `Err(AlreadyStored)` → `Ok(true)`: skip it.
465/// - a transient failure (timeout / insufficient peers / transport error) →
466///   `Ok(false)`: we could not confirm, so queue it for upload rather than
467///   aborting the whole batch. Re-storing an existing chunk is idempotent.
468/// - any other (application) error → propagate; it would recur on a healthy
469///   link and is not something the preflight should paper over.
470///
471/// Without this, a single chunk's transient quote timeout failed the entire
472/// upload — fatal for forced `--merkle` uploads, which (unlike `Auto`) have no
473/// wave-batch fallback, and catastrophic for large files via the multiplicative
474/// per-chunk effect.
475fn 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/// Split `total` addresses into the merkle batches an upload is actually paid in.
487///
488/// Every batch becomes one `MerkleTree`, and a tree is only valid with
489/// `2..=MAX_LEAVES` leaves. The obvious `addresses.chunks(MAX_LEAVES)` split
490/// respects the upper bound but not the lower one: 257 addresses come out as
491/// `[256, 1]`, the 256-address batch is paid for on-chain, and the singleton
492/// remainder cannot build a tree — so the upload fails as a *paid* partial.
493/// Every count congruent to 1 modulo `MAX_LEAVES` (257, 513, 769, …) hits it,
494/// and the merkle preflight can leave an arbitrary count behind.
495///
496/// Borrowing one address from the preceding batch removes the case entirely:
497/// 257 splits as `[255, 2]` and 513 as `[256, 255, 2]`. Order is preserved and
498/// no address is duplicated or synthesised, so the partition is a plain
499/// in-order cover of the input.
500///
501/// Returns an empty vector for `total < 2`, which no merkle path may pay for —
502/// `pay_for_merkle_batch` rejects those counts up front.
503#[must_use]
504pub fn merkle_batch_sizes(total: usize) -> Vec<usize> {
505    merkle_batch_sizes_with_cap(total, MAX_LEAVES)
506}
507
508/// [`merkle_batch_sizes`] with an explicit per-batch leaf cap.
509///
510/// `cap` is clamped to `3..=MAX_LEAVES`: above `MAX_LEAVES` the contract's
511/// depth bound rejects the tree, and below 3 the partition is unsound — with
512/// a cap of 2 every odd total needs a 1-leaf part, which cannot build a
513/// tree (parts of 3 and 2 compose any total ≥ 2, so 3 is the smallest safe
514/// cap). Production callers use [`merkle_batch_sizes`]
515/// (cap = `MAX_LEAVES`); a smaller cap lets tests exercise real multi-batch
516/// signing with kilobyte files (ADR-0003).
517#[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        // Taking a full cap here would strand a single address as the
528        // final batch; take one fewer so the tail is a payable two-leaf tree.
529        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/// Split `addresses` into the sub-batches [`merkle_batch_sizes`] describes.
538///
539/// The slices borrow `addresses` in order, so the partition cannot introduce a
540/// duplicate or a synthetic address.
541#[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/// [`merkle_batch_partitions`] under an explicit per-batch leaf cap
547/// (see [`merkle_batch_sizes_with_cap`] for the clamping rules).
548#[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/// Fold per-batch [`MerkleBatchPaymentResult`]s into one combined receipt.
561///
562/// Mirrors the fold `pay_for_merkle_multi_batch` performs on the wallet path:
563/// proofs merge by extension, costs sum, and the combined
564/// `merkle_payment_timestamp` is the **oldest** sub-batch timestamp so expiry
565/// checks use the worst case. Batches the external signer never paid simply
566/// do not appear in `results` — their chunks end up with no proof, which the
567/// store path reports through `PartialUpload` (ADR-0003).
568#[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
598/// Leaves one merkle batch of `batch_size` addresses is billed for.
599///
600/// `MerkleTree` pads its leaf count up to a power of two and the vault charges
601/// `median16 × 2^depth`, so a 65-address batch pays for 128 leaves.
602fn padded_leaf_count(batch_size: usize) -> u64 {
603    // Saturate rather than wrap: a batch is at most MAX_LEAVES, so the
604    // overflow arm is unreachable, and erring high never under-quotes.
605    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/// Leaves a merkle upload of `chunk_count` chunks is actually billed for.
613///
614/// Summed over the batches [`merkle_batch_sizes`] will really pay for, so the
615/// estimate and the execution path share one batching model rather than two
616/// that can drift. Billing the raw chunk count would under-quote a
617/// non-power-of-two batch by up to 2×, and an under-quote is the harmful
618/// direction: a caller sizing its wallet from the estimate runs dry
619/// mid-upload.
620#[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        // No payable partition exists below two chunks. Nothing costs nothing;
626        // a lone chunk is still quoted as the two-leaf minimum a tree needs.
627        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
636/// Reject an address set that cannot be prepared as a single merkle tree.
637///
638/// The wallet path splits an oversized upload with [`merkle_batch_sizes`] and
639/// pays each batch in its own transaction. The external-signer contract is one
640/// prepared batch → one signature → one payment, so it has no way to express
641/// that split. Refusing before any candidate collection or on-chain spend is
642/// the honest answer; silently switching the caller to a different payment
643/// model is not.
644fn 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/// Determine whether to use merkle payments for a given batch size.
655/// Free function — no Client needed.
656#[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    /// Determine whether to use merkle payments for a given batch size.
667    #[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    /// Pay for a batch of chunks using merkle batch payment.
673    ///
674    /// Builds a merkle tree, collects candidate pools, pays on-chain in one tx,
675    /// and returns per-chunk proofs. Anything longer than `MAX_LEAVES` is split
676    /// by [`merkle_batch_sizes`] and paid one transaction per sub-batch.
677    ///
678    /// This low-level helper assumes the caller has already selected the
679    /// addresses that need payment. User-facing upload paths first run the
680    /// merkle upload planner to skip chunks already stored on the network.
681    ///
682    /// # Errors
683    ///
684    /// Returns an error if the batch is too small, candidate collection fails,
685    /// on-chain payment fails, or proof generation fails.
686    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        // A refusal established by any earlier upload on this client stops
693        // this one before it spends. The verdict is about this build, not
694        // about one operation, so an upload that started after another had
695        // already been told the settlement rules are wrong must not pay.
696        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    /// Check which chunks in a merkle upload still need payment/storage.
717    ///
718    /// Uses the normal per-chunk quote path because it already has the
719    /// close-group majority rule for `AlreadyStored`. Non-stored chunks only
720    /// use the quote response as a probe; their actual payment still happens
721    /// through the merkle batch.
722    ///
723    /// `chunks` contains `(address, data_size)` pairs.
724    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    /// Phase 1 of external-signer merkle payment for an address set of any
844    /// size: partition into `MerkleTree`-sized sub-batches and prepare each.
845    ///
846    /// The partition follows [`merkle_batch_partitions_with_cap`] (≤ `cap`
847    /// leaves per batch, singleton-remainder rebalanced), so the external
848    /// signer pays one transaction per returned batch — the same shape the
849    /// wallet path's `pay_for_merkle_multi_batch` signs internally
850    /// (ADR-0003). Batch order matches address order; the caller's finalize
851    /// supplies one winner hash per batch in the same order.
852    ///
853    /// `cap` is clamped to `3..=MAX_LEAVES` (see
854    /// [`merkle_batch_sizes_with_cap`] for why 3 is the floor); production
855    /// callers pass `MAX_LEAVES`, tests pass small caps to get real
856    /// multi-batch flows from kilobyte files.
857    ///
858    /// # Errors
859    ///
860    /// Returns an error if any sub-batch's candidate collection fails.
861    /// Nothing is spent in either case — payment happens externally after
862    /// this returns.
863    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    /// Phase 1 of external-signer merkle payment: prepare batch without paying.
893    ///
894    /// Builds the merkle tree, collects candidate pools from the network,
895    /// and returns the data needed for the on-chain payment call.
896    /// Requires `EvmNetwork` but NOT a wallet.
897    ///
898    /// # Errors
899    ///
900    /// Returns [`Error::MerkleBatchTooLarge`] if `addresses` holds more than
901    /// `MAX_LEAVES` entries. One prepared batch is one signature and one
902    /// payment, so an oversized set has no valid external-signing form; the
903    /// wallet path splits it across transactions instead
904    /// ([`Client::prepare_merkle_batches_external`] is the partitioned
905    /// equivalent for external signing). The check runs before any candidate
906    /// collection, so nothing is spent.
907    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        // A refusal established by any earlier upload on this client stops
914        // this one before it spends. The verdict is about this build, not
915        // about one operation, so an upload that started after another had
916        // already been told the settlement rules are wrong must not pay.
917        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        // 1. Build merkle tree
928        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        // 2. Get reward candidates (midpoint proofs)
940        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        // 3. Collect candidate pools from the network (all pools in parallel).
950        //    Each candidate's ADR-0004 binding is fully verified during
951        //    collection (shape, cap, exact price, commitment resolution); the
952        //    sidecars themselves are consumed by that validation and NOT
953        //    forwarded in the PUT bundles (see `finalize_merkle_batch`).
954        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        // 4. Build pool commitments for on-chain payment. Every candidate's
964        //    payable amount carries MERKLE_PAYMENT_MULTIPLIER so a merkle
965        //    chunk settles for the same amount a single-node chunk does; the
966        //    signed candidate prices and the pool hashes are untouched.
967        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    /// Pay for a single batch (up to `MAX_LEAVES` chunks).
983    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    /// Handle batches larger than `MAX_LEAVES` by splitting into sub-batches.
1019    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        // Partition with the shared helper, NOT `chunks(MAX_LEAVES)`: the naive
1026        // split leaves a one-address final batch for every count congruent to 1
1027        // modulo MAX_LEAVES, which cannot build a tree and so turns a paid
1028        // upload into a partial failure.
1029        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        // Track the oldest sub-batch timestamp so the overall receipt
1035        // expires when the *first* sub-batch's on-chain payment ages
1036        // out (worst case for resume).
1037        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                        // First sub-batch failed, nothing paid yet -- propagate directly.
1060                        return Err(e);
1061                    }
1062                    // A storer saying this client cannot settle is terminal
1063                    // even mid-batch. Folding it into a partial result would
1064                    // report success, hide the upgrade instruction, and leave
1065                    // the caller to rediscover the same refusal on its next
1066                    // upload. Every remaining sub-batch would be refused the
1067                    // same way, so there is nothing to salvage by continuing.
1068                    if matches!(e, Error::ClientUpdateRequired(_)) {
1069                        // Do NOT return Err here. The caller writes the receipt
1070                        // cache only on the Ok path, so failing the call would
1071                        // discard proofs for sub-batches whose payment has
1072                        // already settled on-chain and cannot be undone. That is
1073                        // the very destruction this work exists to stop.
1074                        //
1075                        // The verdict is not lost by returning the partial
1076                        // result: it was latched client-wide when it was
1077                        // corroborated, so the next quote collection refuses
1078                        // before spending anything and the upgrade instruction
1079                        // reaches the user from there.
1080                        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                    // Return partial result so caller can still store already-paid chunks.
1090                    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    /// Build candidate pools by querying the network for each midpoint (concurrently).
1117    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        // Drained rather than short-circuited. Returning on the first error
1146        // would drop `pool_futures`, cancelling every pool still in flight, and
1147        // one of those may be carrying the second refusal that corroborates a
1148        // verdict about this client. A refusal lost that way is a refusal the
1149        // latch never sees, and the caller can fall back to wave payment and
1150        // spend. This is the same rule the single-node collector follows: stop
1151        // making progress, but never drop a verdict that is already in flight.
1152        //
1153        // A refusal outranks an ordinary failure for the same reason. If one
1154        // pool runs out of peers while another declares this client unable to
1155        // settle, the second is the answer worth returning.
1156        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    /// Collect `CANDIDATES_PER_POOL` (16) merkle candidate quotes from the network.
1172    #[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        // Query extra peers to handle validation failures (bad sigs, wrong type, etc.)
1184        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 DHT closest-nodes didn't return enough, supplement with connected peers.
1191        // On small networks the DHT iterative lookup may not discover enough peers
1192        // close to a random pool address, but we know more peers via direct connections.
1193        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            // A peer that already failed to answer a versioned request is
1218            // asked in the legacy shape directly. Re-probing costs a full
1219            // per-peer timeout every time, and a merkle pool asks sixteen
1220            // candidates per pool, so against a fleet that predates the
1221            // versioned requests the probes dominate the run: measured on this
1222            // suite it went from ~24 minutes to past the 60-minute CI cap.
1223            // The capable set wins. The two sets are updated under separate
1224            // locks, so a slow probe can insert into the legacy set after a
1225            // concurrent request has already proved the peer capable; letting
1226            // capability win makes that interleaving harmless instead of
1227            // permanently preferring the legacy shape.
1228            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            // Declare the settlement version so a storer can turn us away
1243            // before we pay, rather than refusing the payment afterwards.
1244            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            // Fallback for the mixed fleet. A storer that predates the
1268            // versioned request cannot decode it and simply never answers, so
1269            // without this every quote would fail until the whole network had
1270            // upgraded. Retried only on a transport-level failure, never on a
1271            // refusal: see `is_version_unaware` below.
1272            //
1273            // Delete this, and the second request id, once the fleet is known
1274            // to answer V2.
1275            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                // First contact waits only long enough to learn whether the
1297                // peer can parse the shape. A peer already known to be legacy
1298                // gets the caller's full patience, because that request is the
1299                // real one rather than a probe.
1300                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                // Any answer at all to the versioned shape proves the peer
1328                // can parse it, so it can never later be demoted.
1329                // Any recognised answer proves the peer parsed the versioned
1330                // shape, including a structured error. Only silence and send
1331                // failures leave the question open.
1332                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                // Silence from a storer means it could not decode the
1343                // versioned request, so ask again in the shape it understands.
1344                // A storer that answered with a refusal is NOT retried: it
1345                // understood us and said no, and asking again without the
1346                // version would talk it into quoting a client that cannot pay.
1347                let result = match result {
1348                    Err(ref e) if is_version_unaware(e) && !known_legacy => {
1349                        // Only silence is evidence the peer cannot parse the
1350                        // shape. A send failure means the request never
1351                        // arrived and teaches nothing, so it must not strand
1352                        // the peer in the legacy shape for the whole session.
1353                        // Nor may a peer that has answered a versioned request
1354                        // before be demoted by one lost response.
1355                        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    /// Collect and validate merkle candidate responses, then return the
1402    /// `CANDIDATES_PER_POOL` valid responders that are XOR-closest to the
1403    /// pool midpoint.
1404    ///
1405    /// Why distance-sort instead of "first N to respond":
1406    /// the storing-node verifier re-runs a network closest-peers lookup of
1407    /// the pool midpoint and rejects the pool if fewer than 13 of the 16
1408    /// candidate `pub_keys` appear in that authoritative close-set. Pools
1409    /// built from the fastest-to-respond quoters fail this check whenever
1410    /// truly-close peers are slower (NAT/relay paths) than farther peers.
1411    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                    // The candidate's identity is `BLAKE3(candidate.pub_key)` —
1441                    // this is what the storer derives (verifier.rs). Require it
1442                    // to equal the network responder so a two-identity operator
1443                    // cannot answer as B while shipping A's commitment.
1444                    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                    // ADR-0004: the FULL resolve-before-pay binding check, same as
1454                    // the single-node path — a candidate priced off its committed
1455                    // count, or shipping an unresolvable/forged commitment, is
1456                    // dropped before it can enter a pool the client pays. Checked
1457                    // against the CANDIDATE peer (the one the storer audits). The
1458                    // shipped commitment is consumed here (resolution only) and
1459                    // not forwarded in the PUT bundles (see
1460                    // `finalize_merkle_batch`).
1461                    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                // A storer that has explicitly declared this client
1471                // incompatible ends the batch, but only once enough distinct
1472                // peers agree. Collecting a corroborated refusal as one more
1473                // failed peer would let the pool fill from the remaining
1474                // sixteen and go on to pay, which is the burn this mechanism
1475                // exists to prevent; acting on a single peer's unauthenticated
1476                // word would instead let one hostile responder deny every
1477                // upload.
1478                Err(e @ Error::ClientUpdateRequired(_)) => {
1479                    if let Some(corroborated) =
1480                        self.note_settlement_refusal(peer_id, &e.to_string())
1481                    {
1482                        // Name the corroborators — the below-quorum branch logs
1483                        // one peer at a time, so without this line a reader
1484                        // cannot tell a two-peer verdict from a one-peer
1485                        // misfire (V2-1109).
1486                        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    /// Upload chunks using pre-computed merkle proofs from a batch payment.
1529    ///
1530    /// Each chunk is matched to its proof from `batch_result.proofs`, then
1531    /// stored to its close group concurrently. A per-chunk quorum shortfall
1532    /// (`InsufficientPeers`) does **not** abort the file: such chunks are
1533    /// collected and retried — with the same reusable proof and a freshly
1534    /// re-collected close group — for up to [`MERKLE_STORE_MAX_ATTEMPTS`]
1535    /// attempts. `stored_offset` carries chunks already confirmed by an earlier
1536    /// preflight (used for progress numbering and the returned `stored` count),
1537    /// and `total_chunks` is the whole-file total for progress events.
1538    ///
1539    /// Returns how many chunks are stored (including `stored_offset`), how many
1540    /// remained short of quorum after all retries, and the aggregate store
1541    /// stats.
1542    ///
1543    /// # Errors
1544    ///
1545    /// Returns an error only for non-quorum failures (e.g. a missing proof, or a
1546    /// chunk-count/address mismatch); quorum shortfalls are reported via
1547    /// [`MerkleStoreOutcome::failed`].
1548    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        // Clamp fan-out to batch size — partial batches should not
1559        // pay for unused slots (see PERF-RESULTS.md).
1560        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        // Cap closure re-read per scheduler refill so mid-flight limiter growth
1568        // is applied to the rest of the batch (V2-554). Clamped to batch size —
1569        // partial batches should not pay for unused slots (see PERF-RESULTS.md).
1570        let cap = || store_limiter.current().min(batch_size.max(1));
1571
1572        // External-signer path: the chunk bodies are already resident in memory,
1573        // so `store_one` clones each from this map on demand. (The whole-file
1574        // path instead reads bodies from the on-disk spill — same `store_one(addr)`
1575        // shape, so the store scheduler never carries a resident set of bodies.)
1576        let bodies: std::collections::HashMap<[u8; 32], Bytes> =
1577            addresses.iter().copied().zip(chunk_contents).collect();
1578        let addrs = addresses;
1579
1580        // Store one chunk to its (freshly re-collected) close group. Called
1581        // once per chunk per attempt, so a retry round naturally lands on a
1582        // converged routing table. Only `InsufficientPeers` is recoverable;
1583        // a missing proof stays fatal.
1584        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        // The external-signer path treats a non-quorum error as terminal (its
1623        // finalize has no further waves to fold progress into), so re-raise
1624        // the fatal that `merkle_store_with_retry` now carries in the outcome.
1625        // The CLI/spill paths, which fold progress across waves before
1626        // surfacing `PartialUpload`, read `fatal` directly instead. Quorum
1627        // shortfalls stay in `failed`/`failed_addresses`; the external-signer
1628        // finalize turns those into `PartialUpload` too (issue #166).
1629        if let Some(e) = outcome.fatal {
1630            return Err(e);
1631        }
1632        Ok(outcome)
1633    }
1634}
1635
1636/// Total store-attempt budget for a merkle batch: the initial attempt plus up
1637/// to three retries. Chosen to match the wave path's contract
1638/// (`batch.rs` iterates `0..=MAX_RETRIES` with `MAX_RETRIES = 3`) and the
1639/// four-slot [`WaveAggregateStats::retries_histogram`], so a chunk that lands
1640/// on the final retry is recorded in `retries_histogram[3]`.
1641///
1642/// A chunk's close group can transiently reject its `winner_pool` midpoint
1643/// while a few nodes' routing tables disagree about that midpoint; the network
1644/// converges within minutes. Per-chunk proofs are reusable, so retrying the
1645/// same proof after a short backoff recovers these shortfalls for free — no
1646/// re-payment and no new pool.
1647pub(crate) const MERKLE_STORE_MAX_ATTEMPTS: usize = 4;
1648
1649/// Base backoff between merkle store attempts. The routing-table divergence
1650/// that causes `InsufficientPeers` resolves on the order of minutes, so a short
1651/// sleep between rounds is enough to land on a converged close group. The
1652/// actual wait is jittered by [`MERKLE_RETRY_JITTER`] so a large failed set
1653/// does not re-fire against the same divergent nodes in lockstep.
1654pub(crate) const MERKLE_RETRY_BACKOFF: Duration = Duration::from_secs(30);
1655
1656/// Fractional jitter applied to [`MERKLE_RETRY_BACKOFF`] (±10%), spreading the
1657/// retry wave so convergent nodes are not all probed at the same instant.
1658const MERKLE_RETRY_JITTER: f64 = 0.1;
1659
1660/// Outcome of storing a merkle batch: how many chunks landed, how many
1661/// remained short of quorum after all retries, and the aggregate store stats.
1662#[derive(Debug, Default)]
1663pub(crate) struct MerkleStoreOutcome {
1664    /// Chunks that reached quorum, including any `stored_offset` carried in
1665    /// from a preflight (counted once, even if they needed retries).
1666    pub stored: usize,
1667    /// Addresses confirmed stored by this call (excludes the `stored_offset`
1668    /// preflight carry-in — those have no address here). The caller appends
1669    /// these to the file's stored set; using the explicit set (rather than
1670    /// inferring "input minus failed") keeps accounting correct even when a
1671    /// `fatal` error aborts the pass mid-flight, leaving some input chunks
1672    /// neither stored nor in `failed_addresses`.
1673    pub stored_addresses: Vec<[u8; 32]>,
1674    /// Chunks still short of quorum after [`MERKLE_STORE_MAX_ATTEMPTS`].
1675    pub failed: usize,
1676    /// Addresses (and the last error message) of chunks still short of quorum
1677    /// after all retries. Empty when `failed == 0`. Both the CLI path and the
1678    /// external-signer finalize build
1679    /// [`crate::data::Error::PartialUpload`] from this set.
1680    pub failed_addresses: Vec<([u8; 32], String)>,
1681    /// Set when a non-quorum (fatal) store error aborted the pass. Successes
1682    /// completed before the abort are still recorded in `stored`/
1683    /// `stored_addresses`; the chunks that had already failed quorum are in
1684    /// `failed_addresses`; chunks still in flight when the abort hit are in
1685    /// neither (the caller treats input-minus-stored as failed). Callers that
1686    /// want the old "fatal aborts everything" contract re-raise this as `Err`.
1687    pub fatal: Option<Error>,
1688    /// Aggregate store stats (durations, attempts, per-round retry histogram).
1689    pub stats: crate::data::client::batch::WaveAggregateStats,
1690}
1691
1692/// Drive a set of merkle chunk stores with bounded retry of quorum shortfalls.
1693///
1694/// Runs `store_one(addr)` over all `addrs` concurrently, keeping up to `cap()`
1695/// stores in flight and RE-READING `cap()` as each slot frees — so mid-flight
1696/// adaptive growth is applied to the rest of the round instead of being frozen
1697/// at a per-round snapshot (V2-554). `store_one` acquires the chunk body itself
1698/// (e.g. reads it from the on-disk spill), so only the ≤`cap()` in-flight stores
1699/// hold a body in memory — the caller passes addresses, never a resident set of
1700/// bodies, which is what lets the whole-file store run as one cap-bounded
1701/// fan-out instead of memory-bounded waves. Collects quorum shortfalls
1702/// (`InsufficientPeers`, `CloseGroupShortfall`, `RemotePut`) rather than
1703/// aborting. Failed chunks are retried — `store_one` re-collects their close
1704/// group on each call, so a converged routing table can yield a fresh group —
1705/// for up to `max_attempts` rounds, sleeping a jittered `backoff` between
1706/// rounds. A chunk's success is counted once and recorded in the retry round it
1707/// landed on (`retries_histogram[round]`). `stored_offset` seeds the returned
1708/// `stored` count and the progress numbering; `total` is the whole-file total
1709/// reported in progress events.
1710///
1711/// A non-quorum error stops the pass but does **not** discard progress: the
1712/// successes already completed this pass stay in `stored`/`stored_addresses`,
1713/// the quorum shortfalls so far stay in `failed_addresses`, and the error is
1714/// returned in [`MerkleStoreOutcome::fatal`] (as `Ok(outcome)`, not `Err`).
1715/// Callers that want the old abort-everything behaviour re-raise `fatal` as
1716/// `Err`; CLI callers fold it into `PartialUpload` while keeping the stores.
1717#[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        // Carries the failing address forward for the next round plus the last
1742        // quorum-shortfall message, so an exhausted set can report per-chunk
1743        // errors via `failed_addresses`. The chunk BODY is not carried — each
1744        // `store_one` re-reads it on demand, so at most `cap` bodies are ever
1745        // resident regardless of the total chunk count.
1746        let mut next_failed: Vec<([u8; 32], String)> = Vec::new();
1747
1748        // Rolling scheduler: keep up to `cap()` stores in flight, re-reading the
1749        // cap as each slot frees so limiter growth mid-round is applied to the
1750        // remaining chunks (V2-554). Iterator exhaustion bounds the launch count
1751        // to the pending set, so no explicit clamp to `pending.len()` is needed.
1752        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                // A quorum shortfall — whether a timeout-bearing capacity
1788                // shortfall (`InsufficientPeers`), a pure dial-churn shortfall
1789                // (`CloseGroupShortfall`, V2-554), or an app-only rejection
1790                // (`RemotePut`, e.g. pool-rejected / quote-stale / disk-full,
1791                // which are transient) — is recoverable: defer and retry the
1792                // chunk rather than aborting the whole upload (V2-468 / V2-554).
1793                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                    // Non-quorum error: fatal. Stop consuming the stream but do
1802                    // NOT discard the outcome — successes already completed this
1803                    // pass stay recorded in `stored`/`stored_addresses`. Record
1804                    // the fatal chunk itself (and any quorum shortfalls seen so
1805                    // far) as failed; anything still in flight is left for the
1806                    // caller to treat as not-stored (input minus
1807                    // `stored_addresses`).
1808                    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                // Jitter the wait (±MERKLE_RETRY_JITTER) so a large failed set
1834                // does not re-probe the same divergent nodes in lockstep.
1835                // `thread_rng` is !Send, so the value is computed and the rng
1836                // dropped before the await to keep this future Send.
1837                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
1854/// Round delays (seconds) for the merkle upload deferred-retry pass. Round 0
1855/// fires immediately — most quorum shortfalls on a healthy network are
1856/// momentary close-group divergence that clears in well under a second, and
1857/// serializing them behind mandatory sleeps was the single biggest throughput
1858/// sink in the wave path (one bad chunk parked the other 63 slots for minutes).
1859/// Only chunks that survive a round get a longer back-off before the next, so a
1860/// genuinely saturated/diverged group still gets time to settle. Mirrors the
1861/// download path's `DEFERRED_ROUND_DELAYS_SECS`.
1862pub(crate) const DEFERRED_ROUND_DELAYS_SECS: [u64; 3] = [0, 15, 45];
1863
1864/// Histogram slot for a deferred-retry round's successes.
1865///
1866/// The wave first pass lands in slot 0; deferred round `r` (0-indexed) lands in
1867/// slot `r + 1`, clamped to the last slot so the four-slot
1868/// [`WaveAggregateStats::retries_histogram`] keeps recording "which round a
1869/// chunk landed on" under the post-wave deferred structure.
1870pub(crate) fn deferred_round_histogram_slot(round: usize, hist_len: usize) -> usize {
1871    (round + 1).min(hist_len.saturating_sub(1))
1872}
1873
1874/// Outcome of the post-wave deferred-retry pass.
1875#[derive(Debug, Default)]
1876pub(crate) struct DeferredRetryOutcome {
1877    /// Running total of stored chunks, seeded with the `stored_offset` passed in
1878    /// (i.e. everything the wave passes already stored) and advanced by each
1879    /// deferred round's successes.
1880    pub stored: usize,
1881    /// Addresses that reached quorum during the deferred rounds (to be appended
1882    /// to the file's `stored` set).
1883    pub stored_addresses: Vec<[u8; 32]>,
1884    /// Count of chunks still short of quorum after the final deferred round.
1885    pub failed: usize,
1886    /// Addresses (and last quorum-shortfall message) still short after the final
1887    /// round, or — when `fatal` is set — the chunks that were still pending when
1888    /// a non-quorum error aborted the pass.
1889    pub failed_addresses: Vec<([u8; 32], String)>,
1890    /// Set when a deferred round hit a non-quorum (fatal) store error. The
1891    /// caller surfaces this as `PartialUpload` preserving everything stored so
1892    /// far, mirroring the wave path's fatal handling.
1893    pub fatal: Option<String>,
1894    /// Aggregate store stats merged across rounds, with each round's successes
1895    /// already mapped into its [`deferred_round_histogram_slot`].
1896    pub stats: crate::data::client::batch::WaveAggregateStats,
1897}
1898
1899/// Retry a file-level set of quorum-short merkle chunks in concurrent rounds.
1900///
1901/// This is the upload analogue of the download path's deferred-retry loop. The
1902/// whole-file store pass hands its quorum-short chunks here. Each round stores
1903/// all still-pending chunks in a single cap-bounded pass via
1904/// [`merkle_store_with_retry`] at `concurrency_for(len)` — `store_one(addr)`
1905/// re-reads each body on demand (from the spill), so only the ≤`concurrency_for`
1906/// in-flight stores hold a body and peak resident memory stays bounded
1907/// regardless of the deferred-chunk count. Survivors carry to the next round
1908/// after a `round_delays_secs` sleep. Chunks still short after the final round
1909/// become `failed_addresses`; a non-quorum store error stops the pass and is
1910/// reported via `fatal` (with the quorum shortfalls seen so far recorded as
1911/// `failed_addresses`) so the caller can surface `PartialUpload` — reconciled
1912/// against the full address list — without discarding earlier progress.
1913///
1914/// `store_one`, `progress`, `stored_offset` and `total` mirror
1915/// [`merkle_store_with_retry`].
1916#[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        // Store this round's whole pending set in one cap-bounded pass. A
1953        // single-pass round records its successes in histogram slot 0, so
1954        // redirect them into the round's own slot.
1955        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        // Re-read the cap per scheduler refill (V2-554) via `concurrency_for`,
1962        // which re-samples the store limiter clamped to this round's size.
1963        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        // Merge stats, redirecting this round's successes to its slot.
1983        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            // Fatal mid-pass: confirmed stores are preserved above. The store
1997            // helper left this round's quorum shortfalls in `failed_addresses`;
1998            // chunks still in flight / not yet launched are reconciled against
1999            // the full address list by the caller's `partial_upload_after_fatal`.
2000            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        // Quorum-short chunks from this round survive to the next.
2007        remaining = round_outcome.failed_addresses;
2008    }
2009
2010    outcome.failed = remaining.len();
2011    outcome.failed_addresses = remaining;
2012    Ok(outcome)
2013}
2014
2015/// Phase 2 of external-signer merkle payment: generate proofs from winner.
2016///
2017/// Takes the prepared batch and the winner pool hash returned by the
2018/// on-chain payment transaction. Generates per-chunk merkle proofs.
2019pub 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    // Find the winner pool
2027    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    // ADR-0004: commitment sidecars are deliberately NOT forwarded in the
2039    // per-chunk proofs. Sixteen sidecars are ~214 KB serialized, and copying
2040    // them into every chunk's bundle pushed the proof past the storer's
2041    // payment-proof size cap, rejecting every merkle PUT once nodes carried
2042    // live commitments. The client still fully resolves every candidate's
2043    // commitment before paying (during pool collection); the storer's
2044    // cross-check is best-effort and resolves pins from its gossip cache or a
2045    // `GetCommitmentByPin` fetch when no sidecar is shipped.
2046
2047    // Generate proofs for each chunk
2048    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/// Compile-time assertions that merkle method futures are Send.
2081#[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/// Test-only builders shared by this module's tests and the external-finalize
2102/// tests in `file.rs` (ADR-0003).
2103#[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    /// A winner pool hash `finalize_merkle_batch` will accept for `batch`
2169    /// (the first candidate pool's) — the same selection the existing
2170    /// finalize tests use. Lives here because `candidate_pools` is private
2171    /// outside this module.
2172    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    // =========================================================================
2185    // should_use_merkle (free function, no Client needed)
2186    // =========================================================================
2187
2188    #[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    // =========================================================================
2227    // preflight_stored_status — must degrade gracefully, never abort the batch
2228    // =========================================================================
2229
2230    #[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    /// The regression: a transient quote-quorum failure during the preflight
2242    /// must NOT propagate (which would abort the whole forced-merkle upload).
2243    /// It is treated as "not known to be stored" → queue the chunk for upload.
2244    #[test]
2245    fn test_preflight_transient_quote_failure_does_not_abort() {
2246        // The exact error STG-01 hit: couldn't gather a 7-quote quorum.
2247        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    /// Genuine application errors still propagate — the preflight should not
2262    /// silently swallow problems that would recur on a healthy link.
2263    #[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    // =========================================================================
2335    // MerkleTree construction and proof generation (pure, no network)
2336    // =========================================================================
2337
2338    #[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    // =========================================================================
2393    // Proof serialization round-trip
2394    // =========================================================================
2395
2396    #[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        // Build candidate nodes (with dummy signatures — not ML-DSA, just for serialization test)
2414        #[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    // =========================================================================
2450    // Candidate validation logic
2451    // =========================================================================
2452
2453    #[test]
2454    fn test_candidate_wrong_timestamp_rejected() {
2455        // Simulates what collect_validated_candidates checks
2456        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        // Timestamp check: 1000 != 2000
2467        assert_ne!(candidate.merkle_payment_timestamp, 2000);
2468    }
2469
2470    // =========================================================================
2471    // finalize_merkle_batch (external signer)
2472    // =========================================================================
2473
2474    /// Candidate pool with distinct prices, so the median is a specific
2475    /// candidate rather than an artifact of every price being equal.
2476    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            // 100, 200, ... 1600 — upper median (index 8 of 16) is 900.
2490            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    /// The contract's `median16`: upper median, index 8 of 16 ascending.
2505    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    /// The invariant the fix exists for, stated at the level it actually
2546    /// holds: the median payable amount over the winning pool is
2547    /// `MERKLE_PAYMENT_MULTIPLIER x` the median *quoted* price. The contract
2548    /// spends `median16(amount) * 2^depth` over `2^depth` **padded** leaves, so
2549    /// this is the settlement per padded leaf — the same figure the single-node
2550    /// path pays its median-priced issuer per chunk. It says nothing about cost
2551    /// per *actual* chunk: a batch whose size is not a power of two pays for
2552    /// padding leaves too (that is what `merkle_billable_leaves` bills for).
2553    #[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        // Every proof should be non-empty
2586        for proof_bytes in batch.proofs.values() {
2587            assert!(!proof_bytes.is_empty());
2588        }
2589    }
2590
2591    /// DEV-01 regression: per-chunk merkle proofs must never ship commitment
2592    /// sidecars — all 16 winner-pool sidecars (~214 KB serialized) copied into
2593    /// every chunk's proof pushed it past the storer's payment-proof size cap,
2594    /// rejecting every merkle PUT once nodes carried live commitments. The
2595    /// e2e (`adr0004_merkle_upload_against_bound_candidates`) proves the flow
2596    /// end-to-end; this pins the wire invariant directly so it cannot slip
2597    /// back in behind a raised node-side cap.
2598    #[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        // Bind every candidate to a pin, mirroring a network where all nodes
2604        // carry live commitments (the DEV-01 trigger state).
2605        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    // =========================================================================
2656    // Batch splitting edge cases
2657    // =========================================================================
2658
2659    /// Counts spanning every interesting case: the minimum tree, the merkle
2660    /// threshold and just past it, either side of a full batch, and the
2661    /// `1 mod MAX_LEAVES` counts the naive `chunks(MAX_LEAVES)` split turned
2662    /// into an unpayable `[..., 1]` tail.
2663    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    /// The test-seam cap partitions like the production cap, floored at 3 —
2688    /// a cap of 2 cannot partition odd totals into payable ≥2-leaf trees
2689    /// (ADR-0003).
2690    #[test]
2691    fn merkle_batch_sizes_with_cap_partitions_and_clamps() {
2692        // Small caps: rebalanced, every part payable (2..=cap), sums correct.
2693        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        // A cap below the floor clamps to 3 instead of emitting a 1-leaf part.
2697        assert_eq!(merkle_batch_sizes_with_cap(5, 2), vec![3, 2]);
2698        // A cap above MAX_LEAVES clamps down to the contract bound.
2699        assert_eq!(
2700            merkle_batch_sizes_with_cap(MAX_LEAVES + 1, MAX_LEAVES * 4),
2701            vec![MAX_LEAVES - 1, 2]
2702        );
2703        // Exhaustive soundness under the smallest cap: parts within bounds,
2704        // no 1-leaf part, exact cover.
2705        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    /// The external multi-batch fold mirrors the wallet path: proofs union,
2716    /// costs sum, and the merged timestamp is the OLDEST sub-batch's (worst
2717    /// case for the expiry window).
2718    #[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    /// The defect: `[256, 1]` pays the first batch on-chain and then hands a
2745    /// single address to a tree that needs two, so the upload fails *after*
2746    /// spending. Every count must produce trees that can all be built.
2747    #[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    /// A merkle upload of 257 chunks — the count the old split could not pay —
2794    /// is the shape preflight routinely leaves behind, since `to_upload` is
2795    /// whatever the network did not already hold.
2796    #[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    /// No batch may be paid for and then fail to build its tree: the partition
2818    /// is what payment iterates, so proving every batch builds proves no
2819    /// on-chain payment can be followed by a singleton-tree failure.
2820    #[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        // Known figures, spelled out: padding is billed, never hidden.
2850        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        // Nothing to upload costs nothing; a lone chunk still quotes the
2854        // two-leaf minimum a tree needs.
2855        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    /// The external signer prepares one tree, signs once, and pays once, so an
2870    /// oversized set is refused up front rather than being quietly paid under a
2871    /// different model.
2872    #[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    // =========================================================================
2892    // merkle_store_with_retry: collect-not-abort + bounded retry (C2.1 / C2.2)
2893    // =========================================================================
2894
2895    use std::sync::{Arc, Mutex};
2896
2897    /// Build `count` chunk addresses for the store helper. Bodies are read on
2898    /// demand by `store_one`, so the tests pass addresses only.
2899    fn make_addrs(count: usize) -> Vec<[u8; 32]> {
2900        make_test_addresses(count)
2901    }
2902
2903    /// C2.1: a per-chunk `InsufficientPeers` is collected, not propagated —
2904    /// the whole batch must NOT abort. With a single attempt, the failing
2905    /// subset is reported via `failed` and the rest are `stored`.
2906    #[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        // Single attempt → all successes recorded in round 0.
2931        assert_eq!(outcome.stats.retries_histogram[0], 4);
2932        assert_eq!(outcome.stats.chunk_attempts_total, 6);
2933    }
2934
2935    /// #167 regression, preserved at the engine seam the spill path composes
2936    /// (`upload_merkle_from_spill` runs one single-attempt pass and then the
2937    /// deferred rounds): a genuine quorum shortfall — every batch PAID, the
2938    /// store short — survives all deferred retries with
2939    /// `stored + failed == total` and the exact shortfall set in
2940    /// `failed_addresses`, which is precisely what the spill path folds into
2941    /// `Error::PartialUpload` instead of reporting success (#166).
2942    #[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        // Initial pass exactly as the spill path runs it: one attempt,
2959        // shortfalls deferred rather than retried inline.
2960        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        // Deferred rounds (zero delays for the test): the same chunks stay
2977        // short through every round.
2978        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    /// V2-554: the store scheduler must RE-READ the cap as each slot frees
3007    /// (rolling), not snapshot it once like `buffer_unordered`. A snapshot would
3008    /// invoke the cap closure once per attempt; the rolling scheduler invokes it
3009    /// once per drained slot, so mid-flight limiter growth reaches the rest of
3010    /// the round. Proven here by counting cap-closure invocations.
3011    #[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    /// The whole-file store pass is a single cap-bounded fan-out with NO wave
3038    /// barrier: one slow straggler (a chunk whose peers take a long time) must
3039    /// not block the rest of the file, and must be driven to completion
3040    /// concurrently by the fast chunks. If the scheduler serialized (a barrier),
3041    /// the fast chunks could not run until the straggler returned — a deadlock
3042    /// the surrounding timeout would catch.
3043    #[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                    // Block until every fast chunk has finished. This can only
3058                    // resolve if the fast chunks run WHILE this one is parked —
3059                    // i.e. there is no barrier serializing the store pass.
3060                    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    /// Peak memory is bounded by the store cap, not the file size: `store_one`
3080    /// reads each body on demand, so the scheduler holds at most `cap` stores
3081    /// (hence at most `cap` bodies) in flight at once — the property that lets
3082    /// the whole file store as one fan-out without the old 64-chunk waves.
3083    #[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                // Yield so sibling stores get a chance to start concurrently,
3100                // maximising the observed in-flight count.
3101                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    /// V2-468: an app-only quorum shortfall surfaces as `Error::RemotePut`
3133    /// (pool-rejected / quote-stale / disk-full — transient), which must be
3134    /// treated as recoverable just like `InsufficientPeers`: collected and
3135    /// retried, never aborting the whole batch.
3136    #[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    /// A non-quorum error (e.g. a missing proof) is captured in `fatal` rather
3168    /// than discarded — the call returns `Ok(outcome)` so the caller can decide
3169    /// whether to re-raise it or fold it into `PartialUpload`.
3170    #[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    /// A fatal error mid-pass preserves the successes that already completed in
3185    /// the same pass — they are not discarded with the abort. Concurrency 1
3186    /// makes ordering deterministic: the first five chunks store, then the sixth
3187    /// aborts fatally.
3188    #[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        // The five chunks stored before the abort are preserved, not lost.
3206        assert_eq!(outcome.stored, 5);
3207        assert_eq!(outcome.stored_addresses.len(), 5);
3208        assert!(!outcome.stored_addresses.contains(&bad));
3209        // The fatal chunk is reported as failed (not silently dropped).
3210        assert!(outcome.failed_addresses.iter().any(|(a, _)| *a == bad));
3211    }
3212
3213    /// C2.2: only the chunks that failed the previous round are retried.
3214    #[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        // Record every (addr) the store op was invoked with, in call order.
3222        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            // Fails the first round only; succeeds thereafter.
3228            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        // Round 1 drains fully before round 2 starts, so the call log is
3249        // segmented: first `total` calls = round 1 (all chunks), the rest =
3250        // the retry round, which must contain ONLY the failing set.
3251        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    /// C2.2: a chunk that fails attempt 1 and succeeds attempt 2 is counted
3259    /// once as stored and recorded as one retry in `retries_histogram[1]`.
3260    #[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        // 3 chunks landed on the first attempt, 1 on the first retry.
3295        assert_eq!(outcome.stats.retries_histogram[0], total - 1);
3296        assert_eq!(outcome.stats.retries_histogram[1], 1);
3297        // One extra store attempt for the flaky chunk.
3298        assert_eq!(outcome.stats.chunk_attempts_total, total + 1);
3299    }
3300
3301    /// C2.2: when every chunk stays short of quorum through the whole attempt
3302    /// budget, the helper still returns `Ok` (collect-not-abort) with the full
3303    /// batch reported as `failed`, having tried each chunk exactly
3304    /// `MERKLE_STORE_MAX_ATTEMPTS` times.
3305    #[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        // Every chunk was attempted once per round across the full budget.
3330        assert_eq!(
3331            outcome.stats.chunk_attempts_total,
3332            total * MERKLE_STORE_MAX_ATTEMPTS
3333        );
3334        // No successes, so the histogram stays empty.
3335        assert_eq!(outcome.stats.retries_histogram, [0; 4]);
3336    }
3337
3338    /// D (CLI path): when retries are exhausted, `failed_addresses` names
3339    /// exactly the still-short-of-quorum chunks (with their last error message)
3340    /// and excludes the ones that stored. This is what `upload_merkle_from_spill`
3341    /// uses to build `PartialUpload`.
3342    #[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        // `failed_addresses` names exactly the failing set, no stored chunks.
3375        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        // Each carries a non-empty error message for the PartialUpload report.
3380        for (_, msg) in &outcome.failed_addresses {
3381            assert!(msg.contains("permanent shortfall"));
3382        }
3383    }
3384
3385    /// `failed_addresses` is empty when every chunk reaches quorum (no
3386    /// `PartialUpload` is raised by the CLI path in that case).
3387    #[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    // =========================================================================
3412    // merkle_deferred_retry: download-style concurrent post-wave retry (V2-466)
3413    // =========================================================================
3414
3415    /// The histogram slot mapping: the wave first pass is slot 0; deferred
3416    /// round `r` is slot `r + 1`, clamped to the last slot.
3417    #[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        // Beyond the histogram width, clamp to the final slot.
3423        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    /// A chunk that is quorum-short on early rounds but succeeds on a later
3435    /// round is stored exactly once, recorded in that round's histogram slot,
3436    /// and reported with no failures.
3437    #[tokio::test]
3438    async fn deferred_retry_succeeds_on_a_later_round() {
3439        let deferred = deferred_set(3);
3440        // Each chunk fails its first attempt (round 0) and succeeds the second
3441        // (round 1 → histogram slot 2).
3442        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        // Round 1 → slot 2; round 0 (slot 1) saw zero successes.
3479        assert_eq!(outcome.stats.retries_histogram[1], 0);
3480        assert_eq!(outcome.stats.retries_histogram[2], 3);
3481        // Each chunk attempted twice: one failed round + one success round.
3482        assert_eq!(outcome.stats.chunk_attempts_total, 6);
3483    }
3484
3485    /// Chunks still short of quorum after the final deferred round become
3486    /// `failed`, not silently dropped, and no fatal error is set.
3487    #[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        // Three rounds × two chunks, all failing.
3512        assert_eq!(outcome.stats.chunk_attempts_total, 6);
3513    }
3514
3515    /// A non-quorum (fatal) error during a deferred round stops the pass, is
3516    /// surfaced via `fatal`, and preserves an earlier round's success in
3517    /// `stored`/`stored_addresses` while the still-pending chunk is reported as
3518    /// failed.
3519    #[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        // `good` succeeds on round 0; `bad` is quorum-short on round 0, then
3527        // hits a fatal Payment error on round 1.
3528        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    /// An empty deferred set is a no-op: no rounds run, nothing stored or failed.
3570    #[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    // =========================================================================
3596    // Settlement refusals on the merkle path
3597    // =========================================================================
3598
3599    /// A refusal is unauthenticated and two of them latch this client for the
3600    /// rest of its run, so the merkle path must apply the same coherence check
3601    /// the single-node path applies. Without it, two faulty or hostile
3602    /// candidates could deny every upload with a refusal that is not about this
3603    /// client at all.
3604    #[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        // Echoes a version that is not ours: the peer is talking about someone
3622        // else's request.
3623        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        // States a minimum this client already meets, so there is nothing to
3633        // refuse.
3634        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        // A coherent one is believed, and still carries the upgrade wording.
3641        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    /// Pools are drained rather than cancelled on the first error, so more than
3653    /// one can fail. A refusal has to win: reporting the ordinary failure
3654    /// instead would hide the one verdict that stops the caller falling back to
3655    /// another payment path and spending.
3656    #[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        // Ordinary failure first, refusal second.
3662        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        // Refusal first, ordinary failure second. Order must not matter.
3671        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        // With no refusal, the first ordinary failure is what the caller sees.
3680        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        // Every pool succeeded, so there is nothing to report.
3689        assert!(PoolVerdict::default().into_error().is_none());
3690    }
3691}