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