Skip to main content

decdn_client/
driver.rs

1//! The #1608 gap-driven, range-minimized fetch driver.
2//!
3//! [`drive`] satisfies a request `R = [offset, offset+len)` of one blob by
4//! filling ONLY the gaps the store is missing, paying the minimum: held ranges
5//! are read locally, never pulled, never re-paid. It is the integration keystone
6//! of the #1621 ranged-store effort — the piece that turns the sourcing axis
7//! ([`BlobSource`]) and the pacing axis ([`Pacer`]) into a single fetch that
8//! pulls exactly the bytes a request needs.
9//!
10//! # The money-relevant branches
11//!
12//! A whole-tail pull streams one advancing `byte_offset` to the end of the blob;
13//! `drive` instead drives the same money-relevant branches per gap: it draws
14//! and resumes one missing range at a time. The acquire loop
15//! ([`crate::acquire`]) runs the same per-gap loop on each of its lanes. The
16//! branches are:
17//!
18//! - **Draw** (the happy path): open the gap's [`decdn_bao_range::AlignedRange`], stream it through
19//!   [`crate::ClientRangedStore::ingest_stream`] (which durably checkpoints as it goes),
20//!   then [`BlobSource::finish`] to drain the pull and recover the acked voucher
21//!   watermark. The store's checkpoints are what make a mid-gap fault re-enter
22//!   with a SMALLER gap: the store owns durability, so a resume never re-pulls a
23//!   checkpointed prefix.
24//! - **Refuse**: a lane whose deposit cannot cover the next voucher, a node's
25//!   `Unfunded` refusal, and a mid-stream funding rejection all end the lane.
26//!   The acquire loop marks its source priced out at the current deposit, and
27//!   adds funds only through the funding recovery step at the exhausted
28//!   candidate set (ADR 003 § Funding recovery).
29//! - **Paid-frontier resume**: within a gap, each new leg opens at the gap's
30//!   PAID frontier, NOT its checkpointed (delivered) frontier.
31//!   [`crate::ClientRangedStore::ingest_stream`] checkpoints delivered+verified
32//!   bytes payment-agnostically (the ADR 003 credit window lets the node stream a
33//!   full interval before the voucher that pays for it is due), so a leg can
34//!   leave the store's present-range frontier AHEAD of the last PAID byte. The
35//!   per-leg resume offset is [`sink::content_paid_frontier`] of the leg's paid
36//!   wire watermark: the tail is re-delivered (`ingest_stream` re-writes it
37//!   idempotently) and re-billed. A lane that a funding refusal ends records
38//!   that tail on its ledger, and the lane's next drive bills it first.
39//! - **Reseed** (wallet-less resync, #1481): an authenticated
40//!   [`WatermarkBundle`] that ADVANCES our committed watermark is a healable
41//!   desync — the driver reseeds the ledger
42//!   ([`PoolLedger::reseed`]) and retries. This is driver-owned, NOT a
43//!   [`PaceDecision`] — the reseed check runs ahead of pacing, the same ordering
44//!   the node's own miss-pull policy uses.
45//!
46//! # What `drive` does not do
47//!
48//! - No **stale-foreign-partial restart-from-zero**. A
49//!   [`crate::ClientRangedStore`] is keyed to `(root, total_bytes)` and only ever
50//!   holds bao-verified ranges, so an ambiguous `NotFound` can never mean "this
51//!   file belongs to another blob": there is no foreign-partial ambiguity to
52//!   resolve, and resume is always driven by the verified present set.
53//! - No **progress reporting, deadlines, or durable watermark persistence** to a
54//!   `BuyerPoolStore` — these are the caller's job. The acked watermark lives
55//!   in the [`PoolLedger`] the caller owns; persisting it across process restarts,
56//!   and drawing a progress bar, are CLI concerns.
57//!
58//! # Store abstraction
59//!
60//! The store is generic over [`crate::source::IngestStore`] (`RangedStore` +
61//! `ingest_stream`), not the concrete `&ClientRangedStore` — [`crate::ClientRangedStore`]
62//! is one implementer (the CLI/client backend, writing `.partial`/`.ranges`); a
63//! node backend admits to the cache and tees to its downstream client
64//! through the same seam.
65
66use std::future::Future;
67use std::pin::Pin;
68use std::sync::atomic::{AtomicU64, Ordering};
69use std::sync::{Arc, Mutex};
70use std::time::Duration;
71
72use alloy::primitives::U256;
73use bao_tree::ChunkRanges;
74use decdn_bao_range::{AlignedRange, RangedStore, RangedStoreError, align_range};
75use decdn_protocol::VoucherRejectReason;
76
77use crate::fault::HealExhausted;
78use crate::pacer::{DownstreamFrontier, PaceDecision, PaceState};
79use crate::source::{BlobSource, IngestEnd, IngestStore, SourceFuture};
80use crate::{
81    MAX_RESUME_ATTEMPTS, Pacer, PoolContext, PoolLedger, UpstreamPullHeader,
82    UpstreamVoucherRejected, heal_watermark_desync, reject_empty_claim_for_nonempty_root,
83    rejection_watermark,
84};
85
86/// The shared pool cannot fund the next voucher: its remaining deposit is below
87/// the next voucher's cost (the pacer returned [`PaceDecision::Refuse`]).
88///
89/// Typed rather than a bare string so the fault classifier
90/// ([`crate::classify`]) can `downcast_ref` it and rule it
91/// [`crate::Fault::Unaffordable`]: the source is priced out at the current
92/// deposit, and the acquire loop adds funds only through its funding recovery
93/// step (ADR 003 § Funding recovery).
94#[derive(Debug)]
95#[cfg_attr(not(feature = "test-util"), non_exhaustive)]
96pub struct PoolExhausted {
97    /// Start of the gap that could not be funded.
98    pub gap_start: u64,
99    /// Length of the gap that could not be funded.
100    pub gap_len: u64,
101}
102
103impl std::fmt::Display for PoolExhausted {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        write!(
106            f,
107            "gap [{}, +{}) of blob cannot be funded: the remaining deposit cannot cover the \
108             next voucher",
109            self.gap_start, self.gap_len
110        )
111    }
112}
113
114impl std::error::Error for PoolExhausted {}
115
116/// A leg of a gap opened, streamed and finished cleanly, yet left both the gap's
117/// paid frontier and the store's delivered frontier where they were.
118///
119/// A clean finish leaves the ledger crediting every wire byte the leg received:
120/// one reveal per whole chunk, plus a closing signature for any residual. The
121/// paid frontier therefore reaches the end of what the leg delivered, capped at
122/// the delivered frontier, and the leg's bytes land in the store. A clean leg
123/// that moves neither frontier means the store did not keep the bytes, the
124/// ledger did not record the payment, or the upstream ended the stream without
125/// taking the leg's final proof. It never means a slow peer: a stalled leg ends
126/// with a stall error, not a clean finish. Opening the same range again would
127/// pay for the same bytes to the same effect, so the gap-fill loop behind
128/// [`drive`] and [`crate::acquire`] ends the gap with this error instead
129/// (#2194).
130///
131/// [`crate::classify`] rules it [`crate::Fault::Source`]. The cause may be the
132/// source, and each further source costs at most one more leg before it ends the
133/// same way.
134#[derive(Debug)]
135#[doc(hidden)]
136pub struct LegNoProgress {
137    /// Start of the range the leg opened.
138    pub offset: u64,
139    /// Length of the range the leg opened.
140    pub len: u64,
141    /// The gap's paid frontier when the leg opened. The pass after the leg found
142    /// it no further on.
143    pub paid_frontier: u64,
144    /// The store's delivered frontier in the gap when the leg opened. The pass
145    /// after the leg found it no further on.
146    pub delivered_frontier: u64,
147    /// The lane ledger's committed WIRE bytes since the leg opened. The ledger
148    /// is shared, so this counts concurrent pulls on the same lane too. With no
149    /// concurrent pull, enough wire to cover the leg's first chunk group points at
150    /// the store (it did not keep what was paid for), and zero points at the
151    /// ledger or the upstream (the leg's payment was never recorded).
152    pub paid_wire: u64,
153}
154
155impl std::fmt::Display for LegNoProgress {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        write!(
158            f,
159            "leg [{}, +{}) finished cleanly but advanced neither the paid frontier ({}) nor \
160             the delivered frontier ({}), with {} wire bytes credited; refusing to re-open \
161             and re-pay the same range",
162            self.offset, self.len, self.paid_frontier, self.delivered_frontier, self.paid_wire
163        )
164    }
165}
166
167impl std::error::Error for LegNoProgress {}
168
169/// The last leg of a gap that finished cleanly, as the pass that opened it saw the
170/// gap. [`fill_gap`]'s next pass must see one of the two frontiers move.
171#[derive(Debug, Clone, Copy)]
172struct CleanLeg {
173    /// Start of the range the leg opened.
174    offset: u64,
175    /// Length of the range the leg opened.
176    len: u64,
177    /// The gap's paid frontier when the leg opened.
178    paid_frontier: u64,
179    /// The store's delivered frontier when the leg opened.
180    delivered_frontier: u64,
181    /// The ledger generation when the leg opened. A rebase since then moved the
182    /// shared watermark under the leg, so its paid-frontier reading is void.
183    generation: u64,
184}
185
186impl CleanLeg {
187    /// The [`LegNoProgress`] this leg amounts to, given what the next pass reads:
188    /// both frontiers, the wire the ledger credited since the leg opened, and the
189    /// ledger generation. `None` when either frontier moved, or when a rebase makes
190    /// the paid frontier incomparable.
191    const fn stalled(
192        self,
193        paid_frontier: u64,
194        delivered_frontier: u64,
195        paid_wire: u64,
196        generation: u64,
197    ) -> Option<LegNoProgress> {
198        if generation != self.generation
199            || paid_frontier > self.paid_frontier
200            || delivered_frontier > self.delivered_frontier
201        {
202            return None;
203        }
204        Some(LegNoProgress {
205            offset: self.offset,
206            len: self.len,
207            paid_frontier: self.paid_frontier,
208            delivered_frontier: self.delivered_frontier,
209            paid_wire,
210        })
211    }
212}
213
214/// The state ONE pool deposit's concurrent lanes share, injected by the
215/// multi-source scheduler. Absent (`None`) on the single-source path, where the
216/// one lane IS the pool and its own `DriveCounters` and [`PoolContext`] already
217/// hold every fact below.
218///
219/// The pool's spend is a property of the POOL, not of a lane: the deposit gate
220/// must subtract what EVERY lane committed, not what this one did, or N lanes
221/// each spend what only one pool holds. The deposit every lane draws on rises
222/// through the funding recovery step, which credits every lane's
223/// `PoolContext` itself, or through a low-water refill a lane build makes,
224/// which raises every lane to it.
225#[doc(hidden)]
226pub struct SharedPool<'a> {
227    /// Sum, across every lane, of the committed voucher amount — the pool's
228    /// total spend so far.
229    pub spent: &'a (dyn Fn() -> U256 + Send + Sync),
230    /// Where each lane folds the rate and voucher interval its node quoted, so
231    /// the fetch can price the work left ([`crate::FundingEvent`]). `None`
232    /// records nothing.
233    pub quotes: Option<&'a crate::credential::QuoteMax>,
234}
235
236impl std::fmt::Debug for SharedPool<'_> {
237    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238        f.debug_struct("SharedPool")
239            .field("spent", &(self.spent)())
240            .finish()
241    }
242}
243
244/// Why a window-bounded pacer paused the pull, handed to [`PacingWait::wait`]
245/// so the caller can meter the two pauses apart.
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247#[doc(hidden)]
248pub enum WaitReason {
249    /// [`PaceDecision::Wait`]: the pull has run its full window ahead of the
250    /// downstream paid frontier.
251    WindowFull,
252    /// [`PaceDecision::WaitForMinDraw`]: the window has room, but less than the
253    /// minimum draw.
254    MinDraw,
255}
256
257/// The injected wait signal for [`PaceDecision::Wait`] and
258/// [`PaceDecision::WaitForMinDraw`] (ADR 037): the
259/// node hands in an implementor that resolves once its serve leg's paid frontier
260/// or demand frontier has advanced (so a re-decide has a chance of finding room);
261/// the client path never needs one, since `BudgetPacer` never returns `Wait`.
262#[doc(hidden)]
263pub trait PacingWait: Send + Sync {
264    /// Resolve once the caller judges it worth re-deciding (e.g. a downstream
265    /// frontier advanced, or a bounded poll interval elapsed).
266    ///
267    /// `observed` holds the downstream frontiers the caller's `Wait` decision was
268    /// computed from. An implementor backed by an edge-triggered wakeup (a
269    /// [`tokio::sync::Notify`], which stores no permit across `notify_waiters`) MUST
270    /// register its wakeup BEFORE re-reading the live frontiers and return
271    /// immediately if either already moved past `observed` — otherwise an advance
272    /// that races between the decision and the park is lost and the caller wedges
273    /// forever (#1673).
274    ///
275    /// `reason` says which pause the decision was, for metering only; the wait
276    /// itself is the same for both.
277    fn wait(
278        &self,
279        observed: DownstreamFrontier,
280        reason: WaitReason,
281    ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
282}
283
284/// Read the pool context's current deposit through the shared handle. A tiny
285/// helper so the driver never holds the lock across an `.await` — it locks,
286/// copies the `U256`, and drops the guard.
287fn locked_deposit(ctx: &Mutex<PoolContext>) -> anyhow::Result<U256> {
288    Ok(ctx
289        .lock()
290        .map_err(|_| anyhow::anyhow!("pool context lock poisoned"))?
291        .deposit)
292}
293
294/// Bytes per [`bao_tree::ChunkNum`] — a 1 KiB bao chunk. A gap's byte span is its
295/// chunk-range boundaries scaled by this.
296const CHUNK_BYTES: u64 = 1024;
297
298/// How often a fetch's single flush owner persists the `.ranges` present record
299/// while sources are still delivering. The ranged store's ingest `checkpoint`
300/// fsyncs data and outboard every ~4 MiB but never writes the record, so this
301/// interval bounds crash-loss of resume progress to at most one interval (spec
302/// §5.5): a killed fetch resumes from the last flushed frontier instead of
303/// refetching — and re-paying for — the whole in-flight download. 5 seconds
304/// mirrors the voucher-flush cadence.
305pub(crate) const PRESENT_RECORD_FLUSH_INTERVAL: Duration = Duration::from_secs(5);
306
307/// The spans [`drive`] took from its lane's unpaid record
308/// ([`PoolLedger::take_unpaid`]). The lane owes them until a gap bills them,
309/// so whatever ends the drive first, an error or the caller dropping the
310/// drive, notes back every span that ends past the offset billing reached.
311/// A span the gap's own funding refusal also noted bills once:
312/// `take_unpaid` merges.
313struct OwedSpans<'l> {
314    ledger: &'l PoolLedger,
315    hash: [u8; 32],
316    /// The owed spans, as `(start, len)` in ascending order.
317    spans: Vec<(u64, u64)>,
318    /// The content offset billing reached: the start of the gap in flight,
319    /// its paid frontier once the gap failed, or `u64::MAX` once every gap
320    /// is filled.
321    unbilled_from: AtomicU64,
322}
323
324impl<'l> OwedSpans<'l> {
325    /// Take `hash`'s owed spans from `ledger`.
326    fn take(ledger: &'l PoolLedger, hash: [u8; 32]) -> Self {
327        Self {
328            ledger,
329            hash,
330            spans: ledger.take_unpaid(hash),
331            unbilled_from: AtomicU64::new(0),
332        }
333    }
334
335    /// Billing has reached `offset`.
336    fn unbilled_from(&self, offset: u64) {
337        self.unbilled_from.store(offset, Ordering::Relaxed);
338    }
339
340    /// Every gap is filled, so every owed span is billed.
341    fn billed(&self) {
342        self.unbilled_from(u64::MAX);
343    }
344}
345
346impl Drop for OwedSpans<'_> {
347    fn drop(&mut self) {
348        let from = self.unbilled_from.load(Ordering::Relaxed);
349        for &(start, len) in &self.spans {
350            let end = start.saturating_add(len);
351            if end > from {
352                self.ledger.note_unpaid(self.hash, start.max(from), end);
353            }
354        }
355    }
356}
357
358/// Drive `fut` (a fetch's whole gap/worker set) to completion while a single
359/// periodic tick flushes the store's `.ranges` present record every `interval`,
360/// so a crash mid-fetch costs at most one `interval` of resume progress (spec
361/// §5.5). `fut` is the SOLE work driver and this loop is the SOLE periodic flush
362/// owner — nothing inside `fut` flushes — which preserves the single-writer
363/// property the ranged store's ingest `checkpoint` relies on. Returns once `fut`
364/// resolves; the caller does the final flush.
365///
366/// `tokio::time::interval`'s first tick fires immediately, so it is consumed
367/// before the loop to avoid a redundant flush at start.
368pub(crate) async fn drive_with_interval_flush<St, Fut>(
369    store: &St,
370    interval: Duration,
371    fut: Fut,
372) -> anyhow::Result<()>
373where
374    St: IngestStore,
375    Fut: Future<Output = anyhow::Result<()>>,
376{
377    tokio::pin!(fut);
378    let mut tick = tokio::time::interval(interval);
379    tick.tick().await; // consume the immediate first tick
380    // The record write in flight, if any. It runs beside `fut`, never instead
381    // of it: `fut` carries every lane's pay loop, so pausing it for a record
382    // fsync would stall voucher sends. A tick that finds a write still in
383    // flight skips, which keeps this the single record writer.
384    let mut flush: Option<SourceFuture<'_, ()>> = None;
385    loop {
386        tokio::select! {
387            // Prefer completion: if the work is done, finish rather than flush —
388            // the caller's final flush persists the terminal snapshot. Land the
389            // write in flight first, so it cannot rename an older snapshot over
390            // the caller's final one.
391            biased;
392            res = &mut fut => {
393                if let Some(pending) = flush.take() {
394                    let flushed = pending.await;
395                    res?;
396                    return flushed;
397                }
398                return res;
399            }
400            flushed = await_flush(&mut flush), if flush.is_some() => {
401                flush = None;
402                flushed?;
403            }
404            _ = tick.tick() => {
405                if flush.is_none() {
406                    flush = Some(store.flush_present_record());
407                }
408            }
409        }
410    }
411}
412
413/// Await the record write in `flush`. Only polled while one is in flight.
414async fn await_flush(flush: &mut Option<SourceFuture<'_, ()>>) -> anyhow::Result<()> {
415    match flush {
416        Some(pending) => pending.await,
417        None => std::future::pending().await,
418    }
419}
420
421/// A multi-source unit's live progress across its gaps, which [`fill_gap`]
422/// writes as each verified leaf lands, before the store's checkpoint makes it
423/// durable.
424#[derive(Debug, Default)]
425pub(crate) struct UnitProgress {
426    /// The unit's verified bytes. The lane watchdog judges it, and a steal
427    /// reads the unit's rate off it.
428    pub(crate) verified: std::sync::atomic::AtomicU64,
429    /// The end of the verified prefix the unit's legs have reached. A steal
430    /// splits past it, so it never hands a stealer bytes this unit already
431    /// received.
432    pub(crate) frontier: std::sync::atomic::AtomicU64,
433    /// When the unit verified its first byte: the start of its rate clock,
434    /// so a leg's open and a cold first byte stay out of its rate.
435    pub(crate) first_byte: std::sync::OnceLock<tokio::time::Instant>,
436}
437
438/// Counters that persist ACROSS the request's gaps (the reseed budget is
439/// per-fetch, not per-gap), plus the last upstream quote used to price the next
440/// voucher.
441#[derive(Debug, Clone, Copy)]
442pub(crate) struct DriveCounters {
443    /// Desync-reseed retries spent so far — bounded by [`MAX_RESUME_ATTEMPTS`].
444    resume_attempts: u32,
445    /// `ceil(interval_bytes * rate_per_mb / MiB)` from the most recent successful
446    /// open. `ZERO` before the first open, which keeps the pacer's affordability
447    /// gate open so the first draw always proceeds (an exhaustion can only follow
448    /// an open).
449    next_voucher_cost: U256,
450    /// The paid content frontier of the gap [`fill_gap`] ran last: each
451    /// pass's, then the one read after a leg that ended the gap with an error.
452    /// A caller whose gap failed re-notes the spans the lane owes from there.
453    /// `None` before the first pass.
454    pub(crate) gap_paid_frontier: Option<u64>,
455}
456
457impl DriveCounters {
458    /// Fresh per-fetch (single-source) or per-worker (multi-source) counters:
459    /// no reseeds spent, and no priced voucher yet.
460    pub(crate) const fn new() -> Self {
461        Self {
462            resume_attempts: 0,
463            next_voucher_cost: U256::ZERO,
464            gap_paid_frontier: None,
465        }
466    }
467}
468
469/// Say at `info!` that a voucher rejection on the leg at `offset` healed the
470/// lane ledger and the leg reopens, so `-v` shows why a retry happened.
471fn log_healed_retry(
472    hash: [u8; 32],
473    offset: u64,
474    attempt: u32,
475    err: &anyhow::Error,
476    healed: Option<crate::Healed>,
477) {
478    tracing::info!(
479        hash = %blake3::Hash::from_bytes(hash).to_hex(),
480        offset,
481        attempt,
482        max_attempts = MAX_RESUME_ATTEMPTS,
483        reason = ?err
484            .downcast_ref::<UpstreamVoucherRejected>()
485            .map(|rejected| rejected.reason),
486        ?healed,
487        "voucher rejection healed the lane ledger; reopening the leg"
488    );
489}
490
491/// True when `err` is a lane-watermark voucher rejection that a heal past the
492/// resume budget scopes to its source: `UnderFold`, `AmountRegression`,
493/// `Underpaid` or `BytesRegression`. A heal reseeds on any reason whose bundle
494/// is ahead of the ledger, so the reason decides the scope, not the heal. A
495/// `BytesRegression` with no bundle never heals, so it stays bare and ends the
496/// command (ADR 005). A `SpendingCapExhausted` is the pool's, one deposit
497/// behind every lane, so the exhaustion check judges it instead.
498fn scopes_to_source(err: &anyhow::Error) -> bool {
499    err.downcast_ref::<UpstreamVoucherRejected>()
500        .is_some_and(|r| {
501            matches!(
502                r.reason,
503                VoucherRejectReason::UnderFold
504                    | VoucherRejectReason::AmountRegression
505                    | VoucherRejectReason::Underpaid
506                    | VoucherRejectReason::BytesRegression
507            )
508        })
509}
510
511/// Price the next voucher from an upstream header, the exact formula
512/// [`PoolLedger`]'s own `next_voucher` uses.
513fn voucher_cost(header: &UpstreamPullHeader) -> U256 {
514    U256::from(header.interval_bytes)
515        .saturating_mul(U256::from(header.rate_per_mb))
516        .div_ceil(U256::from(decdn_protocol::MB_BYTES))
517}
518
519/// Fold a [`ChunkRanges`] (1 KiB `ChunkNum` units) into the contiguous byte
520/// ranges it covers, in ascending order, clamping the final boundary to
521/// `total_bytes` (the ragged last group). Each entry is `(start, len)` with
522/// `len > 0`.
523pub(crate) fn contiguous_byte_ranges(ranges: &ChunkRanges, total_bytes: u64) -> Vec<(u64, u64)> {
524    let boundaries = ranges.boundaries();
525    let mut out = Vec::new();
526    let mut it = boundaries.iter();
527    while let (Some(a), Some(b)) = (it.next(), it.next()) {
528        let start = a.0.saturating_mul(CHUNK_BYTES).min(total_bytes);
529        let end = b.0.saturating_mul(CHUNK_BYTES).min(total_bytes);
530        if end > start {
531            out.push((start, end - start));
532        }
533    }
534    out
535}
536
537/// Total content bytes a [`ChunkRanges`] covers (clamped to `total_bytes`).
538pub(crate) fn ranges_content_len(ranges: &ChunkRanges, total_bytes: u64) -> u64 {
539    contiguous_byte_ranges(ranges, total_bytes)
540        .iter()
541        .map(|(_, len)| *len)
542        .fold(0u64, u64::saturating_add)
543}
544
545/// The end of the present bytes contiguous from `gap_start` in a gap that ends
546/// at `gap_end`, given the gap's `missing` chunks: the start of the first
547/// missing range that overlaps the gap, clamped to `[gap_start, gap_end]`, or
548/// `gap_end` when nothing in the gap is missing. Present bytes past a hole do
549/// not move it.
550pub(crate) fn contiguous_frontier(
551    missing: &ChunkRanges,
552    total_bytes: u64,
553    gap_start: u64,
554    gap_end: u64,
555) -> u64 {
556    contiguous_byte_ranges(missing, total_bytes)
557        .into_iter()
558        .find(|&(start, len)| start.saturating_add(len) > gap_start && start < gap_end)
559        .map_or(gap_end, |(start, _)| start.max(gap_start).min(gap_end))
560}
561
562/// The chunks of `[start, start + len)` the store misses, clipped to its
563/// bound, and that bound. A range at or past the bound misses nothing. A leg
564/// can prove a smaller size between the bound read and the query, so a query
565/// the store refuses as out of bounds reads the smaller bound again and clips
566/// to it. A proven size is final, so the retry ends.
567///
568/// # Errors
569///
570/// Any other store error, marked [`crate::LocalPullFault`]: a store that cannot
571/// answer is this process's fault, never a source's (#2213).
572pub(crate) async fn missing_below_bound<St>(
573    store: &St,
574    start: u64,
575    len: u64,
576) -> anyhow::Result<(ChunkRanges, u64)>
577where
578    St: RangedStore + ?Sized,
579{
580    loop {
581        let bound = store.total_bytes();
582        let end = start.saturating_add(len).min(bound);
583        if start >= end {
584            return Ok((ChunkRanges::empty(), bound));
585        }
586        match store.missing_ranges(start, end - start).await {
587            Ok(missing) => return Ok((missing, bound)),
588            Err(decdn_bao_range::RangedStoreError::Alignment(_)) if store.total_bytes() < bound => {
589                // A leg proved a smaller size after the bound read: read it
590                // again on the next pass.
591            }
592            Err(e) => return Err(anyhow::Error::new(e).context(crate::LocalPullFault)),
593        }
594    }
595}
596
597/// Satisfy request `R = [offset, offset + len)` of blob `hash` by filling only its
598/// gaps, paying the minimum. `len == 0` means "to the end of the blob". Held
599/// ranges are read locally — never pulled, never paid.
600///
601/// The store is driven one write operation at a time (its documented concurrency
602/// contract): `drive` never issues overlapping `ingest_stream`/`finalize` calls.
603///
604/// `pool` is `None` for a single-source fetch — the one lane is the whole pool.
605/// A caller that drives several per-provider lanes over one shared deposit (the
606/// node's ranged assembly) passes a [`SharedPool`] so every lane's solvency gate
607/// subtracts the aggregate spend (#1506).
608///
609/// # What it does
610///
611/// 1. Compute `store.missing_ranges(offset, len)` and split it into the ordered
612///    contiguous gaps. If none are missing, skip straight to the completion check.
613/// 2. For each gap, run a per-gap resume/pay loop: assemble a [`PaceState`] from
614///    the ledger/ctx/store/header and let the [`Pacer`] choose `Draw` /
615///    `Wait` / `Done` / `Refuse`. A `Draw` opens the gap's [`AlignedRange`] through the
616///    [`BlobSource`], streams it into the store (which checkpoints durably), and
617///    finishes the pull. A mid-gap fault re-enters with the checkpointed prefix
618///    already held, so the re-open covers only the un-checkpointed tail.
619/// 3. When the request's gaps are all filled AND the whole blob is present,
620///    [`finalize`](decdn_bao_range::RangedStore::finalize) it (promoting `.partial` to its
621///    final path). For a partial `R` that does not complete the blob, `drive`
622///    returns `Ok(())` and leaves finalization to a later whole-blob fetch — the
623///    store cannot promote a blob that is still missing bytes outside `R`.
624///
625/// # Errors
626///
627/// A [`PaceDecision::Refuse`] ([`PoolExhausted`]), a terminal source/store
628/// fault that is not a healable desync (a funding refusal or rejection among
629/// them), a clean leg that moved neither frontier ([`LegNoProgress`]), or a
630/// `finalize` failure.
631#[allow(clippy::too_many_arguments)]
632#[doc(hidden)]
633pub async fn drive<St, S, P>(
634    store: &St,
635    source: &S,
636    pacer: &P,
637    ctx: &Arc<Mutex<PoolContext>>,
638    ledger: &Arc<PoolLedger>,
639    hash: [u8; 32],
640    offset: u64,
641    len: u64,
642    on_progress: Option<&(dyn Fn(u64, u64) + Send + Sync + '_)>,
643    pacing_wait: Option<&dyn PacingWait>,
644    downstream: Option<&(dyn Fn() -> DownstreamFrontier + Send + Sync)>,
645    pool: Option<&SharedPool<'_>>,
646) -> anyhow::Result<()>
647where
648    St: IngestStore,
649    S: BlobSource,
650    P: Pacer,
651{
652    let total_bytes = store.total_bytes();
653    // A store sized from a `total_bytes == 0` claim has no gap to fill and is
654    // complete as created, so nothing downstream ever verifies the empty stream
655    // against the root. Prove it here, before the empty store can be finalized
656    // (see [`reject_empty_claim_for_nonempty_root`]).
657    reject_empty_claim_for_nonempty_root(total_bytes, hash)?;
658
659    // Surface the already-present resume base on the progress bar immediately —
660    // before the pre-fetch window (pool open, first chunk). `fill_gap`'s
661    // per-gap reporter fires only from inside `ingest_stream` once streaming
662    // begins (as `base_present + received`), so without this a resumed blob's bar
663    // sits at `0` until the first byte arrives, then jumps to the resume point.
664    // This is exactly the value that reporter emits at `received == 0`: content
665    // bytes, matching the reporter's own unit and whole-blob `total_bytes`
666    // denominator. A no-op on the node's serve legs, which pass `on_progress =
667    // None`.
668    if let Some(cb) = on_progress {
669        let base_present = ranges_content_len(
670            &store.present_ranges().await.map_err(store_query_fault)?,
671            total_bytes,
672        );
673        cb(base_present, total_bytes);
674    }
675
676    let missing = store
677        .missing_ranges(offset, len)
678        .await
679        .map_err(store_query_fault)?;
680    // The spans this lane delivered and did not pay for when a funding refusal
681    // ended its last drive join the gaps, even outside the request: the lane
682    // owes them, so the drive opens at their paid frontier and bills them again
683    // although the store holds the bytes.
684    let owed = OwedSpans::take(ledger, hash);
685    let gaps = crate::ledger::merge_spans(
686        contiguous_byte_ranges(&missing, total_bytes)
687            .into_iter()
688            .chain(owed.spans.iter().copied())
689            .map(|(start, len)| (start, start.saturating_add(len)))
690            .collect(),
691    );
692
693    // Fill every gap while a single periodic tick flushes the `.ranges` present
694    // record (the single-writer flush point). The ranged store's ingest
695    // `checkpoint` never persists the record, so without this interval flush
696    // a crash mid-fetch would leave `.ranges` at pre-session state and re-download
697    // (and re-pay for) the whole in-flight range on resume; the interval bounds
698    // that loss to one `PRESENT_RECORD_FLUSH_INTERVAL`. This loop is the
699    // single-source path's sole periodic flush owner — `fill_gap` never flushes.
700    let owed = &owed;
701    let outcome = drive_with_interval_flush(store, PRESENT_RECORD_FLUSH_INTERVAL, async move {
702        let mut counters = DriveCounters::new();
703        for (gap_start, gap_len) in gaps {
704            owed.unbilled_from(gap_start);
705            fill_gap(
706                store,
707                source,
708                pacer,
709                ctx,
710                ledger,
711                hash,
712                gap_start,
713                gap_len,
714                &mut counters,
715                on_progress,
716                // `None`: `drive` is the single-source path, whose one lane reports
717                // its own present base directly — there is no cross-lane total to
718                // aggregate. Only the multi-source scheduler passes an aggregator.
719                None,
720                // No unit watchdog and no steal on the single-source path.
721                None,
722                pacing_wait,
723                downstream,
724                // `None` on the single-source path: this one lane IS the pool,
725                // so its own ledger already holds the spend. The node's
726                // ranged-drive loop injects a shared spend view across its
727                // per-provider lanes here instead (#1506); the client's
728                // multi-source scheduler calls `fill_gap` directly with the
729                // same view.
730                pool,
731                // No steal on the single-source path.
732                None,
733            )
734            .await
735            .inspect_err(|_| {
736                // The failed gap's owed part stays owed from its paid
737                // frontier on.
738                owed.unbilled_from(counters.gap_paid_frontier.unwrap_or(gap_start));
739            })?;
740        }
741        owed.billed();
742        Ok(())
743    })
744    .await;
745
746    // Final flush of whatever landed — on the FAILURE path too. The bytes a
747    // failed drive did deliver are paid for, and the interval owner's last tick
748    // can be up to one interval stale, so skipping this on `Err` discards
749    // already-bought resume progress the next invocation would have to re-pay
750    // for. The drive's own error is the more informative one, so it wins when
751    // both fail; a flush failure alone still surfaces.
752    let flushed = store.flush_present_record().await;
753    outcome?;
754    flushed?;
755
756    // Promote only when the WHOLE blob is present — `finalize` verifies and
757    // renames the whole `.partial`, which it cannot do while bytes outside `R`
758    // are still missing. A whole-blob `R` reaches this complete; a partial `R`
759    // leaves the `.partial` in place for a later fetch to finish.
760    if store.is_complete().await.map_err(store_query_fault)? {
761        store.finalize().await?;
762    }
763    Ok(())
764}
765
766/// The aligned range one paid leg opens: the unpaid tail `[resume_start,
767/// gap_end)` of a gap, clamped to the `up_to_bytes` the pacer authorized and to
768/// `max_len`, the most content the cap on held bytes lets the leg span
769/// ([`held_room`]; `u64::MAX` is no cap). A caller passes a nonzero `max_len`,
770/// so the length never collapses to the `0` ("to end") sentinel. Alignment
771/// widens the leg to whole chunk groups, so a leg can land up to one chunk
772/// group past the cap.
773///
774/// The one computation both [`fill_gap`] and [`first_leg`] use, so a caller that
775/// opens the first leg ahead of the drive opens exactly the range the drive asks
776/// for.
777pub(crate) fn leg_range(
778    resume_start: u64,
779    gap_end: u64,
780    up_to_bytes: u64,
781    max_len: u64,
782    total_bytes: u64,
783) -> anyhow::Result<AlignedRange> {
784    let draw_len = gap_end
785        .saturating_sub(resume_start)
786        .min(up_to_bytes)
787        .min(max_len);
788    Ok(align_range(resume_start, draw_len, total_bytes)?)
789}
790
791/// The new bytes the cap on held bytes still lets the store take for the
792/// blob: `max_blob_size_bytes` less the content bytes the store `held`. `None`
793/// when there is no cap (`0`). The cap bounds how many bytes of one blob a
794/// store holds, never which offsets it reads, so a range anywhere in a blob
795/// larger than the cap lands while it fits.
796fn held_room(max_blob_size_bytes: u64, held: u64) -> Option<u64> {
797    (max_blob_size_bytes > 0).then(|| max_blob_size_bytes.saturating_sub(held))
798}
799
800/// A store query's error, marked [`crate::LocalPullFault`] when the store itself
801/// failed ([`RangedStoreError::Backend`]): no source caused it, and every source
802/// would meet it. An alignment error is about the requested range, not the store,
803/// so it stays unmarked.
804fn store_query_fault(err: RangedStoreError) -> anyhow::Error {
805    let store_failed = matches!(err, RangedStoreError::Backend(_));
806    let err = anyhow::Error::new(err);
807    if store_failed {
808        err.context(crate::LocalPullFault)
809    } else {
810        err
811    }
812}
813
814/// The missing gaps of every range in `ranges`, merged and in ascending order.
815/// Each range is `(offset, len)`, and `len == 0` means "to the end of the blob".
816/// Overlapping or adjacent ranges merge, so the gaps are disjoint.
817async fn range_set_gaps<St: RangedStore + ?Sized>(
818    store: &St,
819    ranges: &[(u64, u64)],
820) -> anyhow::Result<Vec<(u64, u64)>> {
821    let mut missing = ChunkRanges::empty();
822    for &(offset, len) in ranges {
823        missing |= store
824            .missing_ranges(offset, len)
825            .await
826            .map_err(store_query_fault)?;
827    }
828    Ok(contiguous_byte_ranges(&missing, store.total_bytes()))
829}
830
831/// The aligned range a drive of `ranges` over `store` opens first, or `None`
832/// when every byte of them is already present.
833///
834/// A caller that must open a pull before the drive starts (to read the signed
835/// `total_bytes`, or to learn whether the peer will serve) opens exactly this
836/// range and hands the live pull to the drive through a [`crate::PrimedSource`],
837/// so the first open is also the drive's first leg (#2063). It is the range
838/// [`drive`] opens for its first gap when the pacer's
839/// first decision is `Draw { up_to_bytes }`: a fresh drive's paid frontier
840/// starts at the gap start, so the leg is the gap cut to `up_to_bytes`. A
841/// [`crate::BudgetPacer`] draws the whole unpaid gap, so pass `u64::MAX` for it.
842/// `max_blob_size_bytes` is the source's cap on the bytes held for one blob
843/// ([`BlobSource::max_blob_size_bytes`]): the leg spans at most the room the
844/// store's held bytes leave under it, and a store with no room left opens no
845/// leg (`None`), as the drive ends without one.
846///
847/// # Errors
848///
849/// A store query failure, or a range that does not align against the blob.
850#[doc(hidden)]
851pub async fn first_leg<St: RangedStore + ?Sized>(
852    store: &St,
853    ranges: &[(u64, u64)],
854    up_to_bytes: u64,
855    max_blob_size_bytes: u64,
856) -> anyhow::Result<Option<AlignedRange>> {
857    let gaps = range_set_gaps(store, ranges).await?;
858    let Some(&(start, len)) = gaps.first() else {
859        return Ok(None);
860    };
861    let max_len = if max_blob_size_bytes > 0 {
862        let present = store.present_ranges().await.map_err(store_query_fault)?;
863        let room =
864            max_blob_size_bytes.saturating_sub(ranges_content_len(&present, store.total_bytes()));
865        if room == 0 {
866            return Ok(None);
867        }
868        room
869    } else {
870        u64::MAX
871    };
872    leg_range(
873        start,
874        start.saturating_add(len),
875        up_to_bytes,
876        max_len,
877        store.total_bytes(),
878    )
879    .map(Some)
880}
881
882/// Warn when the store holds bytes after a hole in the gap
883/// `[gap_start, gap_end)`, once per distinct hole start. `hole` is the gap's
884/// contiguous delivered frontier and `missing_bytes` the gap's missing bytes, so
885/// the span past the hole less those is what the store holds after it. The next
886/// leg opens at the hole and pays for those bytes again. `warned_hole` holds the
887/// start of the last hole warned about.
888fn warn_on_new_hole(
889    warned_hole: &mut Option<u64>,
890    hash: [u8; 32],
891    gap_start: u64,
892    hole: u64,
893    gap_end: u64,
894    missing_bytes: u64,
895) {
896    let present_after_hole = gap_end.saturating_sub(missing_bytes).saturating_sub(hole);
897    if present_after_hole == 0 || *warned_hole == Some(hole) {
898        return;
899    }
900    *warned_hole = Some(hole);
901    tracing::warn!(
902        hash = %blake3::Hash::from_bytes(hash).to_hex(),
903        gap_start,
904        hole,
905        gap_end,
906        present_after_hole,
907        "the store holds bytes after a hole in the gap; the next leg opens at the hole and \
908         pays for them again"
909    );
910}
911
912/// The paid and delivered frontiers of the gap `[gap_start, asked_end)`, read
913/// after its last leg ended: the leg's paid frontier, clamped to the store's
914/// contiguous delivered frontier, and that delivered frontier. `None` when the
915/// store cannot answer.
916async fn ended_frontiers<St: RangedStore + ?Sized>(
917    store: &St,
918    ledger: &PoolLedger,
919    gap_start: u64,
920    asked_end: u64,
921    leg_anchor: Option<(u64, U256)>,
922) -> Option<(u64, u64)> {
923    let (still_missing, total_bytes) =
924        missing_below_bound(store, gap_start, asked_end.saturating_sub(gap_start))
925            .await
926            .ok()?;
927    let gap_end = asked_end.min(total_bytes);
928    let delivered = contiguous_frontier(&still_missing, total_bytes, gap_start, gap_end);
929    let (leg_start, baseline) = leg_anchor.unwrap_or((gap_start, ledger.committed().bytes));
930    let paid_wire =
931        u64::try_from(ledger.committed().bytes.saturating_sub(baseline)).unwrap_or(u64::MAX);
932    let paid = crate::sink::content_paid_frontier(leg_start, total_bytes, paid_wire)
933        .min(gap_end)
934        .min(delivered);
935    Some((paid, delivered))
936}
937
938/// The per-gap resume/pay loop. Fills the contiguous content span
939/// `[gap_start, gap_start + gap_len)` — a single gap of `missing_ranges` — driving
940/// the [`Pacer`] until it is fully present. `counters` persist across gaps.
941///
942/// Each pass reads the store's current bound ([`RangedStore::total_bytes`]) and
943/// clips the gap to it, so a gap past a size a leg has just proved ends at
944/// that size. Progress reports `(position, bound)`.
945#[allow(clippy::too_many_arguments)]
946// One sequential decide -> act -> classify loop. The `PaceDecision` arms and the
947// fault classification each justify a money-relevant decision inline (which
948// watermark heals a desync, why a stall is terminal); splitting them out would
949// separate those from the loop state they act on.
950#[allow(clippy::too_many_lines)]
951pub(crate) async fn fill_gap<St, S, P>(
952    store: &St,
953    source: &S,
954    pacer: &P,
955    ctx: &Arc<Mutex<PoolContext>>,
956    ledger: &Arc<PoolLedger>,
957    hash: [u8; 32],
958    gap_start: u64,
959    gap_len: u64,
960    counters: &mut DriveCounters,
961    on_progress: Option<&(dyn Fn(u64, u64) + Send + Sync + '_)>,
962    // Multi-source only: the shared whole-blob delivered-byte counter every lane
963    // folds its own leg deltas into, so the bar reads ONE monotonic position
964    // across interleaved lanes rather than each lane's divergent local
965    // `base_present + received`. `None` on the single-source path, which reports
966    // its own present base directly (there is only ever one lane, so that value is
967    // already the whole-blob position).
968    progress_agg: Option<&std::sync::atomic::AtomicU64>,
969    // Multi-source only: the unit's live progress, which the unit watchdog
970    // and a steal read ([`UnitProgress`]).
971    unit: Option<&UnitProgress>,
972    pacing_wait: Option<&dyn PacingWait>,
973    downstream: Option<&(dyn Fn() -> DownstreamFrontier + Send + Sync)>,
974    pool: Option<&SharedPool<'_>>,
975    // Multi-source only: an end a steal lowers to its split while the gap
976    // runs. The gap ends there, and a leg in flight stops there on its open
977    // stream ([`BlobSource::stop`]) rather than reading on to its range's end.
978    stop_at: Option<&std::sync::atomic::AtomicU64>,
979) -> anyhow::Result<()>
980where
981    St: IngestStore,
982    S: BlobSource,
983    P: Pacer,
984{
985    // Solvency basis for the deposit gate. On the single-source path (`None`) it
986    // is THIS lane's own committed amount — `deposit - own_committed`, unchanged.
987    // On the multi-source path a shared reader sums EVERY lane's committed amount,
988    // so each worker gates on `pool_deposit - aggregate_committed` — the true
989    // SHARED remaining toward the reserved floor, so concurrent lanes drawing on
990    // one pool cannot each independently believe the whole deposit is theirs (ADR
991    // 039 § Payment model: one deposit backs the whole set). It replaces ONLY the
992    // amount subtracted for the deposit gate — the per-leg paid-frontier math below
993    // still reads THIS lane's own `committed.bytes`.
994    let spent = move |own_amount: U256| pool.map_or(own_amount, |p| (p.spent)());
995    counters.gap_paid_frontier = None;
996
997    // Paid-frontier anchor (PER-LEG, not per-gap). A "leg" is one contiguous
998    // delivery from one successful open. `leg_anchor` records `(content offset the
999    // leg opened at, lane-cumulative committed WIRE bytes at that moment)`. It
1000    // is re-anchored on every successful open (to the previous paid frontier) and
1001    // on a reseed (to the healed delivered frontier), and PERSISTS across passes
1002    // so a resumed leg prices the paid frontier against the previous leg's own
1003    // spend. Both the completion signal (`paid_cleared`) and the Draw resume
1004    // start are derived from it; see the per-pass computation at the loop top.
1005    //
1006    // Why the PAID frontier and not the store's DELIVERED frontier: `ingest_stream`
1007    // checkpoints delivered+verified bytes payment-agnostically (ADR 003's credit
1008    // window lets the node stream up to a full interval before the voucher that
1009    // pays for it is due), so a leg can leave the present-range
1010    // frontier AHEAD of the last PAID byte — and the whole blob can be delivered in
1011    // one leg while only part is paid. Gating on delivery would `Done` before the
1012    // tail is billed (under-pay). `content_paid_frontier` maps the wire an accepted
1013    // voucher covered on THIS leg back to the largest chunk-group content boundary
1014    // provably inside it, so completion tracks payment and the resumed open
1015    // re-delivers (idempotently) and re-bills the tail. Fresh / cross-invocation
1016    // resume: `paid_wire == 0`, frontier collapses to the leg start (the disk
1017    // frontier), nothing already-paid is re-pulled (the `cli_fetch_resume` property).
1018    let mut leg_anchor: Option<(u64, U256)> = None;
1019
1020    // The last leg that finished cleanly, with the gap's two frontiers as the pass
1021    // that opened it read them. The next pass must see one of them move (#2194).
1022    let mut clean_leg: Option<CleanLeg> = None;
1023
1024    let asked_end = gap_start.saturating_add(gap_len);
1025
1026    // The start of the last hole this gap warned about, so each distinct hole
1027    // logs once however many passes it takes to fill.
1028    let mut warned_hole: Option<u64> = None;
1029
1030    loop {
1031        // The store's DELIVERED frontier for this gap: the end of the present
1032        // bytes contiguous from `gap_start`, which is the start of the gap's
1033        // first missing range, or the gap's end when nothing is missing. Present
1034        // bytes past a hole do not count: the gap resumes at the hole. It caps
1035        // the paid frontier, so the store holds every byte below the completion
1036        // point and below each resume start. A store that cannot answer is this process's fault,
1037        // not the source's (#2213). The bound it is clipped to is the bound the
1038        // planner works to now: a leg that proves a smaller size shrinks it, and
1039        // the gap ends there.
1040        // A steal may have lowered the gap's end to its split since the last
1041        // pass.
1042        let asked_end = stop_at.map_or(asked_end, |end| asked_end.min(end.load(Ordering::Acquire)));
1043        let (still_missing, total_bytes) =
1044            missing_below_bound(store, gap_start, asked_end.saturating_sub(gap_start)).await?;
1045        let gap_end = asked_end.min(total_bytes);
1046        if gap_start >= gap_end {
1047            return Ok(());
1048        }
1049        let gap_len = gap_end - gap_start;
1050        let delivered_frontier =
1051            contiguous_frontier(&still_missing, total_bytes, gap_start, gap_end);
1052        warn_on_new_hole(
1053            &mut warned_hole,
1054            hash,
1055            gap_start,
1056            delivered_frontier,
1057            gap_end,
1058            ranges_content_len(&still_missing, total_bytes),
1059        );
1060
1061        // The cap on bytes held (#1895): the source's `max_blob_size_bytes` bounds
1062        // the content of this blob the store holds, BLAKE3-verified, never the
1063        // offsets a leg reads and never the peer's unverified signed `total_bytes`.
1064        // `room` is the new bytes the store may still take. The Draw arm cuts
1065        // each leg to it, so a range anywhere in a blob larger than the cap lands
1066        // while it fits. `None` = no cap (and own-origin, whose engine store
1067        // applies its own cap).
1068        let max_blob_size_bytes = source.max_blob_size_bytes();
1069        let held = if max_blob_size_bytes > 0 {
1070            let present = store
1071                .present_ranges()
1072                .await
1073                .map_err(|e| anyhow::Error::new(e).context(crate::LocalPullFault))?;
1074            ranges_content_len(&present, total_bytes)
1075        } else {
1076            0
1077        };
1078        let room = held_room(max_blob_size_bytes, held);
1079
1080        let committed = ledger.committed();
1081        let deposit = locked_deposit(ctx)?;
1082        let remaining_deposit = deposit.saturating_sub(spent(committed.amount));
1083
1084        // Anchor the leg on the first pass at `gap_start` with the current committed
1085        // baseline (fresh / cross-invocation: `paid_wire == 0`, so the frontier is
1086        // `gap_start` and nothing already-paid is re-pulled). `content_paid_frontier`
1087        // inverts the wire cost of ONE contiguous delivery from `leg_start`, so it
1088        // MUST be priced per-leg: `leg_anchor` re-anchors on every successful open to
1089        // the previous paid frontier, keeping the frontier monotonic and never summing
1090        // two legs' (proof-duplicating) wire encodings, which would map PAST the true
1091        // paid frontier and under-pay.
1092        let (leg_start, leg_baseline) = *leg_anchor.get_or_insert((gap_start, committed.bytes));
1093        let paid_wire_this_leg =
1094            u64::try_from(committed.bytes.saturating_sub(leg_baseline)).unwrap_or(u64::MAX);
1095        // The gap's PAID content frontier, clamped to the store's DELIVERED frontier:
1096        // a gap is done — and may resume — only at bytes that are BOTH paid AND
1097        // present, tracking the MIN of the two. `content_paid_frontier` prices ONE
1098        // leg's paid wire, but the `PoolLedger` is SHARED by every concurrent pull on
1099        // the lane (`BuyerLedgers`), so a concurrent pull's acked vouchers inflate
1100        // `committed.bytes` — and thus `paid_wire_this_leg` — past what THIS leg
1101        // delivered. Left unclamped, that overshoot makes `paid_cleared` report the
1102        // gap `Done` (or trips the `paid_frontier >= gap_end` spin-guard in the Draw
1103        // arm) before the bytes are in the store, so `drive` returns `Ok` with the
1104        // blob incomplete and never finalizes it: success for a blob that is not
1105        // there. Clamping to `delivered_frontier` is the same guard against that
1106        // overshoot every resumable pull needs. When the store keeps every byte a leg
1107        // delivers, delivery runs AHEAD of payment (ADR 003's credit window), so
1108        // `delivered_frontier >= paid_frontier` and the clamp is a no-op: resume
1109        // still starts at the true paid frontier and the delivered-but-unpaid tail is
1110        // still re-billed (no under-pay). The clamp bites in two cases. A
1111        // shared-ledger concurrent pull overshoots this leg's delivery. Or the store
1112        // leaves a hole before present bytes: the contiguous delivered frontier stops
1113        // at the hole, so the paid frontier stops there too and the next leg opens
1114        // at the hole.
1115        let paid_frontier =
1116            crate::sink::content_paid_frontier(leg_start, total_bytes, paid_wire_this_leg)
1117                .min(gap_end)
1118                .min(delivered_frontier);
1119        let paid_cleared = paid_frontier.saturating_sub(gap_start);
1120        counters.gap_paid_frontier = Some(paid_frontier);
1121
1122        // A clean leg's payment moves the paid frontier to the end of what it
1123        // delivered, and its bytes land in the store, so this pass sees one of the
1124        // two frontiers move. One that moved neither would re-open the same range
1125        // and pay for the same bytes to the same effect. End the gap.
1126        if let Some(stuck) = clean_leg.take().and_then(|leg| {
1127            leg.stalled(
1128                paid_frontier,
1129                delivered_frontier,
1130                paid_wire_this_leg,
1131                ledger.generation(),
1132            )
1133        }) {
1134            return Err(anyhow::Error::new(stuck));
1135        }
1136
1137        // No room left under the cap, every present byte below the delivered
1138        // frontier paid, and bytes still missing: the next leg could only add
1139        // bytes past the cap. End before it opens, so nothing past the cap is
1140        // pulled or paid for.
1141        if room == Some(0) && paid_frontier >= delivered_frontier && delivered_frontier < gap_end {
1142            return Err(anyhow::Error::new(crate::BlobTooLarge {
1143                reached: held.saturating_add(ranges_content_len(&still_missing, total_bytes)),
1144                ceiling: max_blob_size_bytes,
1145            }));
1146        }
1147
1148        let downstream_now = downstream.map_or(
1149            DownstreamFrontier {
1150                served_paid: paid_frontier,
1151                serve_demand: 0,
1152            },
1153            |f| f(),
1154        );
1155        let state = PaceState {
1156            cleared_bytes: paid_cleared,
1157            requested_bytes: gap_len,
1158            remaining_deposit,
1159            next_voucher_cost: counters.next_voucher_cost,
1160            // `pulled_frontier` is this leg's own admitted/present frontier —
1161            // correct on BOTH the client and node pull legs, since it is always
1162            // THIS leg's delivery progress, never the downstream client's. No
1163            // seam needed.
1164            pulled_frontier: delivered_frontier,
1165            gap_remaining: gap_end.saturating_sub(delivered_frontier),
1166            // `downstream.served_paid` is NOT this leg's own state — it is the
1167            // DOWNSTREAM client's paid frontier, which only the node's serve leg
1168            // advances (`FillSession::advance_served`). On the client path
1169            // (`downstream == None`) there is no downstream leg, so this
1170            // collapses to the inert local `paid_frontier`: harmless, because
1171            // `BudgetPacer` never reads `downstream`. The NODE pull leg
1172            // MUST pass `Some(reader)` here, reading the shared downstream
1173            // `served_paid` frontier, so its `WindowPacer` gates the pull against
1174            // the downstream client's payment — not against this leg's own
1175            // upstream paid frontier, which would be category-wrong (it would
1176            // make the window track the node's own credit-window lag instead of
1177            // the client it is serving).
1178            downstream: downstream_now,
1179        };
1180
1181        match pacer.decide(&state) {
1182            PaceDecision::Done => return Ok(()),
1183            decision @ (PaceDecision::Wait | PaceDecision::WaitForMinDraw) => {
1184                if let Some(hook) = pacing_wait {
1185                    let reason = if decision == PaceDecision::Wait {
1186                        WaitReason::WindowFull
1187                    } else {
1188                        WaitReason::MinDraw
1189                    };
1190                    // Hand the hook the frontiers THIS decision read, so it can
1191                    // register its wakeup then re-check for an advance that raced the
1192                    // decision — closing the lost-wakeup that wedged the window-paused
1193                    // pull under CI scheduling gaps (#1673).
1194                    hook.wait(downstream_now, reason).await;
1195                    continue;
1196                }
1197                anyhow::bail!(
1198                    "gap [{gap_start}, +{gap_len}) of blob paced to Wait but no \
1199                     PacingWait hook was supplied: this is unreachable on the client \
1200                     path (BudgetPacer never returns Wait) — a WindowPacer caller \
1201                     must pass a PacingWait"
1202                );
1203            }
1204            PaceDecision::Refuse => {
1205                // The lane keeps what it delivered and did not pay for, so
1206                // its next drive, if it runs again, bills it.
1207                ledger.note_unpaid(hash, paid_frontier, delivered_frontier);
1208                counters.gap_paid_frontier = Some(paid_frontier);
1209                return Err(anyhow::Error::new(PoolExhausted { gap_start, gap_len }));
1210            }
1211            // Honors `up_to_bytes` (#1608):
1212            // `BudgetPacer` returns the full gap remainder, so clamping to it is a
1213            // no-op there; a `WindowPacer` returns a tighter bound, and clamping
1214            // `draw_len`/`aligned` here is what actually enforces the window — the
1215            // open below must never request more than the pacer authorized.
1216            PaceDecision::Draw { up_to_bytes } => {
1217                // Draw the UNPAID tail `[paid_frontier, gap_end)`. This is the
1218                // still-missing part when payment tracks delivery, and additionally
1219                // the delivered-but-unpaid span `[paid_frontier, delivered_frontier)`
1220                // a previous leg left: `ingest_stream` re-writes the already-present
1221                // bytes idempotently and the pull re-bills them, so the credit-window
1222                // tail the store checkpointed ahead of payment is finally paid. A
1223                // hole the store left before present bytes caps the paid frontier
1224                // at the hole, so the leg opens there and re-delivers the present
1225                // bytes after it the same idempotent way. The pull pays for those
1226                // present bytes again: a bounded over-pay, never an under-pay.
1227                if paid_frontier >= gap_end {
1228                    // Paid AND delivered to the gap end (`paid_frontier` is clamped to
1229                    // the delivered frontier above) — the pacer should have returned
1230                    // `Done`; guard against a spin.
1231                    return Ok(());
1232                }
1233                let resume_start = paid_frontier;
1234                // The present bytes `[resume_start, delivered_frontier)` a leg
1235                // re-delivers to bill them add nothing to the bytes held, so the
1236                // cap's room starts past them.
1237                let max_len = room.map_or(u64::MAX, |room| {
1238                    delivered_frontier
1239                        .saturating_sub(resume_start)
1240                        .saturating_add(room)
1241                });
1242                let aligned = leg_range(resume_start, gap_end, up_to_bytes, max_len, total_bytes)?;
1243
1244                // Whole-blob content already present, so `ingest_stream`'s
1245                // per-range progress can be offset into overall progress: the bar
1246                // reports `base + received` against `total_bytes`.
1247                let base_present = ranges_content_len(
1248                    &(store
1249                        .present_ranges()
1250                        .await
1251                        .map_err(|e| anyhow::Error::new(e).context(crate::LocalPullFault))?),
1252                    total_bytes,
1253                );
1254                // This leg's own previously-reported cumulative, so the multi-source
1255                // aggregator folds in DELTAS (`received` is monotonic per leg, and
1256                // resets to 0 on each new open — hence a fresh counter per leg).
1257                let leg_reported = std::sync::atomic::AtomicU64::new(0);
1258                // `received` counts from the leg's own start.
1259                let leg_start = aligned.fetch_start();
1260                let reporter = move |received: u64| {
1261                    let delta =
1262                        received.saturating_sub(leg_reported.swap(received, Ordering::Relaxed));
1263                    if let Some(unit) = unit {
1264                        if delta > 0 {
1265                            unit.first_byte.get_or_init(tokio::time::Instant::now);
1266                        }
1267                        unit.verified.fetch_add(delta, Ordering::Relaxed);
1268                        // `SeqCst`, against the steal that lowers `stop_at`
1269                        // and then reads this frontier (`Work::steal`).
1270                        unit.frontier
1271                            .fetch_max(leg_start.saturating_add(received), Ordering::SeqCst);
1272                    }
1273                    let Some(cb) = on_progress else { return };
1274                    // Only bytes past the delivered frontier this pass read are
1275                    // new. A leg that opens at the paid frontier re-delivers the
1276                    // present bytes below it to bill them, and those add nothing
1277                    // to the position, so the funding recovery gate never counts
1278                    // them as verified progress.
1279                    let end = leg_start.saturating_add(received);
1280                    let prev_end = leg_start.saturating_add(received.saturating_sub(delta));
1281                    let fresh = end.saturating_sub(prev_end.max(delivered_frontier));
1282                    let position = match progress_agg {
1283                        // Multi-source: fold this leg's new bytes into the shared
1284                        // whole-blob total as deltas, so the bar reads one
1285                        // non-decreasing position across concurrent lanes. The
1286                        // readout clamps to the blob size.
1287                        Some(delivered) => delivered
1288                            .fetch_add(fresh, Ordering::Relaxed)
1289                            .saturating_add(fresh)
1290                            .min(total_bytes),
1291                        // Single-source: this one lane's present base plus the
1292                        // leg's new bytes is the whole-blob position.
1293                        None => base_present
1294                            .saturating_add(end.saturating_sub(delivered_frontier.max(leg_start)))
1295                            .min(total_bytes),
1296                    };
1297                    cb(position, total_bytes);
1298                };
1299
1300                // One paid leg: open -> stream into the store -> drain the pull.
1301                let generation_at_open = ledger.generation();
1302                let leg: anyhow::Result<()> = match source.open(hash, aligned.clone()).await {
1303                    Ok((header, reader)) => {
1304                        counters.next_voucher_cost = voucher_cost(&header);
1305                        if let Some(quotes) = pool.and_then(|p| p.quotes) {
1306                            quotes.observe(header.rate_per_mb, header.interval_bytes);
1307                        }
1308                        // A new leg has opened: re-anchor the paid-frontier baseline
1309                        // to THIS open's start and the committed watermark BEFORE it
1310                        // streams, so the next pass prices its paid frontier
1311                        // against this leg's own wire spend alone.
1312                        leg_anchor = Some((resume_start, ledger.committed().bytes));
1313                        // The store is keyed by offset: the leg verifies under
1314                        // the size its own sender signs, whatever the bound.
1315                        match store
1316                            .ingest_stream(
1317                                &aligned,
1318                                reader,
1319                                Some(&reporter),
1320                                header.total_bytes,
1321                                stop_at,
1322                            )
1323                            .await
1324                        {
1325                            Ok((reader, IngestEnd::Drained)) => {
1326                                // Drain to stream end and recover the acked voucher
1327                                // watermark. It lives in the ledger the caller owns
1328                                // (durable persistence is the caller's job); finishing here
1329                                // enforces wire-byte completeness.
1330                                source.finish(reader).await.map(|_vp| ())
1331                            }
1332                            // A steal lowered the end to a split this leg reached:
1333                            // pay for the received bytes and close the stream. The
1334                            // paid and delivered frontiers now reach the split, the
1335                            // gap's end, so the next pass ends the gap without a
1336                            // new leg.
1337                            Ok((reader, IngestEnd::Stopped)) => {
1338                                source.stop(reader).await.map(|_vp| ())
1339                            }
1340                            Err(err) => Err(err),
1341                        }
1342                    }
1343                    Err(err) => Err(err),
1344                };
1345
1346                if let Err(err) = leg {
1347                    // The heal reads the shared context. The guard is held only
1348                    // across the synchronous read, never across the heal's `.await`.
1349                    let watermark = {
1350                        let guard = ctx
1351                            .lock()
1352                            .map_err(|_| anyhow::anyhow!("pool context lock poisoned"))?;
1353                        rejection_watermark(&err, &guard)
1354                    };
1355
1356                    // 1. Desync heal (driver-owned, NOT a PaceDecision): an
1357                    //    authenticated bundle that ADVANCES our committed
1358                    //    watermark means the node holds a voucher we lost, an
1359                    //    `Underpaid` bundle BEHIND it means we hold vouchers the
1360                    //    node never took, and a `BytesRegression` bundle behind
1361                    //    on amount but ahead on bytes means the lane resumed
1362                    //    below a voucher the node holds — heal the ledger and
1363                    //    retry. A heal past the resume budget still leaves the
1364                    //    ledger where the node holds it, but this source kept
1365                    //    rejecting after each heal: mark the rejection
1366                    //    `HealExhausted`, so the source cools rather than the
1367                    //    command ending. A rejection that no heal takes stays
1368                    //    bare and falls through to step 2. Only a lane-watermark
1369                    //    reason is marked (`scopes_to_source`): a spending-cap
1370                    //    rejection stays a funding rejection.
1371                    let healed = match watermark {
1372                        Some(watermark) => heal_watermark_desync(&err, watermark, ledger).await,
1373                        None => None,
1374                    };
1375                    let past_budget = counters.resume_attempts >= MAX_RESUME_ATTEMPTS;
1376                    if healed.is_some() && past_budget && scopes_to_source(&err) {
1377                        return Err(err.context(HealExhausted));
1378                    }
1379                    if healed.is_some() && !past_budget {
1380                        counters.resume_attempts = counters.resume_attempts.saturating_add(1);
1381                        log_healed_retry(
1382                            hash,
1383                            resume_start,
1384                            counters.resume_attempts,
1385                            &err,
1386                            healed,
1387                        );
1388                        // A reseed means the node HOLDS vouchers for content it
1389                        // already delivered that our record had lost — so the
1390                        // delivered frontier is paid. A rebase means the node took
1391                        // none of the vouchers above its watermark, so the span
1392                        // between is delivered and unpaid; the node already chose to
1393                        // stop there, and the healed ledger pays only for what it
1394                        // delivers next. Either way, re-anchor the leg there with the
1395                        // healed committed baseline, so the next pass prices this
1396                        // gap's paid frontier AT the delivered frontier (`paid_wire
1397                        // delta == 0`): no re-delivery of bytes already delivered, and the
1398                        // reseed's jump is not mistaken for this leg's own spend
1399                        // (which would map past the true frontier and under-pay).
1400                        // `delivered_frontier` is this pass's pre-ingest snapshot
1401                        // (loop top); if the reseed fault arrived after some bytes
1402                        // were checkpointed this pass it sits slightly behind the
1403                        // fresh frontier. That is the SAFE direction — at worst a
1404                        // bounded re-deliver/re-pay of the interim span (over-pay),
1405                        // never under-pay — and reseed refusals almost always
1406                        // surface at open, before any ingest, so the two coincide.
1407                        leg_anchor = Some((delivered_frontier, ledger.committed().bytes));
1408                        continue;
1409                    }
1410
1411                    // 2. Anything else (a stall, reset, hash mismatch, local I/O
1412                    //    fault, a funding refusal or rejection, a voucher
1413                    //    rejection no heal takes) ends the lane. The acquire
1414                    //    loop classifies it ([`crate::classify`]). A lane that
1415                    //    a funding refusal ends keeps the span it delivered and
1416                    //    did not pay for, so its next drive, if it runs again,
1417                    //    bills it.
1418                    if let Some((paid, delivered)) =
1419                        ended_frontiers(store, ledger, gap_start, asked_end, leg_anchor).await
1420                    {
1421                        counters.gap_paid_frontier = Some(paid);
1422                        if crate::classify(&err) == crate::Fault::Unaffordable {
1423                            ledger.note_unpaid(hash, paid, delivered);
1424                        }
1425                    }
1426                    return Err(err);
1427                }
1428
1429                // The leg landed: the next pass must see a frontier move.
1430                clean_leg = Some(CleanLeg {
1431                    offset: aligned.fetch_start(),
1432                    len: aligned.fetch_len(),
1433                    paid_frontier,
1434                    delivered_frontier,
1435                    generation: generation_at_open,
1436                });
1437            }
1438        }
1439    }
1440}
1441
1442#[cfg(test)]
1443#[allow(
1444    clippy::unwrap_used,
1445    clippy::expect_used,
1446    clippy::panic,
1447    clippy::indexing_slicing,
1448    clippy::cast_possible_truncation
1449)] // tests
1450mod tests;