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