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 crate::data::network::send_and_await_chunk_response;
14use ant_protocol::evm::{
15    Amount, MerklePaymentCandidateNode, MerklePaymentCandidatePool, MerklePaymentProof, MerkleTree,
16    MidpointProof, PoolCommitment, CANDIDATES_PER_POOL, MAX_LEAVES,
17};
18use ant_protocol::payment::commitment::{
19    commitment_hash, verify_commitment_signature, StorageCommitment, MAX_COMMITMENT_KEY_COUNT,
20    MAX_COMMITMENT_SIDECAR_BYTES,
21};
22use ant_protocol::payment::{
23    calculate_price, serialize_merkle_proof, verify_merkle_candidate_signature,
24};
25use ant_protocol::transport::PeerId;
26use ant_protocol::{
27    compute_address, ChunkMessage, ChunkMessageBody, MerkleCandidateQuoteRequest,
28    MerkleCandidateQuoteRequestV2, MerkleCandidateQuoteResponse, ProtocolError,
29};
30#[cfg(test)]
31use bytes::Bytes;
32use futures::stream::{FuturesUnordered, StreamExt};
33use rand::Rng;
34use std::collections::HashMap;
35#[cfg(test)]
36use std::collections::VecDeque;
37use std::sync::Arc;
38use std::time::Duration;
39use tokio::sync::mpsc;
40use tracing::{debug, info, warn};
41use xor_name::XorName;
42
43/// Default threshold: use merkle payments when chunk count >= this value.
44pub const DEFAULT_MERKLE_THRESHOLD: usize = 64;
45
46/// Payment multiplier applied to a quoted price before settlement.
47///
48/// Deliberately the **same constant** the single-node path uses rather than a
49/// second copy of `3`: the whole defect this fixes was the two paths disagreeing
50/// about the multiplier, so they now read it from one place. (The storer's
51/// `PAID_QUOTE_PAYMENT_MULTIPLIER` and `ant_protocol`'s single-node builder are
52/// still separate literals; folding all of them into one `ant-protocol`
53/// constant is a follow-up.)
54///
55/// The single-node path pays the median-priced issuer 3× its quoted price, so
56/// the network receives the same revenue as paying three members of the close
57/// group while costing one transaction's gas. The merkle path never applied it:
58/// it submitted the raw quoted price as the on-chain payable amount, so the
59/// contract's `median16(amount) × 2^depth` came to **1×** the median per padded
60/// leaf — a third of what the same chunk earns on the single-node path, for
61/// identical storage and replication.
62///
63/// The on-chain field is `CandidateNode.amount`, the sum the vault pays out,
64/// not the quote itself; the signed candidate keeps its 1× quoted `price` and
65/// the pool hash is unchanged, so every proof still verifies against the
66/// quotes the nodes actually signed.
67use crate::data::client::payment::SINGLE_NODE_PAYMENT_MULTIPLIER as MERKLE_PAYMENT_MULTIPLIER;
68
69/// ADR-0004 resolve-before-pay gate for a merkle candidate — the merkle-path
70/// equivalent of the single-node `quote_commitment_binding_is_valid`. Runs the
71/// FULL binding check (shape, cap, exact price, and for bound candidates the
72/// commitment parse, peer-binding, signature, `hash == pin`, and
73/// `count == key_count`) before the candidate is allowed into a pool the client
74/// may pay. `peer_id` is derived from the candidate's `pub_key`
75/// (`BLAKE3(pub_key)`), matching the storer.
76///
77/// Returns `Ok(())` if the binding fully resolves, else `Err(detail)`.
78fn merkle_candidate_binding_is_valid(
79    peer_id: &PeerId,
80    candidate: &MerklePaymentCandidateNode,
81    commitment: &Option<Vec<u8>>,
82) -> std::result::Result<(), String> {
83    let count = candidate.committed_key_count;
84    let pin = candidate.commitment_pin;
85    match (count, pin.is_some()) {
86        (0, false) | (1.., true) => {}
87        (1.., false) => {
88            return Err(format!(
89                "committed_key_count={count} > 0 but commitment_pin is None (unauditable count)"
90            ));
91        }
92        (0, true) => {
93            return Err("committed_key_count=0 with a commitment_pin (incoherent baseline)".into());
94        }
95    }
96    if count > MAX_COMMITMENT_KEY_COUNT {
97        return Err(format!(
98            "committed_key_count={count} exceeds MAX_COMMITMENT_KEY_COUNT={MAX_COMMITMENT_KEY_COUNT}"
99        ));
100    }
101    let expected = calculate_price(count as usize);
102    if candidate.price != expected {
103        return Err(format!(
104            "price {} does not equal calculate_price(committed_key_count={count}) = {expected}",
105            candidate.price
106        ));
107    }
108
109    let Some(pin) = pin else {
110        return Ok(()); // baseline candidate pins nothing
111    };
112    let Some(blob) = commitment else {
113        return Err("bound candidate did not ship its commitment; pin is unresolvable".into());
114    };
115    if blob.len() > MAX_COMMITMENT_SIDECAR_BYTES {
116        return Err(format!(
117            "shipped commitment is {} bytes, exceeds MAX_COMMITMENT_SIDECAR_BYTES={MAX_COMMITMENT_SIDECAR_BYTES}",
118            blob.len()
119        ));
120    }
121    let commitment: StorageCommitment = rmp_serde::from_slice(blob)
122        .map_err(|e| format!("shipped commitment did not deserialize: {e}"))?;
123    if compute_address(&commitment.sender_public_key) != *peer_id.as_bytes()
124        || commitment.sender_peer_id != *peer_id.as_bytes()
125    {
126        return Err("shipped commitment is not bound to the candidate peer".into());
127    }
128    if !verify_commitment_signature(&commitment) {
129        return Err("shipped commitment has an invalid signature".into());
130    }
131    if commitment_hash(&commitment) != Some(pin) {
132        return Err("shipped commitment does not hash to the candidate's pin".into());
133    }
134    if commitment.key_count != count {
135        return Err(format!(
136            "shipped commitment attests key_count={} but the candidate claims {count}",
137            commitment.key_count
138        ));
139    }
140    Ok(())
141}
142
143/// Build the on-chain [`PoolCommitment`] for a candidate pool, applying
144/// [`MERKLE_PAYMENT_MULTIPLIER`] to every candidate's payable amount.
145///
146/// The contract derives what it pays out from the amounts submitted here
147/// (`total = median16(amount) × 2^depth`, split evenly across `depth`
148/// winners), so multiplying here — and only here — brings the merkle path to
149/// the same per-chunk revenue as the single-node path.
150///
151/// The pool hash is deliberately left as `pool.hash()`, computed over the
152/// candidates' **signed** 1× prices. It is the key the storer resolves the
153/// on-chain payment record under, and it commits to the quotes the nodes
154/// actually signed; multiplying the payable amount must not disturb it.
155fn pool_commitment_with_payment_multiplier(
156    pool: &MerklePaymentCandidatePool,
157) -> Result<PoolCommitment> {
158    let mut commitment = pool.to_commitment();
159    let multiplier = Amount::from(MERKLE_PAYMENT_MULTIPLIER);
160    for candidate in &mut commitment.candidates {
161        candidate.price = candidate.price.checked_mul(multiplier).ok_or_else(|| {
162            Error::Payment(format!(
163                "Merkle candidate amount overflow applying {MERKLE_PAYMENT_MULTIPLIER}x to price {}",
164                candidate.price
165            ))
166        })?;
167    }
168    Ok(commitment)
169}
170
171/// Turn a merkle candidate quote response into the candidate it carries, or
172/// the error that explains why there is none.
173///
174/// Shared by the versioned request and its legacy retry so the two cannot
175/// interpret the same response differently.
176///
177/// `ClientUpdateRequired` is lifted out of the generic protocol-error case
178/// deliberately. It is the one rejection that is about this client rather than
179/// about this request, it already carries wording aimed at the person running
180/// the upload, and it must not be retried in a shape that would get a quote
181/// anyway.
182/// Which failure a fully drained set of candidate pools should report.
183///
184/// Pools are drained rather than short-circuited, so more than one can fail.
185/// A refusal outranks an ordinary failure: if one pool runs out of peers while
186/// another declares this client unable to settle, the second is the answer that
187/// keeps the caller from falling back to a payment path and spending.
188#[derive(Default)]
189struct PoolVerdict {
190    refusal: Option<Error>,
191    first_failure: Option<Error>,
192}
193
194impl PoolVerdict {
195    fn note(&mut self, e: Error) {
196        match e {
197            e @ Error::ClientUpdateRequired(_) => {
198                if self.refusal.is_none() {
199                    self.refusal = Some(e);
200                }
201            }
202            e => {
203                if self.first_failure.is_none() {
204                    self.first_failure = Some(e);
205                }
206            }
207        }
208    }
209
210    fn into_error(self) -> Option<Error> {
211        self.refusal.or(self.first_failure)
212    }
213}
214
215fn map_merkle_candidate_response(
216    peer_id: PeerId,
217    body: ChunkMessageBody,
218) -> Option<Result<(MerklePaymentCandidateNode, Option<Vec<u8>>)>> {
219    match body {
220        ChunkMessageBody::MerkleCandidateQuoteResponse(MerkleCandidateQuoteResponse::Success {
221            candidate_node,
222            commitment,
223        }) => match rmp_serde::from_slice::<MerklePaymentCandidateNode>(&candidate_node) {
224            Ok(node) => Some(Ok((node, commitment))),
225            Err(e) => Some(Err(Error::Serialization(format!(
226                "Failed to deserialize candidate node from {peer_id}: {e}"
227            )))),
228        },
229        // Validated, not taken at face value. A refusal is unauthenticated and
230        // two of them latch this client for its whole run, so the merkle path
231        // applies the same coherence check as the single-node one: the echoed
232        // version must be ours and the stated minimum must genuinely exceed it.
233        // Without it, two faulty or hostile candidates could deny every upload
234        // with a refusal that does not describe this client at all.
235        ChunkMessageBody::MerkleCandidateQuoteResponse(MerkleCandidateQuoteResponse::Error(
236            ProtocolError::ClientUpdateRequired {
237                client_settlement_version,
238                min_settlement_version,
239            },
240        )) => Some(Err(settlement_refusal_error(
241            &peer_id,
242            client_settlement_version,
243            min_settlement_version,
244        ))),
245        ChunkMessageBody::MerkleCandidateQuoteResponse(MerkleCandidateQuoteResponse::Error(
246            behind @ ProtocolError::StorerUpdateRequired { .. },
247        )) => Some(Err(Error::StorerUpdateRequired(behind.to_string()))),
248        ChunkMessageBody::MerkleCandidateQuoteResponse(MerkleCandidateQuoteResponse::Error(e)) => {
249            Some(Err(Error::Protocol(format!(
250                "Merkle quote error from {peer_id}: {e}"
251            ))))
252        }
253        _ => None,
254    }
255}
256
257/// Should this failure be retried as an unversioned request?
258///
259/// A storer built before the settlement version existed cannot decode the
260/// versioned request, so it never replies and the send fails at the transport
261/// layer. Any response this client *recognises*, refusal included, means the
262/// storer understood us, and retrying that in an older shape would defeat the
263/// gate.
264///
265/// A reply that decodes but carries a body the mapper does not recognise is
266/// not distinguished: the wait continues and ends in a timeout, so it takes
267/// the fallback too. That is no more than a peer could achieve by saying
268/// nothing, which is why it is bounded by the cutover guard rather than
269/// special-cased here.
270///
271/// # This is a downgrade path, and it is only safe because nothing can be
272/// refused yet
273///
274/// Silence is **not** proof that a peer cannot parse the request. A dropped
275/// response, packet loss, an overloaded peer, or one deliberately discarding
276/// versioned requests all look identical from here. So this predicate can be
277/// provoked, and a peer that provokes it gets asked again without a version.
278///
279/// That is harmless only while no client can be refused on version grounds,
280/// which is exactly the case while `MIN_SUPPORTED_SETTLEMENT_VERSION` is the
281/// first declarable version. The moment the minimum is raised, this becomes a
282/// way to obtain a quote the gate meant to withhold, and then to burn a
283/// payment against it.
284///
285/// The guard below turns "remember to delete the fallback before raising the
286/// minimum" from a comment into a build failure. It lives in
287/// [`crate::data::client::UNVERSIONED_RETRY_REQUIRES_MIN_V1`] and is referenced
288/// from every fallback site, including the independent single-node one in
289/// `quote.rs`, so deleting one path cannot silently leave the other unguarded.
290const _: () = crate::data::client::UNVERSIONED_RETRY_REQUIRES_MIN_V1;
291
292const fn is_version_unaware(error: &Error) -> bool {
293    matches!(error, Error::Network(_) | Error::Timeout(_))
294}
295
296/// Payment mode for uploads.
297#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
298#[serde(rename_all = "snake_case")]
299pub enum PaymentMode {
300    /// Automatically choose: merkle for batches >= threshold, single otherwise.
301    #[default]
302    Auto,
303    /// Force merkle batch payment regardless of batch size (min 2 chunks).
304    Merkle,
305    /// Force single-node payment (one tx per chunk).
306    Single,
307}
308
309/// Result of a merkle batch payment.
310///
311/// Serializable so it can be persisted across runs for resume after a
312/// partial-upload failure. See `crate::data::client::cached_merkle`.
313#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
314pub struct MerkleBatchPaymentResult {
315    /// Map of `XorName` to serialized tagged proof bytes (ready to use in PUT requests).
316    pub proofs: HashMap<[u8; 32], Vec<u8>>,
317    /// Number of chunks in the batch.
318    pub chunk_count: usize,
319    /// Total storage cost in atto (token smallest unit).
320    pub storage_cost_atto: String,
321    /// Total gas cost in wei.
322    pub gas_cost_wei: u128,
323    /// Unix timestamp (seconds) used for the on-chain merkle payment.
324    /// Persisted so resume can check whether the on-chain payment has
325    /// aged out beyond the merkle expiration window and the cached
326    /// receipt must be discarded.
327    #[serde(default)]
328    pub merkle_payment_timestamp: u64,
329}
330
331/// Prepared merkle batch ready for external payment.
332///
333/// Contains everything needed to submit the on-chain merkle payment
334/// and then finalize proof generation without a wallet.
335#[derive(Clone, serde::Serialize, serde::Deserialize)]
336pub struct PreparedMerkleBatch {
337    /// Merkle tree depth (needed for the on-chain call).
338    pub depth: u8,
339    /// Pool commitments for the on-chain call.
340    pub pool_commitments: Vec<PoolCommitment>,
341    /// Timestamp used for the merkle payment.
342    pub merkle_payment_timestamp: u64,
343    /// Internal: candidate pools (needed for proof generation after payment).
344    candidate_pools: Vec<MerklePaymentCandidatePool>,
345    /// Internal: the merkle tree (needed for proof generation).
346    tree: MerkleTree,
347    /// Internal: chunk addresses in order.
348    addresses: Vec<[u8; 32]>,
349}
350
351impl PreparedMerkleBatch {
352    pub(crate) fn addresses(&self) -> &[[u8; 32]] {
353        &self.addresses
354    }
355
356    pub(crate) fn validate_checkpoint(&self) -> Result<()> {
357        if self.depth != self.tree.depth()
358            || self.addresses.len() != self.tree.leaf_count()
359            || self.candidate_pools.is_empty()
360        {
361            return Err(Error::InvalidData(
362                "invalid prepared Merkle dimensions".into(),
363            ));
364        }
365        for (i, address) in self.addresses.iter().enumerate() {
366            if !self
367                .tree
368                .generate_address_proof(i, XorName(*address))
369                .map_err(|e| Error::InvalidData(e.to_string()))?
370                .verify()
371            {
372                return Err(Error::InvalidData(
373                    "Merkle checkpoint address does not match tree".into(),
374                ));
375            }
376        }
377        let midpoints = self
378            .tree
379            .reward_candidates(self.merkle_payment_timestamp)
380            .map_err(|e| Error::InvalidData(e.to_string()))?;
381        if midpoints.len() != self.candidate_pools.len()
382            || self.pool_commitments.len() != self.candidate_pools.len()
383        {
384            return Err(Error::InvalidData("invalid Merkle pool count".into()));
385        }
386        let mut midpoints = midpoints
387            .into_iter()
388            .map(|proof| (proof.hash(), proof))
389            .collect::<HashMap<_, _>>();
390        for (pool, commitment) in self.candidate_pools.iter().zip(&self.pool_commitments) {
391            if midpoints.remove(&pool.midpoint_proof.hash()).as_ref() != Some(&pool.midpoint_proof)
392                || pool_commitment_with_payment_multiplier(pool)? != *commitment
393                || pool.candidate_nodes.iter().any(|candidate| {
394                    !verify_merkle_candidate_signature(candidate)
395                        || candidate.merkle_payment_timestamp != self.merkle_payment_timestamp
396                        || candidate.committed_key_count > MAX_COMMITMENT_KEY_COUNT
397                        || candidate.price
398                            != calculate_price(candidate.committed_key_count as usize)
399                })
400            {
401                return Err(Error::InvalidData("invalid Merkle checkpoint pool".into()));
402            }
403        }
404        Ok(())
405    }
406}
407
408/// Result of checking a merkle upload batch before payment.
409#[derive(Debug, Clone, Default)]
410pub(crate) struct MerkleUploadPlan {
411    /// Chunks already confirmed by their close group.
412    pub already_stored: Vec<[u8; 32]>,
413    /// Chunks that still need payment and storage.
414    pub to_upload: Vec<[u8; 32]>,
415    /// Total byte size of chunks in `to_upload`.
416    to_upload_total_bytes: u64,
417}
418
419impl MerkleUploadPlan {
420    /// Average byte size of chunks that still need upload.
421    #[must_use]
422    pub fn to_upload_avg_size(&self) -> u64 {
423        if self.to_upload.is_empty() {
424            return 0;
425        }
426
427        self.to_upload_total_bytes / self.to_upload.len() as u64
428    }
429}
430
431impl std::fmt::Debug for PreparedMerkleBatch {
432    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
433        f.debug_struct("PreparedMerkleBatch")
434            .field("depth", &self.depth)
435            .field("pool_commitments", &self.pool_commitments.len())
436            .field("merkle_payment_timestamp", &self.merkle_payment_timestamp)
437            .field("candidate_pools", &self.candidate_pools.len())
438            .field("addresses", &self.addresses.len())
439            .finish()
440    }
441}
442
443/// Select chunk contents that correspond to `addresses`, preserving address order.
444///
445/// Extra chunk contents are ignored; missing contents for any requested address
446/// are treated as corrupted upload state.
447#[cfg(test)]
448pub(crate) fn chunk_contents_for_upload_addresses(
449    chunk_contents: Vec<Bytes>,
450    addresses: &[[u8; 32]],
451) -> Result<Vec<Bytes>> {
452    if addresses.is_empty() {
453        return Ok(Vec::new());
454    }
455
456    let mut needed_by_address: HashMap<[u8; 32], usize> = HashMap::new();
457    for address in addresses {
458        *needed_by_address.entry(*address).or_default() += 1;
459    }
460
461    let mut chunks_by_address: HashMap<[u8; 32], VecDeque<Bytes>> =
462        HashMap::with_capacity(needed_by_address.len());
463    let mut remaining = addresses.len();
464    for chunk in chunk_contents {
465        let address = compute_address(&chunk);
466        if let Some(needed) = needed_by_address.get_mut(&address) {
467            if *needed > 0 {
468                chunks_by_address
469                    .entry(address)
470                    .or_default()
471                    .push_back(chunk);
472                *needed -= 1;
473                remaining -= 1;
474                if remaining == 0 {
475                    break;
476                }
477            }
478        }
479    }
480
481    for (address, needed) in &needed_by_address {
482        if *needed == 0 {
483            continue;
484        }
485
486        if chunks_by_address.contains_key(address) {
487            return Err(Error::InvalidData(format!(
488                "missing duplicate chunk content for merkle address {}",
489                hex::encode(address)
490            )));
491        }
492
493        return Err(Error::InvalidData(format!(
494            "missing chunk content for merkle address {}",
495            hex::encode(address)
496        )));
497    }
498
499    let mut selected = Vec::with_capacity(addresses.len());
500    for address in addresses {
501        let chunks = chunks_by_address.get_mut(address).ok_or_else(|| {
502            Error::InvalidData(format!(
503                "missing chunk content for merkle address {}",
504                hex::encode(address)
505            ))
506        })?;
507        let chunk = chunks.pop_front().ok_or_else(|| {
508            Error::InvalidData(format!(
509                "missing duplicate chunk content for merkle address {}",
510                hex::encode(address)
511            ))
512        })?;
513        selected.push(chunk);
514    }
515
516    Ok(selected)
517}
518
519/// Map a per-chunk quote-collection result to its merkle-preflight stored
520/// status, degrading gracefully on transient failures.
521///
522/// The preflight only needs to answer "is this chunk already on the network?"
523/// so it can be skipped before payment — it is an optimisation, never a gate.
524/// Therefore:
525/// - `Ok(_)` (quotes gathered) → `Ok(false)`: chunk is not stored, upload it.
526/// - `Err(AlreadyStored)` → `Ok(true)`: skip it.
527/// - a transient failure (timeout / insufficient peers / transport error) →
528///   `Ok(false)`: we could not confirm, so queue it for upload rather than
529///   aborting the whole batch. Re-storing an existing chunk is idempotent.
530/// - any other (application) error → propagate; it would recur on a healthy
531///   link and is not something the preflight should paper over.
532///
533/// Without this, a single chunk's transient quote timeout failed the entire
534/// upload — fatal for forced `--merkle` uploads, which (unlike `Auto`) have no
535/// wave-batch fallback, and catastrophic for large files via the multiplicative
536/// per-chunk effect.
537fn preflight_stored_status<T>(result: Result<T>) -> Result<bool> {
538    match result {
539        Ok(_) => Ok(false),
540        Err(Error::AlreadyStored) => Ok(true),
541        Err(e) if matches!(classify_error(&e), Outcome::Timeout | Outcome::NetworkError) => {
542            Ok(false)
543        }
544        Err(e) => Err(e),
545    }
546}
547
548/// Split `total` addresses into the merkle batches an upload is actually paid in.
549///
550/// Every batch becomes one `MerkleTree`, and a tree is only valid with
551/// `2..=MAX_LEAVES` leaves. The obvious `addresses.chunks(MAX_LEAVES)` split
552/// respects the upper bound but not the lower one: 257 addresses come out as
553/// `[256, 1]`, the 256-address batch is paid for on-chain, and the singleton
554/// remainder cannot build a tree — so the upload fails as a *paid* partial.
555/// Every count congruent to 1 modulo `MAX_LEAVES` (257, 513, 769, …) hits it,
556/// and the merkle preflight can leave an arbitrary count behind.
557///
558/// Borrowing one address from the preceding batch removes the case entirely:
559/// 257 splits as `[255, 2]` and 513 as `[256, 255, 2]`. Order is preserved and
560/// no address is duplicated or synthesised, so the partition is a plain
561/// in-order cover of the input.
562///
563/// Returns an empty vector for `total < 2`, which no merkle path may pay for —
564/// `pay_for_merkle_batch` rejects those counts up front.
565#[must_use]
566pub fn merkle_batch_sizes(total: usize) -> Vec<usize> {
567    merkle_batch_sizes_with_cap(total, MAX_LEAVES)
568}
569
570/// [`merkle_batch_sizes`] with an explicit per-batch leaf cap.
571///
572/// `cap` is clamped to `3..=MAX_LEAVES`: above `MAX_LEAVES` the contract's
573/// depth bound rejects the tree, and below 3 the partition is unsound — with
574/// a cap of 2 every odd total needs a 1-leaf part, which cannot build a
575/// tree (parts of 3 and 2 compose any total ≥ 2, so 3 is the smallest safe
576/// cap). Production callers use [`merkle_batch_sizes`]
577/// (cap = `MAX_LEAVES`); a smaller cap lets tests exercise real multi-batch
578/// signing with kilobyte files (ADR-0003).
579#[must_use]
580pub fn merkle_batch_sizes_with_cap(total: usize, cap: usize) -> Vec<usize> {
581    if total < 2 {
582        return Vec::new();
583    }
584    let cap = cap.clamp(3, MAX_LEAVES);
585
586    let mut sizes = Vec::with_capacity(total.div_ceil(cap));
587    let mut remaining = total;
588    while remaining > cap {
589        // Taking a full cap here would strand a single address as the
590        // final batch; take one fewer so the tail is a payable two-leaf tree.
591        let take = if remaining - cap == 1 { cap - 1 } else { cap };
592        sizes.push(take);
593        remaining -= take;
594    }
595    sizes.push(remaining);
596    sizes
597}
598
599/// Split `addresses` into the sub-batches [`merkle_batch_sizes`] describes.
600///
601/// The slices borrow `addresses` in order, so the partition cannot introduce a
602/// duplicate or a synthetic address.
603#[must_use]
604pub fn merkle_batch_partitions(addresses: &[[u8; 32]]) -> Vec<&[[u8; 32]]> {
605    merkle_batch_partitions_with_cap(addresses, MAX_LEAVES)
606}
607
608/// [`merkle_batch_partitions`] under an explicit per-batch leaf cap
609/// (see [`merkle_batch_sizes_with_cap`] for the clamping rules).
610#[must_use]
611pub fn merkle_batch_partitions_with_cap(addresses: &[[u8; 32]], cap: usize) -> Vec<&[[u8; 32]]> {
612    let mut partitions = Vec::new();
613    let mut rest = addresses;
614    for size in merkle_batch_sizes_with_cap(addresses.len(), cap) {
615        let (batch, tail) = rest.split_at(size);
616        partitions.push(batch);
617        rest = tail;
618    }
619    partitions
620}
621
622/// Fold per-batch [`MerkleBatchPaymentResult`]s into one combined receipt.
623///
624/// Mirrors the fold `pay_for_merkle_multi_batch` performs on the wallet path:
625/// proofs merge by extension, costs sum, and the combined
626/// `merkle_payment_timestamp` is the **oldest** sub-batch timestamp so expiry
627/// checks use the worst case. Batches the external signer never paid simply
628/// do not appear in `results` — their chunks end up with no proof, which the
629/// store path reports through `PartialUpload` (ADR-0003).
630#[must_use]
631#[cfg(any(feature = "native", test))]
632pub(crate) fn merge_merkle_batch_results(
633    results: Vec<MerkleBatchPaymentResult>,
634) -> MerkleBatchPaymentResult {
635    let mut merged = MerkleBatchPaymentResult {
636        proofs: HashMap::new(),
637        chunk_count: 0,
638        storage_cost_atto: "0".to_string(),
639        gas_cost_wei: 0,
640        merkle_payment_timestamp: 0,
641    };
642    let mut total_storage = Amount::ZERO;
643    for result in results {
644        merged.proofs.extend(result.proofs);
645        merged.chunk_count += result.chunk_count;
646        if let Ok(cost) = result.storage_cost_atto.parse::<Amount>() {
647            total_storage += cost;
648        }
649        merged.gas_cost_wei = merged.gas_cost_wei.saturating_add(result.gas_cost_wei);
650        if merged.merkle_payment_timestamp == 0
651            || (result.merkle_payment_timestamp > 0
652                && result.merkle_payment_timestamp < merged.merkle_payment_timestamp)
653        {
654            merged.merkle_payment_timestamp = result.merkle_payment_timestamp;
655        }
656    }
657    merged.storage_cost_atto = total_storage.to_string();
658    merged
659}
660
661/// Leaves one merkle batch of `batch_size` addresses is billed for.
662///
663/// `MerkleTree` pads its leaf count up to a power of two and the vault charges
664/// `median16 × 2^depth`, so a 65-address batch pays for 128 leaves.
665fn padded_leaf_count(batch_size: usize) -> u64 {
666    // Saturate rather than wrap: a batch is at most MAX_LEAVES, so the
667    // overflow arm is unreachable, and erring high never under-quotes.
668    let padded = batch_size
669        .max(2)
670        .checked_next_power_of_two()
671        .unwrap_or(usize::MAX);
672    u64::try_from(padded).unwrap_or(u64::MAX)
673}
674
675/// Leaves a merkle upload of `chunk_count` chunks is actually billed for.
676///
677/// Summed over the batches [`merkle_batch_sizes`] will really pay for, so the
678/// estimate and the execution path share one batching model rather than two
679/// that can drift. Billing the raw chunk count would under-quote a
680/// non-power-of-two batch by up to 2×, and an under-quote is the harmful
681/// direction: a caller sizing its wallet from the estimate runs dry
682/// mid-upload.
683#[must_use]
684pub fn merkle_billable_leaves(chunk_count: u64) -> u64 {
685    let total = usize::try_from(chunk_count).unwrap_or(usize::MAX);
686    let batches = merkle_batch_sizes(total);
687    if batches.is_empty() {
688        // No payable partition exists below two chunks. Nothing costs nothing;
689        // a lone chunk is still quoted as the two-leaf minimum a tree needs.
690        return if total == 0 { 0 } else { 2 };
691    }
692
693    batches
694        .into_iter()
695        .map(padded_leaf_count)
696        .fold(0u64, u64::saturating_add)
697}
698
699/// Reject an address set that cannot be prepared as a single merkle tree.
700///
701/// The wallet path splits an oversized upload with [`merkle_batch_sizes`] and
702/// pays each batch in its own transaction. The external-signer contract is one
703/// prepared batch → one signature → one payment, so it has no way to express
704/// that split. Refusing before any candidate collection or on-chain spend is
705/// the honest answer; silently switching the caller to a different payment
706/// model is not.
707fn ensure_single_merkle_tree_batch(address_count: usize) -> Result<()> {
708    if address_count > MAX_LEAVES {
709        return Err(Error::MerkleBatchTooLarge {
710            addresses: address_count,
711            max_leaves: MAX_LEAVES,
712        });
713    }
714    Ok(())
715}
716
717/// Determine whether to use merkle payments for a given batch size.
718/// Free function — no Client needed.
719#[must_use]
720pub fn should_use_merkle(chunk_count: usize, mode: PaymentMode) -> bool {
721    match mode {
722        PaymentMode::Auto => chunk_count >= DEFAULT_MERKLE_THRESHOLD,
723        PaymentMode::Merkle => chunk_count >= 2,
724        PaymentMode::Single => false,
725    }
726}
727
728impl Client {
729    /// Determine whether to use merkle payments for a given batch size.
730    #[must_use]
731    pub fn should_use_merkle(&self, chunk_count: usize, mode: PaymentMode) -> bool {
732        should_use_merkle(chunk_count, mode)
733    }
734
735    /// Pay for a batch of chunks using merkle batch payment.
736    ///
737    /// Builds a merkle tree, collects candidate pools, pays on-chain in one tx,
738    /// and returns per-chunk proofs. Anything longer than `MAX_LEAVES` is split
739    /// by [`merkle_batch_sizes`] and paid one transaction per sub-batch.
740    ///
741    /// This low-level helper assumes the caller has already selected the
742    /// addresses that need payment. User-facing upload paths first run the
743    /// merkle upload planner to skip chunks already stored on the network.
744    ///
745    /// # Errors
746    ///
747    /// Returns an error if the batch is too small, candidate collection fails,
748    /// on-chain payment fails, or proof generation fails.
749    pub async fn pay_for_merkle_batch(
750        &self,
751        addresses: &[[u8; 32]],
752        data_type: u32,
753        data_size: u64,
754    ) -> Result<MerkleBatchPaymentResult> {
755        // A refusal established by any earlier upload on this client stops
756        // this one before it spends. The verdict is about this build, not
757        // about one operation, so an upload that started after another had
758        // already been told the settlement rules are wrong must not pay.
759        if let Some(refusal) = self.corroborated_settlement_refusal() {
760            return Err(Error::ClientUpdateRequired(refusal));
761        }
762        let chunk_count = addresses.len();
763        if chunk_count < 2 {
764            return Err(Error::Payment(
765                "Merkle batch payment requires at least 2 chunks".to_string(),
766            ));
767        }
768
769        if chunk_count > MAX_LEAVES {
770            return self
771                .pay_for_merkle_multi_batch(addresses, data_type, data_size)
772                .await;
773        }
774
775        self.pay_for_merkle_single_batch(addresses, data_type, data_size)
776            .await
777    }
778
779    /// Check which chunks in a merkle upload still need payment/storage.
780    ///
781    /// Uses the normal per-chunk quote path because it already has the
782    /// close-group majority rule for `AlreadyStored`. Non-stored chunks only
783    /// use the quote response as a probe; their actual payment still happens
784    /// through the merkle batch.
785    ///
786    /// `chunks` contains `(address, data_size)` pairs.
787    ///
788    /// Native-only: the sole caller is the filesystem upload path in
789    /// `data::client::file::native`, so a browser build would see it as dead code.
790    #[cfg(feature = "native")]
791    pub(crate) async fn plan_merkle_upload(
792        &self,
793        chunks: Vec<([u8; 32], u64)>,
794        data_type: u32,
795        progress: Option<&mpsc::Sender<UploadEvent>>,
796    ) -> Result<MerkleUploadPlan> {
797        self.plan_merkle_upload_observed(chunks, data_type, progress, &|_, _, _, _| {})
798            .await
799    }
800
801    pub(crate) async fn plan_merkle_upload_observed(
802        &self,
803        chunks: Vec<([u8; 32], u64)>,
804        data_type: u32,
805        progress: Option<&mpsc::Sender<UploadEvent>>,
806        on_checked: &impl Fn([u8; 32], usize, usize, bool),
807    ) -> Result<MerkleUploadPlan> {
808        let total_chunks = chunks.len();
809        if total_chunks == 0 {
810            return Ok(MerkleUploadPlan::default());
811        }
812
813        info!("Checking {total_chunks} merkle chunks for existing storage before payment");
814
815        let quote_limiter = self.controller().quote.clone();
816        let quote_concurrency = quote_limiter.current().min(total_chunks.max(1));
817        let mut check_stream = crate::client_engine::bounded_unordered(
818            chunks
819                .into_iter()
820                .enumerate()
821                .map(|(index, (address, data_size))| {
822                    let limiter = quote_limiter.clone();
823                    async move {
824                        let result = observe_op(
825                            &limiter,
826                            || async move {
827                                self.chunk_already_stored_for_merkle(&address, data_type, data_size)
828                                    .await
829                            },
830                            classify_error,
831                        )
832                        .await;
833                        (index, address, data_size, result)
834                    }
835                }),
836            quote_concurrency,
837        );
838
839        let mut already_stored: Vec<(usize, [u8; 32])> = Vec::new();
840        let mut to_upload: Vec<(usize, [u8; 32], u64)> = Vec::new();
841        let mut checked = 0usize;
842
843        while let Some((index, address, data_size, result)) = check_stream.next().await {
844            let is_already_stored = result?;
845            checked += 1;
846            on_checked(address, checked, total_chunks, is_already_stored);
847
848            if let Some(tx) = progress {
849                let _ = tx.try_send(UploadEvent::ChunkQuoted {
850                    quoted: checked,
851                    total: total_chunks,
852                });
853            }
854
855            if is_already_stored {
856                debug!(
857                    "Merkle preflight {checked}/{total_chunks}: chunk {} already stored",
858                    hex::encode(address)
859                );
860                already_stored.push((index, address));
861                if let Some(tx) = progress {
862                    let _ = tx.try_send(UploadEvent::ChunkStored {
863                        stored: already_stored.len(),
864                        total: total_chunks,
865                    });
866                }
867            } else {
868                debug!(
869                    "Merkle preflight {checked}/{total_chunks}: chunk {} needs upload",
870                    hex::encode(address)
871                );
872                to_upload.push((index, address, data_size));
873            }
874        }
875
876        already_stored.sort_by_key(|(index, _)| *index);
877        to_upload.sort_by_key(|(index, _, _)| *index);
878
879        let to_upload_total_bytes = to_upload.iter().fold(0u64, |acc, (_, _, data_size)| {
880            acc.saturating_add(*data_size)
881        });
882
883        let already_stored = already_stored
884            .into_iter()
885            .map(|(_, address)| address)
886            .collect::<Vec<_>>();
887        let to_upload = to_upload
888            .into_iter()
889            .map(|(_, address, _)| address)
890            .collect::<Vec<_>>();
891
892        info!(
893            "Merkle preflight complete: {} already stored, {} need upload",
894            already_stored.len(),
895            to_upload.len()
896        );
897
898        Ok(MerkleUploadPlan {
899            already_stored,
900            to_upload,
901            to_upload_total_bytes,
902        })
903    }
904
905    async fn chunk_already_stored_for_merkle(
906        &self,
907        address: &[u8; 32],
908        data_type: u32,
909        data_size: u64,
910    ) -> Result<bool> {
911        let result = self
912            .get_store_quotes_with_fault_tolerance(address, data_size, data_type)
913            .await;
914        if let Err(e) = &result {
915            if matches!(classify_error(e), Outcome::Timeout | Outcome::NetworkError) {
916                debug!(
917                    "Merkle preflight: could not determine stored status for {} ({e}); \
918                     treating as not stored and queuing for upload",
919                    hex::encode(address)
920                );
921            }
922        }
923        preflight_stored_status(result)
924    }
925
926    /// Phase 1 of external-signer merkle payment for an address set of any
927    /// size: partition into `MerkleTree`-sized sub-batches and prepare each.
928    ///
929    /// The partition follows [`merkle_batch_partitions_with_cap`] (≤ `cap`
930    /// leaves per batch, singleton-remainder rebalanced), so the external
931    /// signer pays one transaction per returned batch — the same shape the
932    /// wallet path's `pay_for_merkle_multi_batch` signs internally
933    /// (ADR-0003). Batch order matches address order; the caller's finalize
934    /// supplies one winner hash per batch in the same order.
935    ///
936    /// `cap` is clamped to `3..=MAX_LEAVES` (see
937    /// [`merkle_batch_sizes_with_cap`] for why 3 is the floor); production
938    /// callers pass `MAX_LEAVES`, tests pass small caps to get real
939    /// multi-batch flows from kilobyte files.
940    ///
941    /// # Errors
942    ///
943    /// Returns an error if any sub-batch's candidate collection fails.
944    /// Nothing is spent in either case — payment happens externally after
945    /// this returns.
946    pub async fn prepare_merkle_batches_external(
947        &self,
948        addresses: &[[u8; 32]],
949        data_type: u32,
950        data_size: u64,
951        cap: usize,
952    ) -> Result<Vec<PreparedMerkleBatch>> {
953        if addresses.len() < 2 {
954            return Err(Error::Payment(
955                "Merkle batch payment requires at least 2 chunks".to_string(),
956            ));
957        }
958        let partitions = merkle_batch_partitions_with_cap(addresses, cap);
959        let total = partitions.len();
960        let mut batches = Vec::with_capacity(total);
961        for (i, partition) in partitions.into_iter().enumerate() {
962            debug!(
963                "Preparing external merkle sub-batch {}/{total} ({} chunks)",
964                i + 1,
965                partition.len()
966            );
967            batches.push(
968                self.prepare_merkle_batch_external(partition, data_type, data_size)
969                    .await?,
970            );
971        }
972        Ok(batches)
973    }
974
975    /// Phase 1 of external-signer merkle payment: prepare batch without paying.
976    ///
977    /// Builds the merkle tree, collects candidate pools from the network,
978    /// and returns the data needed for the on-chain payment call.
979    /// Requires `EvmNetwork` but NOT a wallet.
980    ///
981    /// # Errors
982    ///
983    /// Returns [`Error::MerkleBatchTooLarge`] if `addresses` holds more than
984    /// `MAX_LEAVES` entries. One prepared batch is one signature and one
985    /// payment, so an oversized set has no valid external-signing form; the
986    /// wallet path splits it across transactions instead
987    /// ([`Client::prepare_merkle_batches_external`] is the partitioned
988    /// equivalent for external signing). The check runs before any candidate
989    /// collection, so nothing is spent.
990    pub async fn prepare_merkle_batch_external(
991        &self,
992        addresses: &[[u8; 32]],
993        data_type: u32,
994        data_size: u64,
995    ) -> Result<PreparedMerkleBatch> {
996        self.prepare_merkle_batch_external_observed(addresses, data_type, data_size, &|_, _| {})
997            .await
998    }
999
1000    pub(crate) async fn prepare_merkle_batch_external_observed(
1001        &self,
1002        addresses: &[[u8; 32]],
1003        data_type: u32,
1004        data_size: u64,
1005        on_pool: &impl Fn(usize, usize),
1006    ) -> Result<PreparedMerkleBatch> {
1007        // A refusal established by any earlier upload on this client stops
1008        // this one before it spends. The verdict is about this build, not
1009        // about one operation, so an upload that started after another had
1010        // already been told the settlement rules are wrong must not pay.
1011        if let Some(refusal) = self.corroborated_settlement_refusal() {
1012            return Err(Error::ClientUpdateRequired(refusal));
1013        }
1014        ensure_single_merkle_tree_batch(addresses.len())?;
1015
1016        let chunk_count = addresses.len();
1017        let xornames: Vec<XorName> = addresses.iter().map(|a| XorName(*a)).collect();
1018
1019        debug!("Building merkle tree for {chunk_count} chunks");
1020
1021        // 1. Build merkle tree
1022        let tree = MerkleTree::from_xornames(xornames)
1023            .map_err(|e| Error::Payment(format!("Failed to build merkle tree: {e}")))?;
1024
1025        let depth = tree.depth();
1026        let merkle_payment_timestamp = web_time::SystemTime::now()
1027            .duration_since(web_time::UNIX_EPOCH)
1028            .map_err(|e| Error::Payment(format!("System time error: {e}")))?
1029            .as_secs();
1030
1031        debug!("Merkle tree: depth={depth}, leaves={chunk_count}, ts={merkle_payment_timestamp}");
1032
1033        // 2. Get reward candidates (midpoint proofs)
1034        let midpoint_proofs = tree
1035            .reward_candidates(merkle_payment_timestamp)
1036            .map_err(|e| Error::Payment(format!("Failed to generate reward candidates: {e}")))?;
1037
1038        debug!(
1039            "Collecting candidate pools from {} midpoints (concurrent)",
1040            midpoint_proofs.len()
1041        );
1042
1043        // 3. Collect candidate pools from the network (all pools in parallel).
1044        //    Each candidate's ADR-0004 binding is fully verified during
1045        //    collection (shape, cap, exact price, commitment resolution); the
1046        //    sidecars themselves are consumed by that validation and NOT
1047        //    forwarded in the PUT bundles (see `finalize_merkle_batch`).
1048        let candidate_pools = self
1049            .build_candidate_pools(
1050                &midpoint_proofs,
1051                data_type,
1052                data_size,
1053                merkle_payment_timestamp,
1054                on_pool,
1055            )
1056            .await?;
1057
1058        // 4. Build pool commitments for on-chain payment. Every candidate's
1059        //    payable amount carries MERKLE_PAYMENT_MULTIPLIER so a merkle
1060        //    chunk settles for the same amount a single-node chunk does; the
1061        //    signed candidate prices and the pool hashes are untouched.
1062        let pool_commitments: Vec<PoolCommitment> = candidate_pools
1063            .iter()
1064            .map(pool_commitment_with_payment_multiplier)
1065            .collect::<Result<Vec<_>>>()?;
1066
1067        Ok(PreparedMerkleBatch {
1068            depth,
1069            pool_commitments,
1070            merkle_payment_timestamp,
1071            candidate_pools,
1072            tree,
1073            addresses: addresses.to_vec(),
1074        })
1075    }
1076
1077    /// Pay for a single batch (up to `MAX_LEAVES` chunks).
1078    async fn pay_for_merkle_single_batch(
1079        &self,
1080        addresses: &[[u8; 32]],
1081        data_type: u32,
1082        data_size: u64,
1083    ) -> Result<MerkleBatchPaymentResult> {
1084        let wallet = self.require_wallet()?;
1085        let prepared = self
1086            .prepare_merkle_batch_external(addresses, data_type, data_size)
1087            .await?;
1088
1089        info!(
1090            "Submitting merkle batch payment on-chain (depth={})",
1091            prepared.depth
1092        );
1093        let (winner_pool_hash, amount, gas_info) = wallet
1094            .pay_for_merkle_tree(
1095                prepared.depth,
1096                prepared.pool_commitments.clone(),
1097                prepared.merkle_payment_timestamp,
1098            )
1099            .await
1100            .map_err(|e| Error::Payment(format!("Merkle batch payment failed: {e}")))?;
1101
1102        info!(
1103            "Merkle payment succeeded: winner pool {}",
1104            hex::encode(winner_pool_hash)
1105        );
1106
1107        let mut result = finalize_merkle_batch(prepared, winner_pool_hash)?;
1108        result.storage_cost_atto = amount.to_string();
1109        result.gas_cost_wei = gas_info.gas_cost_wei;
1110        Ok(result)
1111    }
1112
1113    /// Handle batches larger than `MAX_LEAVES` by splitting into sub-batches.
1114    async fn pay_for_merkle_multi_batch(
1115        &self,
1116        addresses: &[[u8; 32]],
1117        data_type: u32,
1118        data_size: u64,
1119    ) -> Result<MerkleBatchPaymentResult> {
1120        // Partition with the shared helper, NOT `chunks(MAX_LEAVES)`: the naive
1121        // split leaves a one-address final batch for every count congruent to 1
1122        // modulo MAX_LEAVES, which cannot build a tree and so turns a paid
1123        // upload into a partial failure.
1124        let sub_batches = merkle_batch_partitions(addresses);
1125        let total_sub_batches = sub_batches.len();
1126        let mut all_proofs = HashMap::with_capacity(addresses.len());
1127        let mut total_storage = Amount::ZERO;
1128        let mut total_gas: u128 = 0;
1129        // Track the oldest sub-batch timestamp so the overall receipt
1130        // expires when the *first* sub-batch's on-chain payment ages
1131        // out (worst case for resume).
1132        let mut oldest_ts: u64 = 0;
1133
1134        for (i, chunk) in sub_batches.into_iter().enumerate() {
1135            match self
1136                .pay_for_merkle_single_batch(chunk, data_type, data_size)
1137                .await
1138            {
1139                Ok(sub_result) => {
1140                    if let Ok(cost) = sub_result.storage_cost_atto.parse::<Amount>() {
1141                        total_storage += cost;
1142                    }
1143                    total_gas = total_gas.saturating_add(sub_result.gas_cost_wei);
1144                    if oldest_ts == 0
1145                        || (sub_result.merkle_payment_timestamp > 0
1146                            && sub_result.merkle_payment_timestamp < oldest_ts)
1147                    {
1148                        oldest_ts = sub_result.merkle_payment_timestamp;
1149                    }
1150                    all_proofs.extend(sub_result.proofs);
1151                }
1152                Err(e) => {
1153                    if all_proofs.is_empty() {
1154                        // First sub-batch failed, nothing paid yet -- propagate directly.
1155                        return Err(e);
1156                    }
1157                    // A storer saying this client cannot settle is terminal
1158                    // even mid-batch. Folding it into a partial result would
1159                    // report success, hide the upgrade instruction, and leave
1160                    // the caller to rediscover the same refusal on its next
1161                    // upload. Every remaining sub-batch would be refused the
1162                    // same way, so there is nothing to salvage by continuing.
1163                    if matches!(e, Error::ClientUpdateRequired(_)) {
1164                        // Do NOT return Err here. The caller writes the receipt
1165                        // cache only on the Ok path, so failing the call would
1166                        // discard proofs for sub-batches whose payment has
1167                        // already settled on-chain and cannot be undone. That is
1168                        // the very destruction this work exists to stop.
1169                        //
1170                        // The verdict is not lost by returning the partial
1171                        // result: it was latched client-wide when it was
1172                        // corroborated, so the next quote collection refuses
1173                        // before spending anything and the upgrade instruction
1174                        // reaches the user from there.
1175                        warn!(
1176                            "Merkle sub-batch {}/{total_sub_batches}: storers refused this \
1177                             client's settlement version. Returning {} proofs from \
1178                             already-paid sub-batches so that spend is not stranded; the \
1179                             refusal is latched and will stop the next payment.",
1180                            i + 1,
1181                            all_proofs.len()
1182                        );
1183                    }
1184                    // Return partial result so caller can still store already-paid chunks.
1185                    warn!(
1186                        "Merkle sub-batch {}/{total_sub_batches} failed: {e}. \
1187                         Returning {} proofs from prior sub-batches",
1188                        i + 1,
1189                        all_proofs.len()
1190                    );
1191                    return Ok(MerkleBatchPaymentResult {
1192                        chunk_count: all_proofs.len(),
1193                        proofs: all_proofs,
1194                        storage_cost_atto: total_storage.to_string(),
1195                        gas_cost_wei: total_gas,
1196                        merkle_payment_timestamp: oldest_ts,
1197                    });
1198                }
1199            }
1200        }
1201
1202        Ok(MerkleBatchPaymentResult {
1203            chunk_count: addresses.len(),
1204            proofs: all_proofs,
1205            storage_cost_atto: total_storage.to_string(),
1206            gas_cost_wei: total_gas,
1207            merkle_payment_timestamp: oldest_ts,
1208        })
1209    }
1210
1211    /// Build candidate pools by querying the network for each midpoint (concurrently).
1212    async fn build_candidate_pools(
1213        &self,
1214        midpoint_proofs: &[MidpointProof],
1215        data_type: u32,
1216        data_size: u64,
1217        merkle_payment_timestamp: u64,
1218        on_pool: &impl Fn(usize, usize),
1219    ) -> Result<Vec<MerklePaymentCandidatePool>> {
1220        on_pool(0, midpoint_proofs.len());
1221        let mut pool_futures = FuturesUnordered::new();
1222
1223        for midpoint_proof in midpoint_proofs {
1224            let pool_address = midpoint_proof.address();
1225            let mp = midpoint_proof.clone();
1226            pool_futures.push(async move {
1227                let candidate_nodes = self
1228                    .get_merkle_candidate_pool(
1229                        &pool_address.0,
1230                        data_type,
1231                        data_size,
1232                        merkle_payment_timestamp,
1233                    )
1234                    .await?;
1235                Ok::<_, Error>(MerklePaymentCandidatePool {
1236                    midpoint_proof: mp,
1237                    candidate_nodes,
1238                })
1239            });
1240        }
1241
1242        // Drained rather than short-circuited. Returning on the first error
1243        // would drop `pool_futures`, cancelling every pool still in flight, and
1244        // one of those may be carrying the second refusal that corroborates a
1245        // verdict about this client. A refusal lost that way is a refusal the
1246        // latch never sees, and the caller can fall back to wave payment and
1247        // spend. This is the same rule the single-node collector follows: stop
1248        // making progress, but never drop a verdict that is already in flight.
1249        //
1250        // A refusal outranks an ordinary failure for the same reason. If one
1251        // pool runs out of peers while another declares this client unable to
1252        // settle, the second is the answer worth returning.
1253        let mut pools = Vec::with_capacity(midpoint_proofs.len());
1254        let mut verdict = PoolVerdict::default();
1255        while let Some(result) = pool_futures.next().await {
1256            match result {
1257                Ok(pool) => {
1258                    pools.push(pool);
1259                    on_pool(pools.len(), midpoint_proofs.len());
1260                }
1261                Err(e) => verdict.note(e),
1262            }
1263        }
1264        if let Some(e) = verdict.into_error() {
1265            return Err(e);
1266        }
1267
1268        Ok(pools)
1269    }
1270
1271    /// Collect `CANDIDATES_PER_POOL` (16) merkle candidate quotes from the network.
1272    #[allow(clippy::too_many_lines)]
1273    async fn get_merkle_candidate_pool(
1274        &self,
1275        address: &[u8; 32],
1276        data_type: u32,
1277        data_size: u64,
1278        merkle_payment_timestamp: u64,
1279    ) -> Result<[MerklePaymentCandidateNode; CANDIDATES_PER_POOL]> {
1280        let node = self.network();
1281        let timeout = Duration::from_secs(self.config().quote_timeout_secs);
1282
1283        // Query extra peers to handle validation failures (bad sigs, wrong type, etc.)
1284        let query_count = CANDIDATES_PER_POOL * 2;
1285        let mut remote_peers = self
1286            .network()
1287            .find_closest_peers(address, query_count)
1288            .await?;
1289
1290        // If DHT closest-nodes didn't return enough, supplement with connected peers.
1291        // On small networks the DHT iterative lookup may not discover enough peers
1292        // close to a random pool address, but we know more peers via direct connections.
1293        if remote_peers.len() < CANDIDATES_PER_POOL {
1294            let connected = self.network().connected_peers().await;
1295            for peer in connected {
1296                if !remote_peers.iter().any(|(id, _)| *id == peer) {
1297                    remote_peers.push((peer, vec![]));
1298                }
1299            }
1300        }
1301
1302        if remote_peers.len() < CANDIDATES_PER_POOL {
1303            return Err(Error::InsufficientPeers(format!(
1304                "Found {} peers, need {CANDIDATES_PER_POOL} for merkle candidate pool. \
1305                 Use --no-merkle or a larger network.",
1306                remote_peers.len()
1307            )));
1308        }
1309
1310        let mut candidate_futures = FuturesUnordered::new();
1311
1312        let unversioned_peers = self.unversioned_quote_peers();
1313        let versioned_capable = self.versioned_quote_capable_handle();
1314
1315        for (peer_id, peer_addrs) in &remote_peers {
1316            let request_id = self.next_request_id();
1317            // A peer that already failed to answer a versioned request is
1318            // asked in the legacy shape directly. Re-probing costs a full
1319            // per-peer timeout every time, and a merkle pool asks sixteen
1320            // candidates per pool, so against a fleet that predates the
1321            // versioned requests the probes dominate the run: measured on this
1322            // suite it went from ~24 minutes to past the 60-minute CI cap.
1323            // The capable set wins. The two sets are updated under separate
1324            // locks, so a slow probe can insert into the legacy set after a
1325            // concurrent request has already proved the peer capable; letting
1326            // capability win makes that interleaving harmless instead of
1327            // permanently preferring the legacy shape.
1328            let known_legacy = !versioned_capable
1329                .lock()
1330                .is_ok_and(|peers| peers.contains(peer_id))
1331                && unversioned_peers
1332                    .lock()
1333                    .is_ok_and(|peers| peers.contains(peer_id));
1334
1335            let legacy_request = MerkleCandidateQuoteRequest {
1336                address: *address,
1337                data_type,
1338                data_size,
1339                merkle_payment_timestamp,
1340            };
1341
1342            // Declare the settlement version so a storer can turn us away
1343            // before we pay, rather than refusing the payment afterwards.
1344            let message = ChunkMessage {
1345                request_id,
1346                body: if known_legacy {
1347                    ChunkMessageBody::MerkleCandidateQuoteRequest(legacy_request.clone())
1348                } else {
1349                    ChunkMessageBody::MerkleCandidateQuoteRequestV2(
1350                        MerkleCandidateQuoteRequestV2::new(
1351                            *address,
1352                            data_size,
1353                            merkle_payment_timestamp,
1354                        ),
1355                    )
1356                },
1357            };
1358
1359            let message_bytes = match message.encode() {
1360                Ok(bytes) => bytes,
1361                Err(e) => {
1362                    warn!("Failed to encode merkle candidate request for {peer_id}: {e}");
1363                    continue;
1364                }
1365            };
1366
1367            // Fallback for the mixed fleet. A storer that predates the
1368            // versioned request cannot decode it and simply never answers, so
1369            // without this every quote would fail until the whole network had
1370            // upgraded. Retried only on a transport-level failure, never on a
1371            // refusal: see `is_version_unaware` below.
1372            //
1373            // Delete this, and the second request id, once the fleet is known
1374            // to answer V2.
1375            let legacy_request_id = self.next_request_id();
1376            let legacy_message_bytes = match (ChunkMessage {
1377                request_id: legacy_request_id,
1378                body: ChunkMessageBody::MerkleCandidateQuoteRequest(legacy_request),
1379            })
1380            .encode()
1381            {
1382                Ok(bytes) => bytes,
1383                Err(e) => {
1384                    warn!("Failed to encode legacy merkle candidate request for {peer_id}: {e}");
1385                    continue;
1386                }
1387            };
1388
1389            let peer_id_clone = *peer_id;
1390            let addrs_clone = peer_addrs.clone();
1391            let node_clone = node.clone();
1392            let peers_handle = Arc::clone(&unversioned_peers);
1393            let capable_handle = Arc::clone(&versioned_capable);
1394
1395            let fut = async move {
1396                // First contact waits only long enough to learn whether the
1397                // peer can parse the shape. A peer already known to be legacy
1398                // gets the caller's full patience, because that request is the
1399                // real one rather than a probe.
1400                let attempt_timeout = if known_legacy {
1401                    timeout
1402                } else {
1403                    timeout.min(crate::data::client::VERSIONED_QUOTE_PROBE_CEILING)
1404                };
1405
1406                let result = send_and_await_chunk_response(
1407                    &node_clone,
1408                    &peer_id_clone,
1409                    message_bytes,
1410                    request_id,
1411                    attempt_timeout,
1412                    &addrs_clone,
1413                    |body| map_merkle_candidate_response(peer_id_clone, body),
1414                    |e| {
1415                        Error::Network(format!(
1416                            "Failed to send merkle candidate request to {peer_id_clone}: {e}"
1417                        ))
1418                    },
1419                    || {
1420                        Error::Timeout(format!(
1421                            "Timeout waiting for merkle candidate from {peer_id_clone}"
1422                        ))
1423                    },
1424                )
1425                .await;
1426
1427                // Any answer at all to the versioned shape proves the peer
1428                // can parse it, so it can never later be demoted.
1429                // Any recognised answer proves the peer parsed the versioned
1430                // shape, including a structured error. Only silence and send
1431                // failures leave the question open.
1432                let answered = match &result {
1433                    Ok(_) => true,
1434                    Err(e) => !is_version_unaware(e),
1435                };
1436                if !known_legacy && answered {
1437                    if let Ok(mut peers) = capable_handle.lock() {
1438                        peers.insert(peer_id_clone);
1439                    }
1440                }
1441
1442                // Silence from a storer means it could not decode the
1443                // versioned request, so ask again in the shape it understands.
1444                // A storer that answered with a refusal is NOT retried: it
1445                // understood us and said no, and asking again without the
1446                // version would talk it into quoting a client that cannot pay.
1447                let result = match result {
1448                    Err(ref e) if is_version_unaware(e) && !known_legacy => {
1449                        // Only silence is evidence the peer cannot parse the
1450                        // shape. A send failure means the request never
1451                        // arrived and teaches nothing, so it must not strand
1452                        // the peer in the legacy shape for the whole session.
1453                        // Nor may a peer that has answered a versioned request
1454                        // before be demoted by one lost response.
1455                        let ever_answered = capable_handle
1456                            .lock()
1457                            .is_ok_and(|peers| peers.contains(&peer_id_clone));
1458                        if matches!(e, Error::Timeout(_)) && !ever_answered {
1459                            if let Ok(mut peers) = peers_handle.lock() {
1460                                peers.insert(peer_id_clone);
1461                            }
1462                        }
1463                        debug!(
1464                            "Peer {peer_id_clone} did not answer a versioned merkle quote; \
1465                             retrying in the legacy shape"
1466                        );
1467                        send_and_await_chunk_response(
1468                            &node_clone,
1469                            &peer_id_clone,
1470                            legacy_message_bytes,
1471                            legacy_request_id,
1472                            timeout,
1473                            &addrs_clone,
1474                            |body| map_merkle_candidate_response(peer_id_clone, body),
1475                            |e| {
1476                                Error::Network(format!(
1477                                    "Failed to send merkle candidate request to {peer_id_clone}: {e}"
1478                                ))
1479                            },
1480                            || {
1481                                Error::Timeout(format!(
1482                                    "Timeout waiting for merkle candidate from {peer_id_clone}"
1483                                ))
1484                            },
1485                        )
1486                        .await
1487                    }
1488                    other => other,
1489                };
1490
1491                (peer_id_clone, result)
1492            };
1493
1494            candidate_futures.push(fut);
1495        }
1496
1497        self.collect_validated_candidates(&mut candidate_futures, address, merkle_payment_timestamp)
1498            .await
1499    }
1500
1501    /// Collect and validate merkle candidate responses, then return the
1502    /// `CANDIDATES_PER_POOL` valid responders that are XOR-closest to the
1503    /// pool midpoint.
1504    ///
1505    /// Why distance-sort instead of "first N to respond":
1506    /// the storing-node verifier re-runs a network closest-peers lookup of
1507    /// the pool midpoint and rejects the pool if fewer than 13 of the 16
1508    /// candidate `pub_keys` appear in that authoritative close-set. Pools
1509    /// built from the fastest-to-respond quoters fail this check whenever
1510    /// truly-close peers are slower (NAT/relay paths) than farther peers.
1511    async fn collect_validated_candidates(
1512        &self,
1513        futures: &mut FuturesUnordered<
1514            impl std::future::Future<
1515                Output = (
1516                    PeerId,
1517                    std::result::Result<(MerklePaymentCandidateNode, Option<Vec<u8>>), Error>,
1518                ),
1519            >,
1520        >,
1521        target_address: &[u8; 32],
1522        merkle_payment_timestamp: u64,
1523    ) -> Result<[MerklePaymentCandidateNode; CANDIDATES_PER_POOL]> {
1524        let mut valid: Vec<(PeerId, MerklePaymentCandidateNode)> = Vec::new();
1525        let mut failures: Vec<String> = Vec::new();
1526
1527        while let Some((peer_id, result)) = futures.next().await {
1528            match result {
1529                Ok((candidate, commitment)) => {
1530                    if !verify_merkle_candidate_signature(&candidate) {
1531                        warn!("Invalid ML-DSA-65 signature from merkle candidate {peer_id}");
1532                        failures.push(format!("{peer_id}: invalid signature"));
1533                        continue;
1534                    }
1535                    if candidate.merkle_payment_timestamp != merkle_payment_timestamp {
1536                        warn!("Timestamp mismatch from merkle candidate {peer_id}");
1537                        failures.push(format!("{peer_id}: timestamp mismatch"));
1538                        continue;
1539                    }
1540                    // The candidate's identity is `BLAKE3(candidate.pub_key)` —
1541                    // this is what the storer derives (verifier.rs). Require it
1542                    // to equal the network responder so a two-identity operator
1543                    // cannot answer as B while shipping A's commitment.
1544                    let candidate_peer = PeerId::from_bytes(compute_address(&candidate.pub_key));
1545                    if candidate_peer != peer_id {
1546                        warn!(
1547                            "Dropping merkle candidate {peer_id} — pub_key derives {candidate_peer}, \
1548                             not the responding peer"
1549                        );
1550                        failures.push(format!("{peer_id}: candidate pub_key/peer mismatch"));
1551                        continue;
1552                    }
1553                    // ADR-0004: the FULL resolve-before-pay binding check, same as
1554                    // the single-node path — a candidate priced off its committed
1555                    // count, or shipping an unresolvable/forged commitment, is
1556                    // dropped before it can enter a pool the client pays. Checked
1557                    // against the CANDIDATE peer (the one the storer audits). The
1558                    // shipped commitment is consumed here (resolution only) and
1559                    // not forwarded in the PUT bundles (see
1560                    // `finalize_merkle_batch`).
1561                    if let Err(detail) =
1562                        merkle_candidate_binding_is_valid(&candidate_peer, &candidate, &commitment)
1563                    {
1564                        warn!("Dropping merkle candidate {peer_id} — ADR-0004 binding invalid: {detail}");
1565                        failures.push(format!("{peer_id}: bad commitment binding ({detail})"));
1566                        continue;
1567                    }
1568                    valid.push((candidate_peer, candidate));
1569                }
1570                // A storer that has explicitly declared this client
1571                // incompatible ends the batch, but only once enough distinct
1572                // peers agree. Collecting a corroborated refusal as one more
1573                // failed peer would let the pool fill from the remaining
1574                // sixteen and go on to pay, which is the burn this mechanism
1575                // exists to prevent; acting on a single peer's unauthenticated
1576                // word would instead let one hostile responder deny every
1577                // upload.
1578                Err(e @ Error::ClientUpdateRequired(_)) => {
1579                    if let Some(corroborated) =
1580                        self.note_settlement_refusal(peer_id, &e.to_string())
1581                    {
1582                        // Name the corroborators — the below-quorum branch logs
1583                        // one peer at a time, so without this line a reader
1584                        // cannot tell a two-peer verdict from a one-peer
1585                        // misfire (V2-1109).
1586                        let corroborators = self.settlement_refusals().corroborating_peers();
1587                        warn!(
1588                            "Settlement refusal corroborated by {} distinct peers [{}]; aborting before payment",
1589                            corroborators.len(),
1590                            corroborators.join(", ")
1591                        );
1592                        return Err(Error::ClientUpdateRequired(corroborated));
1593                    }
1594                    warn!("Merkle candidate {peer_id} refused this client's settlement version; awaiting corroboration");
1595                    failures.push(format!("{peer_id}: {e}"));
1596                }
1597                Err(e) => {
1598                    debug!("Failed to get merkle candidate from {peer_id}: {e}");
1599                    failures.push(format!("{peer_id}: {e}"));
1600                }
1601            }
1602        }
1603
1604        if valid.len() < CANDIDATES_PER_POOL {
1605            return Err(Error::InsufficientPeers(format!(
1606                "Got {} merkle candidates, need {CANDIDATES_PER_POOL}. Failures: [{}]",
1607                valid.len(),
1608                failures.join("; ")
1609            )));
1610        }
1611
1612        let target_peer = PeerId::from_bytes(*target_address);
1613        valid.sort_by_key(|(peer_id, _)| peer_id.xor_distance(&target_peer));
1614
1615        let candidates: Vec<MerklePaymentCandidateNode> = valid
1616            .into_iter()
1617            .take(CANDIDATES_PER_POOL)
1618            .map(|(_, candidate)| candidate)
1619            .collect();
1620
1621        let array: [MerklePaymentCandidateNode; CANDIDATES_PER_POOL] =
1622            candidates.try_into().map_err(|_| {
1623                Error::Payment("Failed to convert candidates to fixed array".to_string())
1624            })?;
1625        Ok(array)
1626    }
1627
1628    /// Upload chunks using pre-computed merkle proofs from a batch payment.
1629    ///
1630    /// Each chunk is matched to its proof from `batch_result.proofs`, then
1631    /// stored to its close group concurrently. A per-chunk quorum shortfall
1632    /// (`InsufficientPeers`) does **not** abort the file: such chunks are
1633    /// collected and retried — with the same reusable proof and a freshly
1634    /// re-collected close group — for up to [`MERKLE_STORE_MAX_ATTEMPTS`]
1635    /// attempts. `stored_offset` carries chunks already confirmed by an earlier
1636    /// preflight (used for progress numbering and the returned `stored` count),
1637    /// and `total_chunks` is the whole-file total for progress events.
1638    ///
1639    /// Returns how many chunks are stored (including `stored_offset`), how many
1640    /// remained short of quorum after all retries, and the aggregate store
1641    /// stats.
1642    ///
1643    /// # Errors
1644    ///
1645    /// Returns an error only for non-quorum failures (e.g. a missing proof, or a
1646    /// chunk-count/address mismatch); quorum shortfalls are reported via
1647    /// [`MerkleStoreOutcome::failed`].
1648    #[cfg(test)]
1649    pub(crate) async fn merkle_upload_chunks(
1650        &self,
1651        chunk_contents: Vec<Bytes>,
1652        addresses: Vec<[u8; 32]>,
1653        batch_result: &MerkleBatchPaymentResult,
1654        progress: Option<&mpsc::Sender<UploadEvent>>,
1655        stored_offset: usize,
1656        total_chunks: usize,
1657    ) -> Result<MerkleStoreOutcome> {
1658        let store_limiter = self.controller().store.clone();
1659        // Clamp fan-out to batch size — partial batches should not
1660        // pay for unused slots (see PERF-RESULTS.md).
1661        let batch_size = chunk_contents.len();
1662        if batch_size != addresses.len() {
1663            return Err(Error::InvalidData(format!(
1664                "merkle upload has {batch_size} chunk contents but {} addresses",
1665                addresses.len()
1666            )));
1667        }
1668        // Cap closure re-read per scheduler refill so mid-flight limiter growth
1669        // is applied to the rest of the batch (V2-554). Clamped to batch size —
1670        // partial batches should not pay for unused slots (see PERF-RESULTS.md).
1671        let cap = || store_limiter.current().min(batch_size.max(1));
1672
1673        // External-signer path: the chunk bodies are already resident in memory,
1674        // so `store_one` clones each from this map on demand. (The whole-file
1675        // path instead reads bodies from the on-disk spill — same `store_one(addr)`
1676        // shape, so the store scheduler never carries a resident set of bodies.)
1677        let bodies: std::collections::HashMap<[u8; 32], Bytes> =
1678            addresses.iter().copied().zip(chunk_contents).collect();
1679        let addrs = addresses;
1680
1681        // Store one chunk to its (freshly re-collected) close group. Called
1682        // once per chunk per attempt, so a retry round naturally lands on a
1683        // converged routing table. Only `InsufficientPeers` is recoverable;
1684        // a missing proof stays fatal.
1685        let store_one = |addr: [u8; 32]| {
1686            let limiter = store_limiter.clone();
1687            let content = bodies.get(&addr).cloned();
1688            let proof_bytes = batch_result.proofs.get(&addr).cloned();
1689            async move {
1690                let started = web_time::Instant::now();
1691                let content = content.ok_or_else(|| {
1692                    Error::InvalidData(format!("missing chunk body for {}", hex::encode(addr)))
1693                })?;
1694                let proof = proof_bytes.ok_or_else(|| {
1695                    Error::Payment(format!(
1696                        "Missing merkle proof for chunk {}",
1697                        hex::encode(addr)
1698                    ))
1699                })?;
1700                let peers = self.put_target_peers(&addr).await?;
1701                observe_op(
1702                    &limiter,
1703                    || async move { self.chunk_put_to_close_group(content, proof, &peers).await },
1704                    classify_error,
1705                )
1706                .await
1707                .map(|_| started)
1708            }
1709        };
1710
1711        let outcome = merkle_store_with_retry(
1712            addrs,
1713            cap,
1714            MERKLE_STORE_MAX_ATTEMPTS,
1715            MERKLE_RETRY_BACKOFF,
1716            progress,
1717            stored_offset,
1718            total_chunks,
1719            store_one,
1720        )
1721        .await?;
1722
1723        // The external-signer path treats a non-quorum error as terminal (its
1724        // finalize has no further waves to fold progress into), so re-raise
1725        // the fatal that `merkle_store_with_retry` now carries in the outcome.
1726        // The CLI/spill paths, which fold progress across waves before
1727        // surfacing `PartialUpload`, read `fatal` directly instead. Quorum
1728        // shortfalls stay in `failed`/`failed_addresses`; the external-signer
1729        // finalize turns those into `PartialUpload` too (issue #166).
1730        if let Some(e) = outcome.fatal {
1731            return Err(e);
1732        }
1733        Ok(outcome)
1734    }
1735}
1736
1737/// Total store-attempt budget for a merkle batch: the initial attempt plus up
1738/// to three retries. Chosen to match the wave path's contract
1739/// (`batch.rs` iterates `0..=MAX_RETRIES` with `MAX_RETRIES = 3`) and the
1740/// four-slot [`WaveAggregateStats::retries_histogram`], so a chunk that lands
1741/// on the final retry is recorded in `retries_histogram[3]`.
1742///
1743/// A chunk's close group can transiently reject its `winner_pool` midpoint
1744/// while a few nodes' routing tables disagree about that midpoint; the network
1745/// converges within minutes. Per-chunk proofs are reusable, so retrying the
1746/// same proof after a short backoff recovers these shortfalls for free — no
1747/// re-payment and no new pool.
1748#[cfg(test)]
1749pub(crate) const MERKLE_STORE_MAX_ATTEMPTS: usize = 4;
1750
1751/// Base backoff between merkle store attempts. The routing-table divergence
1752/// that causes `InsufficientPeers` resolves on the order of minutes, so a short
1753/// sleep between rounds is enough to land on a converged close group. The
1754/// actual wait is jittered by [`MERKLE_RETRY_JITTER`] so a large failed set
1755/// does not re-fire against the same divergent nodes in lockstep.
1756#[cfg(test)]
1757pub(crate) const MERKLE_RETRY_BACKOFF: Duration = Duration::from_secs(30);
1758
1759/// Fractional jitter applied to [`MERKLE_RETRY_BACKOFF`] (±10%), spreading the
1760/// retry wave so convergent nodes are not all probed at the same instant.
1761const MERKLE_RETRY_JITTER: f64 = 0.1;
1762
1763/// Outcome of storing a merkle batch: how many chunks landed, how many
1764/// remained short of quorum after all retries, and the aggregate store stats.
1765#[derive(Debug, Default)]
1766pub(crate) struct MerkleStoreOutcome {
1767    /// Chunks that reached quorum, including any `stored_offset` carried in
1768    /// from a preflight (counted once, even if they needed retries).
1769    pub stored: usize,
1770    /// Addresses confirmed stored by this call (excludes the `stored_offset`
1771    /// preflight carry-in — those have no address here). The caller appends
1772    /// these to the file's stored set; using the explicit set (rather than
1773    /// inferring "input minus failed") keeps accounting correct even when a
1774    /// `fatal` error aborts the pass mid-flight, leaving some input chunks
1775    /// neither stored nor in `failed_addresses`.
1776    pub stored_addresses: Vec<[u8; 32]>,
1777    /// Chunks still short of quorum after [`MERKLE_STORE_MAX_ATTEMPTS`].
1778    pub failed: usize,
1779    /// Addresses (and the last error message) of chunks still short of quorum
1780    /// after all retries. Empty when `failed == 0`. Both the CLI path and the
1781    /// external-signer finalize build
1782    /// [`crate::data::Error::PartialUpload`] from this set.
1783    pub failed_addresses: Vec<([u8; 32], String)>,
1784    /// Set when a non-quorum (fatal) store error aborted the pass. Successes
1785    /// completed before the abort are still recorded in `stored`/
1786    /// `stored_addresses`; the chunks that had already failed quorum are in
1787    /// `failed_addresses`; chunks still in flight when the abort hit are in
1788    /// neither (the caller treats input-minus-stored as failed). Callers that
1789    /// want the old "fatal aborts everything" contract re-raise this as `Err`.
1790    pub fatal: Option<Error>,
1791    /// Aggregate store stats (durations, attempts, per-round retry histogram).
1792    pub stats: crate::data::client::batch::WaveAggregateStats,
1793}
1794
1795/// Drive a set of merkle chunk stores with bounded retry of quorum shortfalls.
1796///
1797/// Runs `store_one(addr)` over all `addrs` concurrently, keeping up to `cap()`
1798/// stores in flight and RE-READING `cap()` as each slot frees — so mid-flight
1799/// adaptive growth is applied to the rest of the round instead of being frozen
1800/// at a per-round snapshot (V2-554). `store_one` acquires the chunk body itself
1801/// (e.g. reads it from the on-disk spill), so only the ≤`cap()` in-flight stores
1802/// hold a body in memory — the caller passes addresses, never a resident set of
1803/// bodies, which is what lets the whole-file store run as one cap-bounded
1804/// fan-out instead of memory-bounded waves. Collects quorum shortfalls
1805/// (`InsufficientPeers`, `CloseGroupShortfall`, `RemotePut`) rather than
1806/// aborting. Failed chunks are retried — `store_one` re-collects their close
1807/// group on each call, so a converged routing table can yield a fresh group —
1808/// for up to `max_attempts` rounds, sleeping a jittered `backoff` between
1809/// rounds. A chunk's success is counted once and recorded in the retry round it
1810/// landed on (`retries_histogram[round]`). `stored_offset` seeds the returned
1811/// `stored` count and the progress numbering; `total` is the whole-file total
1812/// reported in progress events.
1813///
1814/// A non-quorum error stops the pass but does **not** discard progress: the
1815/// successes already completed this pass stay in `stored`/`stored_addresses`,
1816/// the quorum shortfalls so far stay in `failed_addresses`, and the error is
1817/// returned in [`MerkleStoreOutcome::fatal`] (as `Ok(outcome)`, not `Err`).
1818/// Callers that want the old abort-everything behaviour re-raise `fatal` as
1819/// `Err`; CLI callers fold it into `PartialUpload` while keeping the stores.
1820#[allow(clippy::too_many_arguments)]
1821pub(crate) async fn merkle_store_with_retry<F, Fut, C>(
1822    addrs: Vec<[u8; 32]>,
1823    cap: C,
1824    max_attempts: usize,
1825    backoff: Duration,
1826    progress: Option<&mpsc::Sender<UploadEvent>>,
1827    stored_offset: usize,
1828    total: usize,
1829    store_one: F,
1830) -> Result<MerkleStoreOutcome>
1831where
1832    F: Fn([u8; 32]) -> Fut,
1833    Fut: std::future::Future<Output = Result<web_time::Instant>>,
1834    C: Fn() -> usize,
1835{
1836    let attempts = max_attempts.max(1);
1837    let mut outcome = MerkleStoreOutcome {
1838        stored: stored_offset,
1839        ..MerkleStoreOutcome::default()
1840    };
1841    let mut pending = addrs;
1842
1843    for attempt in 0..attempts {
1844        // Carries the failing address forward for the next round plus the last
1845        // quorum-shortfall message, so an exhausted set can report per-chunk
1846        // errors via `failed_addresses`. The chunk BODY is not carried — each
1847        // `store_one` re-reads it on demand, so at most `cap` bodies are ever
1848        // resident regardless of the total chunk count.
1849        let mut next_failed: Vec<([u8; 32], String)> = Vec::new();
1850
1851        // Rolling scheduler: keep up to `cap()` stores in flight, re-reading the
1852        // cap as each slot frees so limiter growth mid-round is applied to the
1853        // remaining chunks (V2-554). Iterator exhaustion bounds the launch count
1854        // to the pending set, so no explicit clamp to `pending.len()` is needed.
1855        let mut pending_iter = pending.into_iter();
1856        let mut in_flight = FuturesUnordered::new();
1857        loop {
1858            let slots = cap().max(1);
1859            while in_flight.len() < slots {
1860                match pending_iter.next() {
1861                    Some(addr) => {
1862                        let fut = store_one(addr);
1863                        in_flight.push(async move { (addr, fut.await) });
1864                    }
1865                    None => break,
1866                }
1867            }
1868            let Some((addr, result)) = in_flight.next().await else {
1869                break;
1870            };
1871            outcome.stats.chunk_attempts_total =
1872                outcome.stats.chunk_attempts_total.saturating_add(1);
1873            match result {
1874                Ok(started) => {
1875                    let duration_ms =
1876                        u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX);
1877                    outcome.stats.store_durations_ms.push(duration_ms);
1878                    let idx = attempt.min(outcome.stats.retries_histogram.len().saturating_sub(1));
1879                    outcome.stats.retries_histogram[idx] =
1880                        outcome.stats.retries_histogram[idx].saturating_add(1);
1881                    outcome.stored += 1;
1882                    outcome.stored_addresses.push(addr);
1883                    if let Some(tx) = progress {
1884                        let _ = tx.try_send(UploadEvent::ChunkStored {
1885                            stored: outcome.stored,
1886                            total,
1887                        });
1888                    }
1889                }
1890                // A quorum shortfall — whether a timeout-bearing capacity
1891                // shortfall (`InsufficientPeers`), a pure dial-churn shortfall
1892                // (`CloseGroupShortfall`, V2-554), or an app-only rejection
1893                // (`RemotePut`, e.g. pool-rejected / quote-stale / disk-full,
1894                // which are transient) — is recoverable: defer and retry the
1895                // chunk rather than aborting the whole upload (V2-468 / V2-554).
1896                Err(
1897                    e @ (Error::InsufficientPeers(_)
1898                    | Error::CloseGroupShortfall(_)
1899                    | Error::RemotePut { .. }),
1900                ) => {
1901                    next_failed.push((addr, e.to_string()));
1902                }
1903                Err(e) => {
1904                    // Non-quorum error: fatal. Stop consuming the stream but do
1905                    // NOT discard the outcome — successes already completed this
1906                    // pass stay recorded in `stored`/`stored_addresses`. Record
1907                    // the fatal chunk itself (and any quorum shortfalls seen so
1908                    // far) as failed; anything still in flight is left for the
1909                    // caller to treat as not-stored (input minus
1910                    // `stored_addresses`).
1911                    next_failed.push((addr, e.to_string()));
1912                    outcome.fatal = Some(e);
1913                    break;
1914                }
1915            }
1916        }
1917
1918        if outcome.fatal.is_some() {
1919            outcome.failed = next_failed.len();
1920            outcome.failed_addresses = next_failed;
1921            return Ok(outcome);
1922        }
1923
1924        if next_failed.is_empty() {
1925            break;
1926        }
1927
1928        if attempt + 1 < attempts {
1929            warn!(
1930                failed = next_failed.len(),
1931                attempt = attempt + 1,
1932                "merkle chunks short of quorum, retrying after backoff"
1933            );
1934            pending = next_failed.into_iter().map(|(addr, _msg)| addr).collect();
1935            if backoff > Duration::ZERO {
1936                // Jitter the wait (±MERKLE_RETRY_JITTER) so a large failed set
1937                // does not re-probe the same divergent nodes in lockstep.
1938                // `thread_rng` is !Send, so the value is computed and the rng
1939                // dropped before the await to keep this future Send.
1940                let wait = {
1941                    let mut rng = rand::thread_rng();
1942                    let factor = 1.0 + rng.gen_range(-MERKLE_RETRY_JITTER..=MERKLE_RETRY_JITTER);
1943                    backoff.mul_f64(factor)
1944                };
1945                crate::runtime::sleep(wait).await;
1946            }
1947        } else {
1948            outcome.failed = next_failed.len();
1949            outcome.failed_addresses = next_failed;
1950            break;
1951        }
1952    }
1953
1954    Ok(outcome)
1955}
1956
1957/// Round delays (seconds) for the merkle upload deferred-retry pass. Round 0
1958/// fires immediately — most quorum shortfalls on a healthy network are
1959/// momentary close-group divergence that clears in well under a second, and
1960/// serializing them behind mandatory sleeps was the single biggest throughput
1961/// sink in the wave path (one bad chunk parked the other 63 slots for minutes).
1962/// Only chunks that survive a round get a longer back-off before the next, so a
1963/// genuinely saturated/diverged group still gets time to settle. Mirrors the
1964/// download path's `DEFERRED_ROUND_DELAYS_SECS`.
1965pub(crate) const DEFERRED_ROUND_DELAYS_SECS: [u64; 3] = [0, 15, 45];
1966
1967/// Histogram slot for a deferred-retry round's successes.
1968///
1969/// The wave first pass lands in slot 0; deferred round `r` (0-indexed) lands in
1970/// slot `r + 1`, clamped to the last slot so the four-slot
1971/// [`WaveAggregateStats::retries_histogram`] keeps recording "which round a
1972/// chunk landed on" under the post-wave deferred structure.
1973pub(crate) fn deferred_round_histogram_slot(round: usize, hist_len: usize) -> usize {
1974    (round + 1).min(hist_len.saturating_sub(1))
1975}
1976
1977/// Outcome of the post-wave deferred-retry pass.
1978#[derive(Debug, Default)]
1979pub(crate) struct DeferredRetryOutcome {
1980    /// Running total of stored chunks, seeded with the `stored_offset` passed in
1981    /// (i.e. everything the wave passes already stored) and advanced by each
1982    /// deferred round's successes.
1983    pub stored: usize,
1984    /// Addresses that reached quorum during the deferred rounds (to be appended
1985    /// to the file's `stored` set).
1986    pub stored_addresses: Vec<[u8; 32]>,
1987    /// Count of chunks still short of quorum after the final deferred round.
1988    pub failed: usize,
1989    /// Addresses (and last quorum-shortfall message) still short after the final
1990    /// round, or — when `fatal` is set — the chunks that were still pending when
1991    /// a non-quorum error aborted the pass.
1992    pub failed_addresses: Vec<([u8; 32], String)>,
1993    /// Set when a deferred round hit a non-quorum (fatal) store error. The
1994    /// caller surfaces this as `PartialUpload` preserving everything stored so
1995    /// far, mirroring the wave path's fatal handling.
1996    pub fatal: Option<String>,
1997    /// Aggregate store stats merged across rounds, with each round's successes
1998    /// already mapped into its [`deferred_round_histogram_slot`].
1999    pub stats: crate::data::client::batch::WaveAggregateStats,
2000}
2001
2002/// Retry a file-level set of quorum-short merkle chunks in concurrent rounds.
2003///
2004/// This is the upload analogue of the download path's deferred-retry loop. The
2005/// whole-file store pass hands its quorum-short chunks here. Each round stores
2006/// all still-pending chunks in a single cap-bounded pass via
2007/// [`merkle_store_with_retry`] at `concurrency_for(len)` — `store_one(addr)`
2008/// re-reads each body on demand (from the spill), so only the ≤`concurrency_for`
2009/// in-flight stores hold a body and peak resident memory stays bounded
2010/// regardless of the deferred-chunk count. Survivors carry to the next round
2011/// after a `round_delays_secs` sleep. Chunks still short after the final round
2012/// become `failed_addresses`; a non-quorum store error stops the pass and is
2013/// reported via `fatal` (with the quorum shortfalls seen so far recorded as
2014/// `failed_addresses`) so the caller can surface `PartialUpload` — reconciled
2015/// against the full address list — without discarding earlier progress.
2016///
2017/// `store_one`, `progress`, `stored_offset` and `total` mirror
2018/// [`merkle_store_with_retry`].
2019#[allow(clippy::too_many_arguments)]
2020pub(crate) async fn merkle_deferred_retry<CF, SF, Fut>(
2021    deferred: Vec<([u8; 32], String)>,
2022    round_delays_secs: &[u64],
2023    concurrency_for: CF,
2024    progress: Option<&mpsc::Sender<UploadEvent>>,
2025    stored_offset: usize,
2026    total: usize,
2027    store_one: SF,
2028) -> Result<DeferredRetryOutcome>
2029where
2030    CF: Fn(usize) -> usize,
2031    SF: Fn([u8; 32]) -> Fut,
2032    Fut: std::future::Future<Output = Result<web_time::Instant>>,
2033{
2034    let mut outcome = DeferredRetryOutcome {
2035        stored: stored_offset,
2036        ..DeferredRetryOutcome::default()
2037    };
2038    let mut remaining = deferred;
2039    let rounds = round_delays_secs.len();
2040
2041    for (round, &delay_secs) in round_delays_secs.iter().enumerate() {
2042        if remaining.is_empty() {
2043            break;
2044        }
2045        if delay_secs > 0 {
2046            crate::runtime::sleep(Duration::from_secs(delay_secs)).await;
2047        }
2048        info!(
2049            "Deferred merkle retry round {}/{}: {} chunk(s) short of quorum",
2050            round + 1,
2051            rounds,
2052            remaining.len(),
2053        );
2054
2055        // Store this round's whole pending set in one cap-bounded pass. A
2056        // single-pass round records its successes in histogram slot 0, so
2057        // redirect them into the round's own slot.
2058        let slot = deferred_round_histogram_slot(round, outcome.stats.retries_histogram.len());
2059        let round_addrs: Vec<[u8; 32]> = std::mem::take(&mut remaining)
2060            .into_iter()
2061            .map(|(addr, _msg)| addr)
2062            .collect();
2063        let round_len = round_addrs.len();
2064        // Re-read the cap per scheduler refill (V2-554) via `concurrency_for`,
2065        // which re-samples the store limiter clamped to this round's size.
2066        let cap = || concurrency_for(round_len);
2067
2068        let round_outcome = merkle_store_with_retry(
2069            round_addrs,
2070            cap,
2071            1,
2072            Duration::ZERO,
2073            progress,
2074            outcome.stored,
2075            total,
2076            &store_one,
2077        )
2078        .await?;
2079
2080        outcome.stored = round_outcome.stored;
2081        outcome
2082            .stored_addresses
2083            .extend(round_outcome.stored_addresses);
2084
2085        // Merge stats, redirecting this round's successes to its slot.
2086        outcome.stats.chunk_attempts_total = outcome
2087            .stats
2088            .chunk_attempts_total
2089            .saturating_add(round_outcome.stats.chunk_attempts_total);
2090        outcome
2091            .stats
2092            .store_durations_ms
2093            .extend(round_outcome.stats.store_durations_ms);
2094        let landed: usize = round_outcome.stats.retries_histogram.iter().sum();
2095        outcome.stats.retries_histogram[slot] =
2096            outcome.stats.retries_histogram[slot].saturating_add(landed);
2097
2098        if let Some(fatal) = round_outcome.fatal {
2099            // Fatal mid-pass: confirmed stores are preserved above. The store
2100            // helper left this round's quorum shortfalls in `failed_addresses`;
2101            // chunks still in flight / not yet launched are reconciled against
2102            // the full address list by the caller's `partial_upload_after_fatal`.
2103            outcome.fatal = Some(fatal.to_string());
2104            outcome.failed = round_outcome.failed_addresses.len();
2105            outcome.failed_addresses = round_outcome.failed_addresses;
2106            return Ok(outcome);
2107        }
2108
2109        // Quorum-short chunks from this round survive to the next.
2110        remaining = round_outcome.failed_addresses;
2111    }
2112
2113    outcome.failed = remaining.len();
2114    outcome.failed_addresses = remaining;
2115    Ok(outcome)
2116}
2117
2118/// Phase 2 of external-signer merkle payment: generate proofs from winner.
2119///
2120/// Takes the prepared batch and the winner pool hash returned by the
2121/// on-chain payment transaction. Generates per-chunk merkle proofs.
2122pub fn finalize_merkle_batch(
2123    prepared: PreparedMerkleBatch,
2124    winner_pool_hash: [u8; 32],
2125) -> Result<MerkleBatchPaymentResult> {
2126    let chunk_count = prepared.addresses.len();
2127    let xornames: Vec<XorName> = prepared.addresses.iter().map(|a| XorName(*a)).collect();
2128
2129    // Find the winner pool
2130    let winner_pool = prepared
2131        .candidate_pools
2132        .iter()
2133        .find(|pool| pool.hash() == winner_pool_hash)
2134        .ok_or_else(|| {
2135            Error::Payment(format!(
2136                "Winner pool {} not found in candidate pools",
2137                hex::encode(winner_pool_hash)
2138            ))
2139        })?;
2140
2141    // ADR-0004: commitment sidecars are deliberately NOT forwarded in the
2142    // per-chunk proofs. Sixteen sidecars are ~214 KB serialized, and copying
2143    // them into every chunk's bundle pushed the proof past the storer's
2144    // payment-proof size cap, rejecting every merkle PUT once nodes carried
2145    // live commitments. The client still fully resolves every candidate's
2146    // commitment before paying (during pool collection); the storer's
2147    // cross-check is best-effort and resolves pins from its gossip cache or a
2148    // `GetCommitmentByPin` fetch when no sidecar is shipped.
2149
2150    // Generate proofs for each chunk
2151    info!("Generating merkle proofs for {chunk_count} chunks");
2152    let mut proofs = HashMap::with_capacity(chunk_count);
2153
2154    for (i, xorname) in xornames.iter().enumerate() {
2155        let address_proof = prepared
2156            .tree
2157            .generate_address_proof(i, *xorname)
2158            .map_err(|e| {
2159                Error::Payment(format!(
2160                    "Failed to generate address proof for chunk {i}: {e}"
2161                ))
2162            })?;
2163
2164        let merkle_proof = MerklePaymentProof::new(*xorname, address_proof, winner_pool.clone());
2165
2166        let tagged_bytes = serialize_merkle_proof(&merkle_proof)
2167            .map_err(|e| Error::Serialization(format!("Failed to serialize merkle proof: {e}")))?;
2168
2169        proofs.insert(prepared.addresses[i], tagged_bytes);
2170    }
2171
2172    info!("Merkle batch payment complete: {chunk_count} proofs generated");
2173
2174    Ok(MerkleBatchPaymentResult {
2175        proofs,
2176        chunk_count,
2177        storage_cost_atto: "0".to_string(),
2178        gas_cost_wei: 0,
2179        merkle_payment_timestamp: prepared.merkle_payment_timestamp,
2180    })
2181}
2182
2183/// Compile-time assertions that merkle method futures are Send.
2184#[cfg(test)]
2185mod send_assertions {
2186    use super::*;
2187    use crate::data::client::Client;
2188
2189    fn _assert_send<T: Send>(_: &T) {}
2190
2191    #[allow(
2192        dead_code,
2193        unreachable_code,
2194        unused_variables,
2195        clippy::diverging_sub_expression
2196    )]
2197    async fn _merkle_upload_chunks_is_send(client: &Client) {
2198        let batch_result: MerkleBatchPaymentResult = todo!();
2199        let fut = client.merkle_upload_chunks(Vec::new(), Vec::new(), &batch_result, None, 0, 0);
2200        _assert_send(&fut);
2201    }
2202}
2203
2204/// Test-only builders shared by this module's tests and the external-finalize
2205/// tests in `file.rs` (ADR-0003).
2206#[cfg(test)]
2207#[allow(clippy::unwrap_used, clippy::expect_used)]
2208pub(crate) mod test_support {
2209    use super::*;
2210    use ant_protocol::evm::RewardsAddress;
2211
2212    pub(crate) fn make_test_addresses(count: usize) -> Vec<[u8; 32]> {
2213        (0..count)
2214            .map(|i| {
2215                let xn = XorName::from_content(&i.to_le_bytes());
2216                xn.0
2217            })
2218            .collect()
2219    }
2220
2221    pub(crate) fn make_dummy_candidate_nodes(
2222        timestamp: u64,
2223    ) -> [MerklePaymentCandidateNode; CANDIDATES_PER_POOL] {
2224        std::array::from_fn(|i| MerklePaymentCandidateNode {
2225            pub_key: vec![i as u8; 32],
2226            price: Amount::from(1024u64),
2227            reward_address: RewardsAddress::new([i as u8; 20]),
2228            merkle_payment_timestamp: timestamp,
2229            signature: vec![i as u8; 64],
2230            committed_key_count: 0,
2231            commitment_pin: None,
2232        })
2233    }
2234
2235    pub(crate) fn make_prepared_merkle_batch(count: usize) -> PreparedMerkleBatch {
2236        let addrs = make_test_addresses(count);
2237        let xornames: Vec<XorName> = addrs.iter().map(|a| XorName(*a)).collect();
2238        let tree = MerkleTree::from_xornames(xornames).unwrap();
2239
2240        let timestamp = std::time::SystemTime::now()
2241            .duration_since(std::time::UNIX_EPOCH)
2242            .unwrap()
2243            .as_secs();
2244
2245        let midpoints = tree.reward_candidates(timestamp).unwrap();
2246
2247        let candidate_pools: Vec<MerklePaymentCandidatePool> = midpoints
2248            .into_iter()
2249            .map(|mp| MerklePaymentCandidatePool {
2250                midpoint_proof: mp,
2251                candidate_nodes: make_dummy_candidate_nodes(timestamp),
2252            })
2253            .collect();
2254
2255        let pool_commitments = candidate_pools
2256            .iter()
2257            .map(pool_commitment_with_payment_multiplier)
2258            .collect::<Result<Vec<_>>>()
2259            .unwrap();
2260
2261        PreparedMerkleBatch {
2262            depth: tree.depth(),
2263            pool_commitments,
2264            merkle_payment_timestamp: timestamp,
2265            candidate_pools,
2266            tree,
2267            addresses: addrs,
2268        }
2269    }
2270
2271    /// A winner pool hash `finalize_merkle_batch` will accept for `batch`
2272    /// (the first candidate pool's) — the same selection the existing
2273    /// finalize tests use. Lives here because `candidate_pools` is private
2274    /// outside this module.
2275    pub(crate) fn winner_hash_for(batch: &PreparedMerkleBatch) -> [u8; 32] {
2276        batch.candidate_pools[0].hash()
2277    }
2278}
2279
2280#[cfg(test)]
2281#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
2282mod tests {
2283    use super::test_support::*;
2284    use super::*;
2285    use ant_protocol::evm::{Amount, MerkleTree, RewardsAddress, CANDIDATES_PER_POOL};
2286
2287    // =========================================================================
2288    // should_use_merkle (free function, no Client needed)
2289    // =========================================================================
2290
2291    #[test]
2292    fn test_auto_below_threshold() {
2293        assert!(!should_use_merkle(1, PaymentMode::Auto));
2294        assert!(!should_use_merkle(10, PaymentMode::Auto));
2295        assert!(!should_use_merkle(63, PaymentMode::Auto));
2296    }
2297
2298    #[test]
2299    fn test_auto_at_and_above_threshold() {
2300        assert!(should_use_merkle(64, PaymentMode::Auto));
2301        assert!(should_use_merkle(65, PaymentMode::Auto));
2302        assert!(should_use_merkle(1000, PaymentMode::Auto));
2303    }
2304
2305    #[test]
2306    fn test_merkle_mode_forces_at_2() {
2307        assert!(!should_use_merkle(1, PaymentMode::Merkle));
2308        assert!(should_use_merkle(2, PaymentMode::Merkle));
2309        assert!(should_use_merkle(3, PaymentMode::Merkle));
2310    }
2311
2312    #[test]
2313    fn test_single_mode_always_false() {
2314        assert!(!should_use_merkle(0, PaymentMode::Single));
2315        assert!(!should_use_merkle(64, PaymentMode::Single));
2316        assert!(!should_use_merkle(1000, PaymentMode::Single));
2317    }
2318
2319    #[test]
2320    fn test_default_mode_is_auto() {
2321        assert_eq!(PaymentMode::default(), PaymentMode::Auto);
2322    }
2323
2324    #[test]
2325    fn test_threshold_value() {
2326        assert_eq!(DEFAULT_MERKLE_THRESHOLD, 64);
2327    }
2328
2329    // =========================================================================
2330    // preflight_stored_status — must degrade gracefully, never abort the batch
2331    // =========================================================================
2332
2333    #[test]
2334    fn test_preflight_quotes_gathered_means_not_stored() {
2335        assert!(matches!(preflight_stored_status(Ok(())), Ok(false)));
2336    }
2337
2338    #[test]
2339    fn test_preflight_already_stored_is_stored() {
2340        let r: Result<()> = Err(Error::AlreadyStored);
2341        assert!(matches!(preflight_stored_status(r), Ok(true)));
2342    }
2343
2344    /// The regression: a transient quote-quorum failure during the preflight
2345    /// must NOT propagate (which would abort the whole forced-merkle upload).
2346    /// It is treated as "not known to be stored" → queue the chunk for upload.
2347    #[test]
2348    fn test_preflight_transient_quote_failure_does_not_abort() {
2349        // The exact error STG-01 hit: couldn't gather a 7-quote quorum.
2350        let insufficient: Result<()> =
2351            Err(Error::InsufficientPeers("Got 5 quotes, need 7".to_string()));
2352        assert!(
2353            matches!(preflight_stored_status(insufficient), Ok(false)),
2354            "insufficient-peers during preflight must degrade to not-stored, not error"
2355        );
2356
2357        let timeout: Result<()> = Err(Error::Timeout("Timeout waiting for quote".to_string()));
2358        assert!(matches!(preflight_stored_status(timeout), Ok(false)));
2359
2360        let network: Result<()> = Err(Error::Network("connection reset".to_string()));
2361        assert!(matches!(preflight_stored_status(network), Ok(false)));
2362    }
2363
2364    /// Genuine application errors still propagate — the preflight should not
2365    /// silently swallow problems that would recur on a healthy link.
2366    #[test]
2367    fn test_preflight_application_error_propagates() {
2368        let payment: Result<()> = Err(Error::Payment("bad payment".to_string()));
2369        assert!(matches!(
2370            preflight_stored_status(payment),
2371            Err(Error::Payment(_))
2372        ));
2373    }
2374
2375    #[test]
2376    fn chunk_contents_for_upload_addresses_preserves_requested_order() {
2377        let first = Bytes::from_static(b"first");
2378        let second = Bytes::from_static(b"second");
2379        let first_addr = compute_address(&first);
2380        let second_addr = compute_address(&second);
2381
2382        let selected = chunk_contents_for_upload_addresses(
2383            vec![first.clone(), second.clone()],
2384            &[second_addr, first_addr],
2385        )
2386        .unwrap();
2387
2388        assert_eq!(selected, vec![second, first]);
2389    }
2390
2391    #[test]
2392    fn chunk_contents_for_upload_addresses_preserves_duplicate_requests() {
2393        let repeated = Bytes::from_static(b"same-content");
2394        let other = Bytes::from_static(b"other-content");
2395        let repeated_addr = compute_address(&repeated);
2396
2397        let selected = chunk_contents_for_upload_addresses(
2398            vec![repeated.clone(), other, repeated.clone()],
2399            &[repeated_addr, repeated_addr],
2400        )
2401        .unwrap();
2402
2403        assert_eq!(selected, vec![repeated.clone(), repeated]);
2404    }
2405
2406    #[test]
2407    fn chunk_contents_for_upload_addresses_ignores_unrequested_duplicates() {
2408        let requested = Bytes::from_static(b"requested-content");
2409        let unrequested = Bytes::from_static(b"unrequested-content");
2410        let requested_addr = compute_address(&requested);
2411
2412        let selected = chunk_contents_for_upload_addresses(
2413            vec![
2414                unrequested.clone(),
2415                requested.clone(),
2416                unrequested.clone(),
2417                unrequested,
2418            ],
2419            &[requested_addr],
2420        )
2421        .unwrap();
2422
2423        assert_eq!(selected, vec![requested]);
2424    }
2425
2426    #[test]
2427    fn chunk_contents_for_upload_addresses_errors_for_missing_content() {
2428        let present = Bytes::from_static(b"present-content");
2429        let missing = Bytes::from_static(b"missing-content");
2430        let missing_addr = compute_address(&missing);
2431
2432        let result = chunk_contents_for_upload_addresses(vec![present], &[missing_addr]);
2433
2434        assert!(matches!(result, Err(Error::InvalidData(_))));
2435    }
2436
2437    // =========================================================================
2438    // MerkleTree construction and proof generation (pure, no network)
2439    // =========================================================================
2440
2441    #[test]
2442    fn test_tree_depth_for_known_sizes() {
2443        let cases = [(2, 1), (4, 2), (16, 4), (100, 7), (256, 8)];
2444        for (count, expected_depth) in cases {
2445            let addrs = make_test_addresses(count);
2446            let xornames: Vec<XorName> = addrs.iter().map(|a| XorName(*a)).collect();
2447            let tree = MerkleTree::from_xornames(xornames).unwrap();
2448            assert_eq!(
2449                tree.depth(),
2450                expected_depth,
2451                "depth mismatch for {count} leaves"
2452            );
2453        }
2454    }
2455
2456    #[test]
2457    fn test_proof_generation_and_verification_for_all_leaves() {
2458        let addrs = make_test_addresses(16);
2459        let xornames: Vec<XorName> = addrs.iter().map(|a| XorName(*a)).collect();
2460        let tree = MerkleTree::from_xornames(xornames.clone()).unwrap();
2461
2462        for (i, xn) in xornames.iter().enumerate() {
2463            let proof = tree.generate_address_proof(i, *xn).unwrap();
2464            assert!(proof.verify(), "proof for leaf {i} should verify");
2465            assert_eq!(proof.depth(), tree.depth() as usize);
2466        }
2467    }
2468
2469    #[test]
2470    fn test_proof_fails_for_wrong_address() {
2471        let addrs = make_test_addresses(8);
2472        let xornames: Vec<XorName> = addrs.iter().map(|a| XorName(*a)).collect();
2473        let tree = MerkleTree::from_xornames(xornames).unwrap();
2474
2475        let wrong = XorName::from_content(b"wrong");
2476        let proof = tree.generate_address_proof(0, wrong).unwrap();
2477        assert!(!proof.verify(), "proof with wrong address should fail");
2478    }
2479
2480    #[test]
2481    fn test_tree_too_few_leaves() {
2482        let xornames = vec![XorName::from_content(b"only_one")];
2483        let result = MerkleTree::from_xornames(xornames);
2484        assert!(result.is_err());
2485    }
2486
2487    #[test]
2488    fn test_tree_at_max_leaves() {
2489        let addrs = make_test_addresses(MAX_LEAVES);
2490        let xornames: Vec<XorName> = addrs.iter().map(|a| XorName(*a)).collect();
2491        let tree = MerkleTree::from_xornames(xornames).unwrap();
2492        assert_eq!(tree.leaf_count(), MAX_LEAVES);
2493    }
2494
2495    // =========================================================================
2496    // Proof serialization round-trip
2497    // =========================================================================
2498
2499    #[test]
2500    fn test_merkle_proof_serialize_deserialize_roundtrip() {
2501        use ant_protocol::evm::{Amount, MerklePaymentCandidateNode, RewardsAddress};
2502        use ant_protocol::payment::{deserialize_merkle_proof, serialize_merkle_proof};
2503
2504        let addrs = make_test_addresses(4);
2505        let xornames: Vec<XorName> = addrs.iter().map(|a| XorName(*a)).collect();
2506        let tree = MerkleTree::from_xornames(xornames.clone()).unwrap();
2507
2508        let timestamp = web_time::SystemTime::now()
2509            .duration_since(web_time::UNIX_EPOCH)
2510            .unwrap()
2511            .as_secs();
2512
2513        let candidates = tree.reward_candidates(timestamp).unwrap();
2514        let midpoint = candidates.first().unwrap().clone();
2515
2516        // Build candidate nodes (with dummy signatures — not ML-DSA, just for serialization test)
2517        #[allow(clippy::cast_possible_truncation)]
2518        let candidate_nodes: [MerklePaymentCandidateNode; CANDIDATES_PER_POOL] =
2519            std::array::from_fn(|i| MerklePaymentCandidateNode {
2520                pub_key: vec![i as u8; 32],
2521                price: Amount::from(1024u64),
2522                reward_address: RewardsAddress::new([i as u8; 20]),
2523                merkle_payment_timestamp: timestamp,
2524                signature: vec![i as u8; 64],
2525                committed_key_count: 0,
2526                commitment_pin: None,
2527            });
2528
2529        let pool = MerklePaymentCandidatePool {
2530            midpoint_proof: midpoint,
2531            candidate_nodes,
2532        };
2533
2534        let address_proof = tree.generate_address_proof(0, xornames[0]).unwrap();
2535        let merkle_proof = MerklePaymentProof::new(xornames[0], address_proof, pool);
2536
2537        let tagged = serialize_merkle_proof(&merkle_proof).unwrap();
2538        assert_eq!(
2539            tagged.first().copied(),
2540            Some(0x02),
2541            "tag should be PROOF_TAG_MERKLE"
2542        );
2543
2544        let deserialized = deserialize_merkle_proof(&tagged).unwrap();
2545        assert_eq!(deserialized.address, merkle_proof.address);
2546        assert_eq!(
2547            deserialized.winner_pool.candidate_nodes.len(),
2548            CANDIDATES_PER_POOL
2549        );
2550    }
2551
2552    // =========================================================================
2553    // Candidate validation logic
2554    // =========================================================================
2555
2556    #[test]
2557    fn test_candidate_wrong_timestamp_rejected() {
2558        // Simulates what collect_validated_candidates checks
2559        let candidate = MerklePaymentCandidateNode {
2560            pub_key: vec![0u8; 32],
2561            price: ant_protocol::evm::Amount::ZERO,
2562            reward_address: ant_protocol::evm::RewardsAddress::new([0u8; 20]),
2563            merkle_payment_timestamp: 1000,
2564            signature: vec![0u8; 64],
2565            committed_key_count: 0,
2566            commitment_pin: None,
2567        };
2568
2569        // Timestamp check: 1000 != 2000
2570        assert_ne!(candidate.merkle_payment_timestamp, 2000);
2571    }
2572
2573    // =========================================================================
2574    // finalize_merkle_batch (external signer)
2575    // =========================================================================
2576
2577    /// Candidate pool with distinct prices, so the median is a specific
2578    /// candidate rather than an artifact of every price being equal.
2579    fn pool_with_varied_prices(timestamp: u64) -> MerklePaymentCandidatePool {
2580        let addrs = make_test_addresses(4);
2581        let xornames: Vec<XorName> = addrs.iter().map(|a| XorName(*a)).collect();
2582        let tree = MerkleTree::from_xornames(xornames).unwrap();
2583        let midpoint = tree
2584            .reward_candidates(timestamp)
2585            .unwrap()
2586            .into_iter()
2587            .next()
2588            .unwrap();
2589
2590        let candidate_nodes = std::array::from_fn(|i| MerklePaymentCandidateNode {
2591            pub_key: vec![i as u8; 32],
2592            // 100, 200, ... 1600 — upper median (index 8 of 16) is 900.
2593            price: Amount::from((i as u64 + 1) * 100),
2594            reward_address: RewardsAddress::new([i as u8; 20]),
2595            merkle_payment_timestamp: timestamp,
2596            signature: vec![i as u8; 64],
2597            committed_key_count: 0,
2598            commitment_pin: None,
2599        });
2600
2601        MerklePaymentCandidatePool {
2602            midpoint_proof: midpoint,
2603            candidate_nodes,
2604        }
2605    }
2606
2607    /// The contract's `median16`: upper median, index 8 of 16 ascending.
2608    fn median16(mut amounts: Vec<Amount>) -> Amount {
2609        amounts.sort_unstable();
2610        *amounts.get(amounts.len() / 2).unwrap()
2611    }
2612
2613    #[test]
2614    fn pool_commitment_applies_payment_multiplier_to_every_candidate() {
2615        let pool = pool_with_varied_prices(1_700_000_000);
2616        let commitment = pool_commitment_with_payment_multiplier(&pool).unwrap();
2617
2618        for (candidate, signed) in commitment
2619            .candidates
2620            .iter()
2621            .zip(pool.candidate_nodes.iter())
2622        {
2623            assert_eq!(
2624                candidate.price,
2625                signed.price * Amount::from(MERKLE_PAYMENT_MULTIPLIER),
2626                "on-chain payable amount must be {MERKLE_PAYMENT_MULTIPLIER}x the quoted price"
2627            );
2628        }
2629    }
2630
2631    #[test]
2632    fn pool_commitment_multiplier_leaves_signed_prices_and_pool_hash_untouched() {
2633        let pool = pool_with_varied_prices(1_700_000_000);
2634        let before: Vec<Amount> = pool.candidate_nodes.iter().map(|c| c.price).collect();
2635
2636        let commitment = pool_commitment_with_payment_multiplier(&pool).unwrap();
2637
2638        let after: Vec<Amount> = pool.candidate_nodes.iter().map(|c| c.price).collect();
2639        assert_eq!(before, after, "signed candidate prices must not change");
2640        assert_eq!(
2641            commitment.pool_hash,
2642            pool.hash(),
2643            "pool hash is the storer's on-chain lookup key and must be \
2644             computed over the signed 1x prices"
2645        );
2646    }
2647
2648    /// The invariant the fix exists for, stated at the level it actually
2649    /// holds: the median payable amount over the winning pool is
2650    /// `MERKLE_PAYMENT_MULTIPLIER x` the median *quoted* price. The contract
2651    /// spends `median16(amount) * 2^depth` over `2^depth` **padded** leaves, so
2652    /// this is the settlement per padded leaf — the same figure the single-node
2653    /// path pays its median-priced issuer per chunk. It says nothing about cost
2654    /// per *actual* chunk: a batch whose size is not a power of two pays for
2655    /// padding leaves too (that is what `merkle_billable_leaves` bills for).
2656    #[test]
2657    fn merkle_settlement_per_padded_leaf_is_the_multiplied_pool_median() {
2658        let pool = pool_with_varied_prices(1_700_000_000);
2659        let commitment = pool_commitment_with_payment_multiplier(&pool).unwrap();
2660
2661        let quoted_median = median16(pool.candidate_nodes.iter().map(|c| c.price).collect());
2662        let per_chunk = median16(commitment.candidates.iter().map(|c| c.price).collect());
2663
2664        assert_eq!(quoted_median, Amount::from(900u64));
2665        assert_eq!(
2666            per_chunk,
2667            quoted_median * Amount::from(MERKLE_PAYMENT_MULTIPLIER),
2668            "merkle per-chunk settlement must equal the single-node \
2669             {MERKLE_PAYMENT_MULTIPLIER}x median, not the bare quoted price"
2670        );
2671    }
2672
2673    #[test]
2674    fn test_finalize_merkle_batch_with_valid_winner() {
2675        let prepared = make_prepared_merkle_batch(4);
2676        let winner_hash = prepared.candidate_pools[0].hash();
2677
2678        let result = finalize_merkle_batch(prepared, winner_hash);
2679        assert!(
2680            result.is_ok(),
2681            "should succeed with valid winner: {result:?}"
2682        );
2683
2684        let batch = result.unwrap();
2685        assert_eq!(batch.chunk_count, 4);
2686        assert_eq!(batch.proofs.len(), 4);
2687
2688        // Every proof should be non-empty
2689        for proof_bytes in batch.proofs.values() {
2690            assert!(!proof_bytes.is_empty());
2691        }
2692    }
2693
2694    /// DEV-01 regression: per-chunk merkle proofs must never ship commitment
2695    /// sidecars — all 16 winner-pool sidecars (~214 KB serialized) copied into
2696    /// every chunk's proof pushed it past the storer's payment-proof size cap,
2697    /// rejecting every merkle PUT once nodes carried live commitments. The
2698    /// e2e (`adr0004_merkle_upload_against_bound_candidates`) proves the flow
2699    /// end-to-end; this pins the wire invariant directly so it cannot slip
2700    /// back in behind a raised node-side cap.
2701    #[test]
2702    fn test_finalize_merkle_batch_ships_no_commitment_sidecars() {
2703        use ant_protocol::payment::deserialize_merkle_proof;
2704
2705        let mut prepared = make_prepared_merkle_batch(4);
2706        // Bind every candidate to a pin, mirroring a network where all nodes
2707        // carry live commitments (the DEV-01 trigger state).
2708        for pool in &mut prepared.candidate_pools {
2709            for candidate in &mut pool.candidate_nodes {
2710                candidate.committed_key_count = 9_000;
2711                candidate.commitment_pin = Some([7u8; 32]);
2712            }
2713        }
2714        let winner_hash = prepared.candidate_pools[0].hash();
2715
2716        let batch = finalize_merkle_batch(prepared, winner_hash).unwrap();
2717        assert_eq!(batch.proofs.len(), 4);
2718        for proof_bytes in batch.proofs.values() {
2719            let proof = deserialize_merkle_proof(proof_bytes).unwrap();
2720            assert!(
2721                proof.commitment_sidecars.is_empty(),
2722                "per-chunk merkle proofs must not ship commitment sidecars"
2723            );
2724        }
2725    }
2726
2727    #[test]
2728    fn test_finalize_merkle_batch_with_invalid_winner() {
2729        let prepared = make_prepared_merkle_batch(4);
2730        let bad_hash = [0xFF; 32];
2731
2732        let result = finalize_merkle_batch(prepared, bad_hash);
2733        assert!(result.is_err());
2734        let err = result.unwrap_err().to_string();
2735        assert!(err.contains("not found in candidate pools"), "got: {err}");
2736    }
2737
2738    #[test]
2739    fn test_finalize_merkle_batch_proofs_are_deserializable() {
2740        use ant_protocol::payment::deserialize_merkle_proof;
2741
2742        let prepared = make_prepared_merkle_batch(8);
2743        let winner_hash = prepared.candidate_pools[0].hash();
2744
2745        let batch = finalize_merkle_batch(prepared, winner_hash).unwrap();
2746
2747        for (addr, proof_bytes) in &batch.proofs {
2748            let proof = deserialize_merkle_proof(proof_bytes);
2749            assert!(
2750                proof.is_ok(),
2751                "proof for {} should deserialize: {:?}",
2752                hex::encode(addr),
2753                proof.err()
2754            );
2755        }
2756    }
2757
2758    // =========================================================================
2759    // Batch splitting edge cases
2760    // =========================================================================
2761
2762    /// Counts spanning every interesting case: the minimum tree, the merkle
2763    /// threshold and just past it, either side of a full batch, and the
2764    /// `1 mod MAX_LEAVES` counts the naive `chunks(MAX_LEAVES)` split turned
2765    /// into an unpayable `[..., 1]` tail.
2766    const PARTITION_CASES: [(usize, &[usize]); 10] = [
2767        (2, &[2]),
2768        (64, &[64]),
2769        (65, &[65]),
2770        (255, &[255]),
2771        (256, &[256]),
2772        (257, &[255, 2]),
2773        (300, &[256, 44]),
2774        (512, &[256, 256]),
2775        (513, &[256, 255, 2]),
2776        (769, &[256, 256, 255, 2]),
2777    ];
2778
2779    #[test]
2780    fn merkle_batch_sizes_rebalance_singleton_remainders() {
2781        for (total, expected) in PARTITION_CASES {
2782            assert_eq!(
2783                merkle_batch_sizes(total),
2784                expected,
2785                "{total} addresses must partition as {expected:?}"
2786            );
2787        }
2788    }
2789
2790    /// The test-seam cap partitions like the production cap, floored at 3 —
2791    /// a cap of 2 cannot partition odd totals into payable ≥2-leaf trees
2792    /// (ADR-0003).
2793    #[test]
2794    fn merkle_batch_sizes_with_cap_partitions_and_clamps() {
2795        // Small caps: rebalanced, every part payable (2..=cap), sums correct.
2796        assert_eq!(merkle_batch_sizes_with_cap(6, 3), vec![3, 3]);
2797        assert_eq!(merkle_batch_sizes_with_cap(7, 3), vec![3, 2, 2]);
2798        assert_eq!(merkle_batch_sizes_with_cap(4, 3), vec![2, 2]);
2799        // A cap below the floor clamps to 3 instead of emitting a 1-leaf part.
2800        assert_eq!(merkle_batch_sizes_with_cap(5, 2), vec![3, 2]);
2801        // A cap above MAX_LEAVES clamps down to the contract bound.
2802        assert_eq!(
2803            merkle_batch_sizes_with_cap(MAX_LEAVES + 1, MAX_LEAVES * 4),
2804            vec![MAX_LEAVES - 1, 2]
2805        );
2806        // Exhaustive soundness under the smallest cap: parts within bounds,
2807        // no 1-leaf part, exact cover.
2808        for total in 2..200usize {
2809            let sizes = merkle_batch_sizes_with_cap(total, 3);
2810            assert_eq!(sizes.iter().sum::<usize>(), total, "cover for {total}");
2811            assert!(
2812                sizes.iter().all(|&s| (2..=3).contains(&s)),
2813                "unpayable part for {total}: {sizes:?}"
2814            );
2815        }
2816    }
2817
2818    /// The external multi-batch fold mirrors the wallet path: proofs union,
2819    /// costs sum, and the merged timestamp is the OLDEST sub-batch's (worst
2820    /// case for the expiry window).
2821    #[test]
2822    fn merge_merkle_batch_results_unions_proofs_and_keeps_oldest_timestamp() {
2823        let a = MerkleBatchPaymentResult {
2824            proofs: [([1u8; 32], vec![1u8])].into_iter().collect(),
2825            chunk_count: 1,
2826            storage_cost_atto: "100".into(),
2827            gas_cost_wei: 7,
2828            merkle_payment_timestamp: 2_000,
2829        };
2830        let b = MerkleBatchPaymentResult {
2831            proofs: [([2u8; 32], vec![2u8]), ([3u8; 32], vec![3u8])]
2832                .into_iter()
2833                .collect(),
2834            chunk_count: 2,
2835            storage_cost_atto: "50".into(),
2836            gas_cost_wei: 5,
2837            merkle_payment_timestamp: 1_500,
2838        };
2839        let merged = merge_merkle_batch_results(vec![a, b]);
2840        assert_eq!(merged.proofs.len(), 3);
2841        assert_eq!(merged.chunk_count, 3);
2842        assert_eq!(merged.storage_cost_atto, "150");
2843        assert_eq!(merged.gas_cost_wei, 12);
2844        assert_eq!(merged.merkle_payment_timestamp, 1_500);
2845    }
2846
2847    /// The defect: `[256, 1]` pays the first batch on-chain and then hands a
2848    /// single address to a tree that needs two, so the upload fails *after*
2849    /// spending. Every count must produce trees that can all be built.
2850    #[test]
2851    fn merkle_batch_sizes_are_always_buildable_trees() {
2852        for total in 2..=(4 * MAX_LEAVES + 3) {
2853            let sizes = merkle_batch_sizes(total);
2854            assert!(!sizes.is_empty(), "{total} addresses must produce batches");
2855            assert_eq!(
2856                sizes.iter().sum::<usize>(),
2857                total,
2858                "{total} addresses: partition must cover every address"
2859            );
2860            for size in sizes {
2861                assert!(
2862                    (2..=MAX_LEAVES).contains(&size),
2863                    "{total} addresses produced a batch of {size}, outside 2..={MAX_LEAVES}"
2864                );
2865            }
2866        }
2867    }
2868
2869    #[test]
2870    fn merkle_batch_sizes_below_two_have_no_payable_partition() {
2871        assert!(merkle_batch_sizes(0).is_empty());
2872        assert!(merkle_batch_sizes(1).is_empty());
2873    }
2874
2875    #[test]
2876    fn merkle_batch_partitions_preserve_order_and_use_each_address_once() {
2877        for (total, _) in PARTITION_CASES {
2878            let addrs = make_test_addresses(total);
2879            let partitions = merkle_batch_partitions(&addrs);
2880
2881            let flattened: Vec<[u8; 32]> = partitions.concat();
2882            assert_eq!(
2883                flattened, addrs,
2884                "{total} addresses: partitions must concatenate back to the input in order"
2885            );
2886
2887            let unique: std::collections::HashSet<[u8; 32]> = flattened.iter().copied().collect();
2888            assert_eq!(
2889                unique.len(),
2890                total,
2891                "{total} addresses: no address may be duplicated or synthesised"
2892            );
2893        }
2894    }
2895
2896    /// A merkle upload of 257 chunks — the count the old split could not pay —
2897    /// is the shape preflight routinely leaves behind, since `to_upload` is
2898    /// whatever the network did not already hold.
2899    #[test]
2900    fn post_preflight_plan_of_257_partitions_into_payable_batches() {
2901        let plan = MerkleUploadPlan {
2902            already_stored: make_test_addresses(3),
2903            to_upload: make_test_addresses(257),
2904            to_upload_total_bytes: 257 * 1024,
2905        };
2906        assert_eq!(plan.to_upload.len(), 257);
2907
2908        let partitions = merkle_batch_partitions(&plan.to_upload);
2909        let sizes: Vec<usize> = partitions.iter().map(|batch| batch.len()).collect();
2910        assert_eq!(sizes, vec![255, 2]);
2911        for batch in partitions {
2912            let xornames: Vec<XorName> = batch.iter().map(|a| XorName(*a)).collect();
2913            assert!(
2914                MerkleTree::from_xornames(xornames).is_ok(),
2915                "every partition of a 257-chunk plan must build a tree"
2916            );
2917        }
2918    }
2919
2920    /// No batch may be paid for and then fail to build its tree: the partition
2921    /// is what payment iterates, so proving every batch builds proves no
2922    /// on-chain payment can be followed by a singleton-tree failure.
2923    #[test]
2924    fn no_partition_pays_before_a_singleton_tree_failure() {
2925        for total in [257usize, 513, 769] {
2926            let addrs = make_test_addresses(total);
2927            for batch in merkle_batch_partitions(&addrs) {
2928                let xornames: Vec<XorName> = batch.iter().map(|a| XorName(*a)).collect();
2929                assert!(
2930                    MerkleTree::from_xornames(xornames).is_ok(),
2931                    "{total} addresses: batch of {} is unpayable",
2932                    batch.len()
2933                );
2934            }
2935        }
2936    }
2937
2938    #[test]
2939    fn merkle_billable_leaves_sum_the_padded_partitions() {
2940        for (total, expected) in PARTITION_CASES {
2941            let padded: u64 = expected
2942                .iter()
2943                .map(|size| size.next_power_of_two() as u64)
2944                .sum();
2945            assert_eq!(
2946                merkle_billable_leaves(total as u64),
2947                padded,
2948                "{total} chunks must bill for the padded partition {expected:?}"
2949            );
2950        }
2951
2952        // Known figures, spelled out: padding is billed, never hidden.
2953        assert_eq!(merkle_billable_leaves(65), 128);
2954        assert_eq!(merkle_billable_leaves(257), 256 + 2);
2955        assert_eq!(merkle_billable_leaves(300), 256 + 64);
2956        // Nothing to upload costs nothing; a lone chunk still quotes the
2957        // two-leaf minimum a tree needs.
2958        assert_eq!(merkle_billable_leaves(0), 0);
2959        assert_eq!(merkle_billable_leaves(1), 2);
2960    }
2961
2962    #[test]
2963    fn merkle_billable_leaves_never_under_quote() {
2964        for chunks in 1..2000u64 {
2965            assert!(
2966                merkle_billable_leaves(chunks) >= chunks,
2967                "{chunks} chunks must never be billed as fewer leaves"
2968            );
2969        }
2970    }
2971
2972    /// The external signer prepares one tree, signs once, and pays once, so an
2973    /// oversized set is refused up front rather than being quietly paid under a
2974    /// different model.
2975    #[test]
2976    fn external_preparation_refuses_more_than_one_tree_of_addresses() {
2977        assert!(ensure_single_merkle_tree_batch(2).is_ok());
2978        assert!(ensure_single_merkle_tree_batch(MAX_LEAVES).is_ok());
2979
2980        for oversized in [MAX_LEAVES + 1, 300, 513] {
2981            match ensure_single_merkle_tree_batch(oversized) {
2982                Err(Error::MerkleBatchTooLarge {
2983                    addresses,
2984                    max_leaves,
2985                }) => {
2986                    assert_eq!(addresses, oversized);
2987                    assert_eq!(max_leaves, MAX_LEAVES);
2988                }
2989                other => panic!("{oversized} addresses should be refused, got {other:?}"),
2990            }
2991        }
2992    }
2993
2994    // =========================================================================
2995    // merkle_store_with_retry: collect-not-abort + bounded retry (C2.1 / C2.2)
2996    // =========================================================================
2997
2998    use std::sync::{Arc, Mutex};
2999
3000    /// Build `count` chunk addresses for the store helper. Bodies are read on
3001    /// demand by `store_one`, so the tests pass addresses only.
3002    fn make_addrs(count: usize) -> Vec<[u8; 32]> {
3003        make_test_addresses(count)
3004    }
3005
3006    /// C2.1: a per-chunk `InsufficientPeers` is collected, not propagated —
3007    /// the whole batch must NOT abort. With a single attempt, the failing
3008    /// subset is reported via `failed` and the rest are `stored`.
3009    #[tokio::test]
3010    async fn store_with_retry_collects_failures_instead_of_aborting() {
3011        let chunks = make_addrs(6);
3012        let failing: std::collections::HashSet<[u8; 32]> = chunks.iter().take(2).copied().collect();
3013        let failing_for_closure = failing.clone();
3014
3015        let store_one = move |addr: [u8; 32]| {
3016            let fail = failing_for_closure.contains(&addr);
3017            async move {
3018                if fail {
3019                    Err(Error::InsufficientPeers("test shortfall".into()))
3020                } else {
3021                    Ok(web_time::Instant::now())
3022                }
3023            }
3024        };
3025
3026        let outcome =
3027            merkle_store_with_retry(chunks, || 8, 1, Duration::ZERO, None, 0, 6, store_one)
3028                .await
3029                .expect("quorum shortfalls must not abort the batch");
3030
3031        assert_eq!(outcome.stored, 4);
3032        assert_eq!(outcome.failed, 2);
3033        // Single attempt → all successes recorded in round 0.
3034        assert_eq!(outcome.stats.retries_histogram[0], 4);
3035        assert_eq!(outcome.stats.chunk_attempts_total, 6);
3036    }
3037
3038    /// #167 regression, preserved at the engine seam the spill path composes
3039    /// (`upload_merkle_from_spill` runs one single-attempt pass and then the
3040    /// deferred rounds): a genuine quorum shortfall — every batch PAID, the
3041    /// store short — survives all deferred retries with
3042    /// `stored + failed == total` and the exact shortfall set in
3043    /// `failed_addresses`, which is precisely what the spill path folds into
3044    /// `Error::PartialUpload` instead of reporting success (#166).
3045    #[tokio::test]
3046    async fn quorum_shortfall_survives_deferred_retries_with_exact_accounting() {
3047        let chunks = make_addrs(5);
3048        let short: std::collections::HashSet<[u8; 32]> = chunks.iter().take(2).copied().collect();
3049        let short_for_closure = short.clone();
3050        let store_one = move |addr: [u8; 32]| {
3051            let fail = short_for_closure.contains(&addr);
3052            async move {
3053                if fail {
3054                    Err(Error::InsufficientPeers("still short of quorum".into()))
3055                } else {
3056                    Ok(std::time::Instant::now())
3057                }
3058            }
3059        };
3060
3061        // Initial pass exactly as the spill path runs it: one attempt,
3062        // shortfalls deferred rather than retried inline.
3063        let pass = merkle_store_with_retry(
3064            chunks.clone(),
3065            || 8,
3066            1,
3067            Duration::ZERO,
3068            None,
3069            0,
3070            5,
3071            &store_one,
3072        )
3073        .await
3074        .expect("quorum shortfalls must not abort the pass");
3075        assert!(pass.fatal.is_none());
3076        assert_eq!(pass.stored, 3);
3077        assert_eq!(pass.failed, 2);
3078
3079        // Deferred rounds (zero delays for the test): the same chunks stay
3080        // short through every round.
3081        let dr = merkle_deferred_retry(
3082            pass.failed_addresses.clone(),
3083            &[0, 0, 0],
3084            |n: usize| n.max(1),
3085            None,
3086            pass.stored,
3087            5,
3088            &store_one,
3089        )
3090        .await
3091        .expect("deferred shortfalls must not abort");
3092
3093        assert!(dr.fatal.is_none());
3094        assert_eq!(
3095            dr.stored + dr.failed,
3096            5,
3097            "stored + failed must account for every chunk"
3098        );
3099        assert_eq!(dr.stored, 3, "paid-and-stored chunks must stay counted");
3100        assert_eq!(dr.failed, 2);
3101        let failed_set: std::collections::HashSet<[u8; 32]> =
3102            dr.failed_addresses.iter().map(|(a, _)| *a).collect();
3103        assert_eq!(
3104            failed_set, short,
3105            "failed set must be exactly the shortfall chunks"
3106        );
3107    }
3108
3109    /// V2-554: the store scheduler must RE-READ the cap as each slot frees
3110    /// (rolling), not snapshot it once like `buffer_unordered`. A snapshot would
3111    /// invoke the cap closure once per attempt; the rolling scheduler invokes it
3112    /// once per drained slot, so mid-flight limiter growth reaches the rest of
3113    /// the round. Proven here by counting cap-closure invocations.
3114    #[tokio::test]
3115    async fn store_with_retry_rereads_cap_per_slot() {
3116        let count = 6;
3117        let chunks = make_addrs(count);
3118        let cap_calls = Arc::new(Mutex::new(0usize));
3119        let cap_calls_for_closure = cap_calls.clone();
3120        let cap = move || {
3121            *cap_calls_for_closure.lock().expect("cap counter poisoned") += 1;
3122            2
3123        };
3124        let store_one = move |_addr: [u8; 32]| async move { Ok(web_time::Instant::now()) };
3125
3126        let outcome =
3127            merkle_store_with_retry(chunks, cap, 1, Duration::ZERO, None, 0, count, store_one)
3128                .await
3129                .expect("all stores succeed");
3130
3131        assert_eq!(outcome.stored, count);
3132        let calls = *cap_calls.lock().expect("cap counter poisoned");
3133        assert!(
3134            calls >= count,
3135            "cap must be re-read per drained slot (rolling), not snapshotted once — \
3136             expected >= {count} invocations, got {calls}",
3137        );
3138    }
3139
3140    /// The whole-file store pass is a single cap-bounded fan-out with NO wave
3141    /// barrier: one slow straggler (a chunk whose peers take a long time) must
3142    /// not block the rest of the file, and must be driven to completion
3143    /// concurrently by the fast chunks. If the scheduler serialized (a barrier),
3144    /// the fast chunks could not run until the straggler returned — a deadlock
3145    /// the surrounding timeout would catch.
3146    #[tokio::test]
3147    async fn store_pass_has_no_barrier() {
3148        use std::sync::atomic::{AtomicUsize, Ordering};
3149        let count = 8;
3150        let addrs = make_addrs(count);
3151        let slow = addrs[0];
3152        let fast_completed = Arc::new(AtomicUsize::new(0));
3153        let release_slow = Arc::new(tokio::sync::Notify::new());
3154
3155        let store_one = move |addr: [u8; 32]| {
3156            let fast_completed = fast_completed.clone();
3157            let release_slow = release_slow.clone();
3158            async move {
3159                if addr == slow {
3160                    // Block until every fast chunk has finished. This can only
3161                    // resolve if the fast chunks run WHILE this one is parked —
3162                    // i.e. there is no barrier serializing the store pass.
3163                    release_slow.notified().await;
3164                } else if fast_completed.fetch_add(1, Ordering::SeqCst) + 1 == count - 1 {
3165                    release_slow.notify_one();
3166                }
3167                Ok(web_time::Instant::now())
3168            }
3169        };
3170
3171        let outcome = crate::runtime::timeout(
3172            Duration::from_secs(5),
3173            merkle_store_with_retry(addrs, || 8, 1, Duration::ZERO, None, 0, count, store_one),
3174        )
3175        .await
3176        .expect("store pass must not deadlock — a slow chunk must not block the others")
3177        .expect("all stores succeed");
3178
3179        assert_eq!(outcome.stored, count);
3180    }
3181
3182    /// Peak memory is bounded by the store cap, not the file size: `store_one`
3183    /// reads each body on demand, so the scheduler holds at most `cap` stores
3184    /// (hence at most `cap` bodies) in flight at once — the property that lets
3185    /// the whole file store as one fan-out without the old 64-chunk waves.
3186    #[tokio::test]
3187    async fn store_pass_keeps_at_most_cap_in_flight() {
3188        use std::sync::atomic::{AtomicUsize, Ordering};
3189        let count = 40;
3190        let cap = 4;
3191        let addrs = make_addrs(count);
3192        let in_flight = Arc::new(AtomicUsize::new(0));
3193        let max_in_flight = Arc::new(AtomicUsize::new(0));
3194        let max_in_flight_for_closure = max_in_flight.clone();
3195
3196        let store_one = move |_addr: [u8; 32]| {
3197            let in_flight = in_flight.clone();
3198            let max_in_flight = max_in_flight_for_closure.clone();
3199            async move {
3200                let now = in_flight.fetch_add(1, Ordering::SeqCst) + 1;
3201                max_in_flight.fetch_max(now, Ordering::SeqCst);
3202                // Yield so sibling stores get a chance to start concurrently,
3203                // maximising the observed in-flight count.
3204                tokio::task::yield_now().await;
3205                in_flight.fetch_sub(1, Ordering::SeqCst);
3206                Ok(web_time::Instant::now())
3207            }
3208        };
3209
3210        let outcome = merkle_store_with_retry(
3211            addrs,
3212            move || cap,
3213            1,
3214            Duration::ZERO,
3215            None,
3216            0,
3217            count,
3218            store_one,
3219        )
3220        .await
3221        .expect("all stores succeed");
3222
3223        assert_eq!(outcome.stored, count);
3224        let peak = max_in_flight.load(Ordering::SeqCst);
3225        assert!(
3226            peak <= cap,
3227            "at most `cap` bodies may be in flight (memory bound), got peak {peak} > cap {cap}",
3228        );
3229        assert!(
3230            peak > 1,
3231            "the pass must actually run concurrently, not serialize (peak {peak})",
3232        );
3233    }
3234
3235    /// V2-468: an app-only quorum shortfall surfaces as `Error::RemotePut`
3236    /// (pool-rejected / quote-stale / disk-full — transient), which must be
3237    /// treated as recoverable just like `InsufficientPeers`: collected and
3238    /// retried, never aborting the whole batch.
3239    #[tokio::test]
3240    async fn store_with_retry_treats_remote_put_as_recoverable() {
3241        let chunks = make_addrs(6);
3242        let failing: std::collections::HashSet<[u8; 32]> = chunks.iter().take(2).copied().collect();
3243        let failing_for_closure = failing.clone();
3244
3245        let store_one = move |addr: [u8; 32]| {
3246            let fail = failing_for_closure.contains(&addr);
3247            async move {
3248                if fail {
3249                    Err(Error::RemotePut {
3250                        address: hex::encode(addr),
3251                        source: ant_protocol::ProtocolError::StorageFailed(
3252                            "insufficient disk space".into(),
3253                        ),
3254                    })
3255                } else {
3256                    Ok(web_time::Instant::now())
3257                }
3258            }
3259        };
3260
3261        let outcome =
3262            merkle_store_with_retry(chunks, || 8, 1, Duration::ZERO, None, 0, 6, store_one)
3263                .await
3264                .expect("remote app-rejections must not abort the batch");
3265
3266        assert_eq!(outcome.stored, 4);
3267        assert_eq!(outcome.failed, 2);
3268    }
3269
3270    /// A non-quorum error (e.g. a missing proof) is captured in `fatal` rather
3271    /// than discarded — the call returns `Ok(outcome)` so the caller can decide
3272    /// whether to re-raise it or fold it into `PartialUpload`.
3273    #[tokio::test]
3274    async fn store_with_retry_reports_non_quorum_errors_as_fatal() {
3275        let chunks = make_addrs(3);
3276        let store_one = |_addr: [u8; 32]| async move {
3277            Err::<web_time::Instant, _>(Error::Payment("missing proof".into()))
3278        };
3279
3280        let outcome =
3281            merkle_store_with_retry(chunks, || 8, 3, Duration::ZERO, None, 0, 3, store_one)
3282                .await
3283                .expect("fatal is carried in the outcome, not returned as Err");
3284        assert!(matches!(outcome.fatal, Some(Error::Payment(_))));
3285    }
3286
3287    /// A fatal error mid-pass preserves the successes that already completed in
3288    /// the same pass — they are not discarded with the abort. Concurrency 1
3289    /// makes ordering deterministic: the first five chunks store, then the sixth
3290    /// aborts fatally.
3291    #[tokio::test]
3292    async fn store_with_retry_fatal_preserves_same_pass_successes() {
3293        let chunks = make_addrs(6);
3294        let bad = chunks[5];
3295        let store_one = move |addr: [u8; 32]| async move {
3296            if addr == bad {
3297                Err(Error::Payment("fatal".into()))
3298            } else {
3299                Ok(web_time::Instant::now())
3300            }
3301        };
3302
3303        let outcome =
3304            merkle_store_with_retry(chunks, || 1, 1, Duration::ZERO, None, 0, 6, store_one)
3305                .await
3306                .expect("fatal carried in outcome, not returned as Err");
3307        assert!(matches!(outcome.fatal, Some(Error::Payment(_))));
3308        // The five chunks stored before the abort are preserved, not lost.
3309        assert_eq!(outcome.stored, 5);
3310        assert_eq!(outcome.stored_addresses.len(), 5);
3311        assert!(!outcome.stored_addresses.contains(&bad));
3312        // The fatal chunk is reported as failed (not silently dropped).
3313        assert!(outcome.failed_addresses.iter().any(|(a, _)| *a == bad));
3314    }
3315
3316    /// C2.2: only the chunks that failed the previous round are retried.
3317    #[tokio::test]
3318    async fn store_with_retry_retries_only_the_failed_set() {
3319        let chunks = make_addrs(5);
3320        let total = chunks.len();
3321        let failing: std::collections::HashSet<[u8; 32]> = chunks.iter().take(2).copied().collect();
3322        let failing_for_closure = failing.clone();
3323
3324        // Record every (addr) the store op was invoked with, in call order.
3325        let calls = Arc::new(Mutex::new(Vec::<[u8; 32]>::new()));
3326        let calls_for_closure = calls.clone();
3327
3328        let store_one = move |addr: [u8; 32]| {
3329            let calls = calls_for_closure.clone();
3330            // Fails the first round only; succeeds thereafter.
3331            let already_seen = calls.lock().unwrap().iter().filter(|&&a| a == addr).count();
3332            let fail = failing_for_closure.contains(&addr) && already_seen == 0;
3333            calls.lock().unwrap().push(addr);
3334            async move {
3335                if fail {
3336                    Err(Error::InsufficientPeers("round-1 shortfall".into()))
3337                } else {
3338                    Ok(web_time::Instant::now())
3339                }
3340            }
3341        };
3342
3343        let outcome =
3344            merkle_store_with_retry(chunks, || 8, 3, Duration::ZERO, None, 0, total, store_one)
3345                .await
3346                .expect("should converge after retry");
3347
3348        assert_eq!(outcome.stored, total);
3349        assert_eq!(outcome.failed, 0);
3350
3351        // Round 1 drains fully before round 2 starts, so the call log is
3352        // segmented: first `total` calls = round 1 (all chunks), the rest =
3353        // the retry round, which must contain ONLY the failing set.
3354        let calls = calls.lock().unwrap();
3355        assert_eq!(calls.len(), total + failing.len());
3356        let round_two: std::collections::HashSet<[u8; 32]> =
3357            calls[total..].iter().copied().collect();
3358        assert_eq!(round_two, failing);
3359    }
3360
3361    /// C2.2: a chunk that fails attempt 1 and succeeds attempt 2 is counted
3362    /// once as stored and recorded as one retry in `retries_histogram[1]`.
3363    #[tokio::test]
3364    async fn store_with_retry_counts_retry_success_once_in_histogram() {
3365        let chunks = make_addrs(4);
3366        let total = chunks.len();
3367        let flaky_addr = chunks[0];
3368
3369        let attempts = Arc::new(Mutex::new(HashMap::<[u8; 32], usize>::new()));
3370        let attempts_for_closure = attempts.clone();
3371
3372        let store_one = move |addr: [u8; 32]| {
3373            let attempts = attempts_for_closure.clone();
3374            let n = {
3375                let mut m = attempts.lock().unwrap();
3376                let entry = m.entry(addr).or_insert(0);
3377                *entry += 1;
3378                *entry
3379            };
3380            let fail = addr == flaky_addr && n == 1;
3381            async move {
3382                if fail {
3383                    Err(Error::InsufficientPeers("transient".into()))
3384                } else {
3385                    Ok(web_time::Instant::now())
3386                }
3387            }
3388        };
3389
3390        let outcome =
3391            merkle_store_with_retry(chunks, || 8, 3, Duration::ZERO, None, 0, total, store_one)
3392                .await
3393                .expect("flaky chunk should recover on retry");
3394
3395        assert_eq!(outcome.stored, total);
3396        assert_eq!(outcome.failed, 0);
3397        // 3 chunks landed on the first attempt, 1 on the first retry.
3398        assert_eq!(outcome.stats.retries_histogram[0], total - 1);
3399        assert_eq!(outcome.stats.retries_histogram[1], 1);
3400        // One extra store attempt for the flaky chunk.
3401        assert_eq!(outcome.stats.chunk_attempts_total, total + 1);
3402    }
3403
3404    /// C2.2: when every chunk stays short of quorum through the whole attempt
3405    /// budget, the helper still returns `Ok` (collect-not-abort) with the full
3406    /// batch reported as `failed`, having tried each chunk exactly
3407    /// `MERKLE_STORE_MAX_ATTEMPTS` times.
3408    #[tokio::test]
3409    async fn store_with_retry_reports_all_failed_when_retries_exhausted() {
3410        let chunks = make_addrs(3);
3411        let total = chunks.len();
3412
3413        let store_one = |_addr: [u8; 32]| async move {
3414            Err::<web_time::Instant, _>(Error::InsufficientPeers("never converges".into()))
3415        };
3416
3417        let outcome = merkle_store_with_retry(
3418            chunks,
3419            || 8,
3420            MERKLE_STORE_MAX_ATTEMPTS,
3421            Duration::ZERO,
3422            None,
3423            0,
3424            total,
3425            store_one,
3426        )
3427        .await
3428        .expect("an exhausted retry budget is reported, not propagated as Err");
3429
3430        assert_eq!(outcome.stored, 0);
3431        assert_eq!(outcome.failed, total);
3432        // Every chunk was attempted once per round across the full budget.
3433        assert_eq!(
3434            outcome.stats.chunk_attempts_total,
3435            total * MERKLE_STORE_MAX_ATTEMPTS
3436        );
3437        // No successes, so the histogram stays empty.
3438        assert_eq!(outcome.stats.retries_histogram, [0; 4]);
3439    }
3440
3441    /// D (CLI path): when retries are exhausted, `failed_addresses` names
3442    /// exactly the still-short-of-quorum chunks (with their last error message)
3443    /// and excludes the ones that stored. This is what `upload_merkle_from_spill`
3444    /// uses to build `PartialUpload`.
3445    #[tokio::test]
3446    async fn store_with_retry_records_failed_addresses_when_exhausted() {
3447        let chunks = make_addrs(6);
3448        let failing: std::collections::HashSet<[u8; 32]> = chunks.iter().take(2).copied().collect();
3449        let failing_for_closure = failing.clone();
3450
3451        let store_one = move |addr: [u8; 32]| {
3452            let fail = failing_for_closure.contains(&addr);
3453            async move {
3454                if fail {
3455                    Err(Error::InsufficientPeers("permanent shortfall".into()))
3456                } else {
3457                    Ok(web_time::Instant::now())
3458                }
3459            }
3460        };
3461
3462        let outcome = merkle_store_with_retry(
3463            chunks,
3464            || 8,
3465            MERKLE_STORE_MAX_ATTEMPTS,
3466            Duration::ZERO,
3467            None,
3468            0,
3469            6,
3470            store_one,
3471        )
3472        .await
3473        .expect("quorum shortfalls must not abort the batch");
3474
3475        assert_eq!(outcome.stored, 4);
3476        assert_eq!(outcome.failed, 2);
3477        // `failed_addresses` names exactly the failing set, no stored chunks.
3478        assert_eq!(outcome.failed_addresses.len(), 2);
3479        let reported: std::collections::HashSet<[u8; 32]> =
3480            outcome.failed_addresses.iter().map(|(a, _)| *a).collect();
3481        assert_eq!(reported, failing);
3482        // Each carries a non-empty error message for the PartialUpload report.
3483        for (_, msg) in &outcome.failed_addresses {
3484            assert!(msg.contains("permanent shortfall"));
3485        }
3486    }
3487
3488    /// `failed_addresses` is empty when every chunk reaches quorum (no
3489    /// `PartialUpload` is raised by the CLI path in that case).
3490    #[tokio::test]
3491    async fn store_with_retry_failed_addresses_empty_on_full_success() {
3492        let chunks = make_addrs(4);
3493        let total = chunks.len();
3494        let store_one = |_addr: [u8; 32]| async move { Ok(web_time::Instant::now()) };
3495
3496        let outcome = merkle_store_with_retry(
3497            chunks,
3498            || 8,
3499            MERKLE_STORE_MAX_ATTEMPTS,
3500            Duration::ZERO,
3501            None,
3502            0,
3503            total,
3504            store_one,
3505        )
3506        .await
3507        .expect("all chunks store");
3508
3509        assert_eq!(outcome.stored, total);
3510        assert_eq!(outcome.failed, 0);
3511        assert!(outcome.failed_addresses.is_empty());
3512    }
3513
3514    // =========================================================================
3515    // merkle_deferred_retry: download-style concurrent post-wave retry (V2-466)
3516    // =========================================================================
3517
3518    /// The histogram slot mapping: the wave first pass is slot 0; deferred
3519    /// round `r` is slot `r + 1`, clamped to the last slot.
3520    #[test]
3521    fn deferred_round_histogram_slot_maps_and_clamps() {
3522        assert_eq!(deferred_round_histogram_slot(0, 4), 1);
3523        assert_eq!(deferred_round_histogram_slot(1, 4), 2);
3524        assert_eq!(deferred_round_histogram_slot(2, 4), 3);
3525        // Beyond the histogram width, clamp to the final slot.
3526        assert_eq!(deferred_round_histogram_slot(3, 4), 3);
3527        assert_eq!(deferred_round_histogram_slot(9, 4), 3);
3528    }
3529
3530    fn deferred_set(count: usize) -> Vec<([u8; 32], String)> {
3531        make_test_addresses(count)
3532            .into_iter()
3533            .map(|addr| (addr, "short of quorum".to_string()))
3534            .collect()
3535    }
3536
3537    /// A chunk that is quorum-short on early rounds but succeeds on a later
3538    /// round is stored exactly once, recorded in that round's histogram slot,
3539    /// and reported with no failures.
3540    #[tokio::test]
3541    async fn deferred_retry_succeeds_on_a_later_round() {
3542        let deferred = deferred_set(3);
3543        // Each chunk fails its first attempt (round 0) and succeeds the second
3544        // (round 1 → histogram slot 2).
3545        let attempts = Arc::new(Mutex::new(HashMap::<[u8; 32], usize>::new()));
3546        let attempts_for_closure = attempts.clone();
3547        let store_one = move |addr: [u8; 32]| {
3548            let attempts = attempts_for_closure.clone();
3549            async move {
3550                let n = {
3551                    let mut map = attempts.lock().unwrap();
3552                    let e = map.entry(addr).or_insert(0);
3553                    *e += 1;
3554                    *e
3555                };
3556                if n < 2 {
3557                    Err(Error::InsufficientPeers("still short".into()))
3558                } else {
3559                    Ok(web_time::Instant::now())
3560                }
3561            }
3562        };
3563
3564        let outcome = merkle_deferred_retry(
3565            deferred,
3566            &[0, 0, 0],
3567            |n: usize| n.max(1),
3568            None,
3569            0,
3570            3,
3571            store_one,
3572        )
3573        .await
3574        .expect("deferred retry must not abort on quorum shortfalls");
3575
3576        assert_eq!(outcome.stored, 3, "all three land by round 1");
3577        assert_eq!(outcome.stored_addresses.len(), 3);
3578        assert_eq!(outcome.failed, 0);
3579        assert!(outcome.failed_addresses.is_empty());
3580        assert!(outcome.fatal.is_none());
3581        // Round 1 → slot 2; round 0 (slot 1) saw zero successes.
3582        assert_eq!(outcome.stats.retries_histogram[1], 0);
3583        assert_eq!(outcome.stats.retries_histogram[2], 3);
3584        // Each chunk attempted twice: one failed round + one success round.
3585        assert_eq!(outcome.stats.chunk_attempts_total, 6);
3586    }
3587
3588    /// Chunks still short of quorum after the final deferred round become
3589    /// `failed`, not silently dropped, and no fatal error is set.
3590    #[tokio::test]
3591    async fn deferred_retry_leftovers_become_failed() {
3592        let deferred = deferred_set(2);
3593        let store_one = |_addr: [u8; 32]| async move {
3594            Err::<web_time::Instant, _>(Error::InsufficientPeers("always short".into()))
3595        };
3596
3597        let outcome = merkle_deferred_retry(
3598            deferred,
3599            &[0, 0, 0],
3600            |n: usize| n.max(1),
3601            None,
3602            0,
3603            2,
3604            store_one,
3605        )
3606        .await
3607        .expect("exhausted retries report failures, not an error");
3608
3609        assert_eq!(outcome.stored, 0);
3610        assert!(outcome.stored_addresses.is_empty());
3611        assert_eq!(outcome.failed, 2);
3612        assert_eq!(outcome.failed_addresses.len(), 2);
3613        assert!(outcome.fatal.is_none());
3614        // Three rounds × two chunks, all failing.
3615        assert_eq!(outcome.stats.chunk_attempts_total, 6);
3616    }
3617
3618    /// A non-quorum (fatal) error during a deferred round stops the pass, is
3619    /// surfaced via `fatal`, and preserves an earlier round's success in
3620    /// `stored`/`stored_addresses` while the still-pending chunk is reported as
3621    /// failed.
3622    #[tokio::test]
3623    async fn deferred_retry_fatal_error_preserves_prior_progress() {
3624        let addrs = make_test_addresses(2);
3625        let good = addrs[0];
3626        let bad = addrs[1];
3627        let deferred = vec![(good, "short".to_string()), (bad, "short".to_string())];
3628
3629        // `good` succeeds on round 0; `bad` is quorum-short on round 0, then
3630        // hits a fatal Payment error on round 1.
3631        let attempts = Arc::new(Mutex::new(HashMap::<[u8; 32], usize>::new()));
3632        let attempts_for_closure = attempts.clone();
3633        let store_one = move |addr: [u8; 32]| {
3634            let attempts = attempts_for_closure.clone();
3635            async move {
3636                let n = {
3637                    let mut map = attempts.lock().unwrap();
3638                    let e = map.entry(addr).or_insert(0);
3639                    *e += 1;
3640                    *e
3641                };
3642                if addr == good {
3643                    Ok(web_time::Instant::now())
3644                } else if n == 1 {
3645                    Err(Error::InsufficientPeers("short".into()))
3646                } else {
3647                    Err(Error::Payment("fatal on retry".into()))
3648                }
3649            }
3650        };
3651
3652        let outcome = merkle_deferred_retry(
3653            deferred,
3654            &[0, 0, 0],
3655            |n: usize| n.max(1),
3656            None,
3657            0,
3658            2,
3659            store_one,
3660        )
3661        .await
3662        .expect("a fatal round error is reported via `fatal`, not as Err");
3663
3664        assert!(outcome.fatal.is_some(), "fatal error must be captured");
3665        assert_eq!(outcome.stored, 1, "round-0 success preserved");
3666        assert_eq!(outcome.stored_addresses, vec![good]);
3667        assert_eq!(outcome.failed, 1);
3668        assert_eq!(outcome.failed_addresses.len(), 1);
3669        assert_eq!(outcome.failed_addresses[0].0, bad);
3670    }
3671
3672    /// An empty deferred set is a no-op: no rounds run, nothing stored or failed.
3673    #[tokio::test]
3674    async fn deferred_retry_empty_set_is_a_noop() {
3675        let store_one = |_addr: [u8; 32]| async move {
3676            Err::<web_time::Instant, _>(Error::InsufficientPeers("unused".into()))
3677        };
3678
3679        let outcome = merkle_deferred_retry(
3680            Vec::new(),
3681            &DEFERRED_ROUND_DELAYS_SECS,
3682            |n: usize| n.max(1),
3683            None,
3684            7,
3685            7,
3686            store_one,
3687        )
3688        .await
3689        .expect("empty deferred set is a no-op");
3690
3691        assert_eq!(outcome.stored, 7, "stored_offset carried through unchanged");
3692        assert_eq!(outcome.failed, 0);
3693        assert!(outcome.stored_addresses.is_empty());
3694        assert!(outcome.failed_addresses.is_empty());
3695        assert!(outcome.fatal.is_none());
3696    }
3697
3698    // =========================================================================
3699    // Settlement refusals on the merkle path
3700    // =========================================================================
3701
3702    /// A refusal is unauthenticated and two of them latch this client for the
3703    /// rest of its run, so the merkle path must apply the same coherence check
3704    /// the single-node path applies. Without it, two faulty or hostile
3705    /// candidates could deny every upload with a refusal that is not about this
3706    /// client at all.
3707    #[test]
3708    fn a_merkle_refusal_that_does_not_describe_this_client_is_ignored() {
3709        use ant_protocol::CURRENT_SETTLEMENT_VERSION;
3710
3711        let peer_id = PeerId::from_bytes([0x71; 32]);
3712        let refusal = |client: u32, min: u32| {
3713            map_merkle_candidate_response(
3714                peer_id,
3715                ChunkMessageBody::MerkleCandidateQuoteResponse(
3716                    MerkleCandidateQuoteResponse::Error(ProtocolError::ClientUpdateRequired {
3717                        client_settlement_version: client,
3718                        min_settlement_version: min,
3719                    }),
3720                ),
3721            )
3722        };
3723
3724        // Echoes a version that is not ours: the peer is talking about someone
3725        // else's request.
3726        let wrong_echo = refusal(
3727            CURRENT_SETTLEMENT_VERSION.saturating_add(7),
3728            CURRENT_SETTLEMENT_VERSION.saturating_add(8),
3729        );
3730        assert!(
3731            matches!(wrong_echo, Some(Err(Error::Protocol(_)))),
3732            "{wrong_echo:?}"
3733        );
3734
3735        // States a minimum this client already meets, so there is nothing to
3736        // refuse.
3737        let no_gap = refusal(CURRENT_SETTLEMENT_VERSION, CURRENT_SETTLEMENT_VERSION);
3738        assert!(
3739            matches!(no_gap, Some(Err(Error::Protocol(_)))),
3740            "{no_gap:?}"
3741        );
3742
3743        // A coherent one is believed, and still carries the upgrade wording.
3744        match refusal(
3745            CURRENT_SETTLEMENT_VERSION,
3746            CURRENT_SETTLEMENT_VERSION.saturating_add(1),
3747        ) {
3748            Some(Err(Error::ClientUpdateRequired(msg))) => {
3749                assert!(msg.contains("ant update"), "{msg}");
3750            }
3751            other => panic!("expected ClientUpdateRequired, got {other:?}"),
3752        }
3753    }
3754
3755    /// Pools are drained rather than cancelled on the first error, so more than
3756    /// one can fail. A refusal has to win: reporting the ordinary failure
3757    /// instead would hide the one verdict that stops the caller falling back to
3758    /// another payment path and spending.
3759    #[test]
3760    fn a_refusal_outranks_an_ordinary_pool_failure() {
3761        let refusal = || Error::ClientUpdateRequired("run ant update".to_string());
3762        let ordinary = || Error::InsufficientPeers("need 16, got 2".to_string());
3763
3764        // Ordinary failure first, refusal second.
3765        let mut verdict = PoolVerdict::default();
3766        verdict.note(ordinary());
3767        verdict.note(refusal());
3768        assert!(
3769            matches!(verdict.into_error(), Some(Error::ClientUpdateRequired(_))),
3770            "a later refusal must still outrank an earlier failure"
3771        );
3772
3773        // Refusal first, ordinary failure second. Order must not matter.
3774        let mut verdict = PoolVerdict::default();
3775        verdict.note(refusal());
3776        verdict.note(ordinary());
3777        assert!(
3778            matches!(verdict.into_error(), Some(Error::ClientUpdateRequired(_))),
3779            "an earlier refusal must not be displaced by a later failure"
3780        );
3781
3782        // With no refusal, the first ordinary failure is what the caller sees.
3783        let mut verdict = PoolVerdict::default();
3784        verdict.note(ordinary());
3785        verdict.note(Error::Protocol("second".to_string()));
3786        assert!(
3787            matches!(verdict.into_error(), Some(Error::InsufficientPeers(_))),
3788            "the first ordinary failure is the one reported"
3789        );
3790
3791        // Every pool succeeded, so there is nothing to report.
3792        assert!(PoolVerdict::default().into_error().is_none());
3793    }
3794}