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;