Skip to main content

decdn_client/
source.rs

1//! The sourcing and funding seams of a fetch (#1608): [`BlobSource`], a dumb
2//! producer of raw interleaved bao bytes for a contiguous range, and
3//! [`Funder`], the injected funding recovery seam.
4//!
5//! # Why "dumb"
6//!
7//! A [`BlobSource`] never decodes and never verifies. It opens a pull over the
8//! raw bao encoding of one [`decdn_bao_range::AlignedRange`] and yields the wire
9//! bytes on demand; the STORE's ingest decoder (rooted at the blob hash `H`)
10//! verifies each chunk group exactly once, as the bytes arrive. This keeps a
11//! source — `PeerSource` (paid `cdn/client/v1`) or a
12//! future `BackendSource` (origin re-encode) — free of bao logic, and matches the
13//! "admit verifies once" contract of [`decdn_bao_range::RangedStore`].
14//!
15//! The reader a source yields is a [`BaoRangeReader`]: an
16//! [`iroh_io::AsyncStreamReader`] that also preserves the pull's typed faults via
17//! [`crate::sink::StashedFault`], so a stalled or refusing peer is surfaced to the
18//! reputation layer rather than collapsed into an anonymous decode error.
19//!
20//! # The `Funder` seam
21//!
22//! The funding recovery step (ADR 003 § Funding recovery) goes through the
23//! injected [`Funder`] trait so the acquire loop and `PeerSource` stay
24//! chain-handle-agnostic: each deployment supplies its own implementation (the
25//! self-owned pool's `PoolFunder`, the node's `NodeFunder`) rather than the loop naming a
26//! contract instance directly.
27
28use std::sync::atomic::AtomicU64;
29use std::sync::{Arc, Mutex};
30use std::time::{Duration, SystemTime, UNIX_EPOCH};
31
32use alloy::dyn_abi::Eip712Domain;
33use alloy::primitives::{Address, U256};
34use anyhow::Context as _;
35use decdn_bao_range::AlignedRange;
36use decdn_incentive::PoolId;
37use iroh::{Endpoint, EndpointAddr};
38
39use crate::sink::{PullReader, StashedFault};
40use crate::{
41    Connections, PoolContext, PoolLedger, PullDeadlines, UpstreamPull, UpstreamPullHeader,
42    VoucherProgress,
43};
44
45/// A boxed, `Send` future returned by the async trait methods in this module —
46/// the same boxed-future async-trait shape [`decdn_bao_range::RangedStore`] and
47/// `cache::Origin` use, so a [`Funder`] can be held as `&dyn Funder`.
48pub type SourceFuture<'a, T> =
49    core::pin::Pin<Box<dyn core::future::Future<Output = anyhow::Result<T>> + Send + 'a>>;
50
51/// A boxed, `Send` stream returned by the streaming methods in this module,
52/// the stream sibling of [`SourceFuture`].
53#[doc(hidden)]
54pub type SourceStream<'a, T> = core::pin::Pin<Box<dyn futures_util::Stream<Item = T> + Send + 'a>>;
55
56/// A reader over the raw interleaved bao encoding of one range.
57///
58/// The store's ingest decoder pulls bytes on demand through
59/// [`iroh_io::AsyncStreamReader`]; a typed peer fault (stall, refusal, voucher
60/// rejection) is preserved through [`StashedFault`] so it beats the decoder's
61/// generic "bytes stopped arriving". Blanket-implemented, so any reader that is
62/// both is a `BaoRangeReader` with no extra code.
63#[doc(hidden)]
64pub trait BaoRangeReader: iroh_io::AsyncStreamReader + StashedFault + Send {}
65
66impl<T> BaoRangeReader for T where T: iroh_io::AsyncStreamReader + StashedFault + Send {}
67
68/// A source of raw, unverified interleaved bao bytes for contiguous ranges of one
69/// blob. Dumb: it produces bytes (and, for paid sources, pays) — it never decodes
70/// or verifies.
71///
72/// One `BlobSource` serves one gap-driven fetch. The driver calls [`open`] once
73/// per gap (a contiguous [`AlignedRange`] from
74/// [`missing_ranges`](decdn_bao_range::RangedStore::missing_ranges)), streams the
75/// reader into the store's ingest decoder, then calls [`finish`] to drain the
76/// pull to completion and recover the acked voucher watermark to persist.
77///
78/// [`open`]: BlobSource::open
79/// [`finish`]: BlobSource::finish
80#[doc(hidden)]
81pub trait BlobSource: Send + Sync {
82    /// The reader this source yields — raw bao bytes plus preserved typed faults.
83    type Reader: BaoRangeReader;
84
85    /// Open a pull over the raw bao encoding of `range` of the blob `hash`.
86    ///
87    /// Returns the upstream [`UpstreamPullHeader`] (the committed `total_bytes`
88    /// and the quoted `rate_per_mb` / `interval_bytes` the driver prices the next
89    /// voucher from) alongside the live [`Self::Reader`]. An unpaid source reports
90    /// a zero rate/interval; `total_bytes` is always authoritative.
91    ///
92    /// # Errors
93    ///
94    /// The handshake/response faults of the underlying pull (connect/transport, a
95    /// refused or zero-rate response, an over-ceiling rate or size, a resume
96    /// offset past the blob end) — the same set `open_progressive_pull` raises.
97    fn open(
98        &self,
99        hash: [u8; 32],
100        range: AlignedRange,
101    ) -> SourceFuture<'_, (UpstreamPullHeader, Self::Reader)>;
102
103    /// Consume a fully-decoded gap's reader: drain the pull to its stream end,
104    /// enforce wire-byte completeness, and return the acked voucher watermark for
105    /// the driver to persist. An unpaid source returns
106    /// [`VoucherProgress::default`].
107    ///
108    /// # Errors
109    ///
110    /// A short delivery (fewer wire bytes than promised before the stream end) or
111    /// a transport/protocol error while draining — the faults
112    /// [`crate::UpstreamPull::finish`] raises.
113    fn finish(&self, reader: Self::Reader) -> SourceFuture<'_, VoucherProgress>;
114
115    /// Consume a reader whose ingest stopped before the end of its range
116    /// ([`IngestEnd::Stopped`]): pay for every wire byte the pull received,
117    /// then close the pull's transport (its stream, or its one-shot
118    /// connection), and return the voucher watermark for the driver to
119    /// persist. A steal that takes the tail of the range stops the pull this
120    /// way at the split. An unpaid source returns
121    /// [`VoucherProgress::default`].
122    ///
123    /// # Errors
124    ///
125    /// A rejection the source already sent, or a closing payment that fails
126    /// on our side ([`crate::UpstreamPull::stop`]).
127    fn stop(&self, reader: Self::Reader) -> SourceFuture<'_, VoucherProgress>;
128
129    /// The buyer's cap on bytes held for one blob (#1895). The driver cuts each
130    /// leg to the room the store's BLAKE3-verified bytes leave under it, and ends
131    /// with [`crate::BlobTooLarge`] once a missing byte could only land past it.
132    /// The cap bounds bytes held, never the offsets read, and never reads the
133    /// peer's unverified signed `total_bytes`. `0` = unlimited — the default, and
134    /// correct for an own-origin source whose engine store already caps on stored
135    /// bytes.
136    fn max_blob_size_bytes(&self) -> u64 {
137        0
138    }
139}
140
141/// The future [`IngestStore::ingest_stream`] returns: the reader back, and how
142/// the ingest ended.
143#[doc(hidden)]
144pub type IngestFuture<'a, R> =
145    core::pin::Pin<Box<dyn core::future::Future<Output = anyhow::Result<(R, IngestEnd)>> + 'a>>;
146
147/// How an [`IngestStore::ingest_stream`] ended without a fault.
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149#[doc(hidden)]
150pub enum IngestEnd {
151    /// The decoder reached the end of the range: the source
152    /// [`finish`](BlobSource::finish)es the pull.
153    Drained,
154    /// The verified prefix reached the lowered `stop_at` before the end of the
155    /// range: the source [`stop`](BlobSource::stop)s the pull.
156    Stopped,
157}
158
159/// The store/sink capability the gap-driven [`crate::drive`] needs beyond
160/// [`decdn_bao_range::RangedStore`]'s queries: ingest one gap's raw bao. The
161/// client backend writes `.partial`; a node backend admits to the cache and
162/// tees to its downstream client. Kept a generic method (not
163/// `dyn`) so an impl can stream any [`BaoRangeReader`]; `drive` is already
164/// fully generic.
165///
166/// The returned future is intentionally NOT `Send`-bounded, unlike
167/// [`SourceFuture`]. [`iroh_io::AsyncStreamReader`]'s methods are
168/// return-position-impl-trait-in-trait with no `Send` bound on the trait
169/// itself, so a method generic over `R: BaoRangeReader` (as this one must be,
170/// to stream ANY reader an [`IngestStore`] impl is handed) can never prove its
171/// decode-loop future is `Send` for an arbitrary `R` — only a concrete,
172/// non-generic instantiation could. `drive`/`fill_gap` only ever `.await` this
173/// future in place (never spawn it across a task boundary), so dropping `Send`
174/// here is behavior-preserving.
175#[doc(hidden)]
176pub trait IngestStore: decdn_bao_range::RangedStore {
177    /// Decode-and-admit the raw bao bytes in `reader` as `range`'s content,
178    /// verifying against the store's rooted hash as it streams. Returns the
179    /// drained `reader` (its typed fault, if any, surfaces via
180    /// [`StashedFault`] on the caller's copy) so the
181    /// source can [`finish`](BlobSource::finish) the pull.
182    ///
183    /// `claimed_total` is the blob size the leg's sender signs in its header.
184    /// The sender serves `range` clamped to that size and encodes it under
185    /// that size's tree, so a store keyed by offset verifies the leg under
186    /// it.
187    ///
188    /// `stop_at` is an end the caller can lower while the leg streams. Once
189    /// the verified prefix reaches it, the ingest checkpoints that prefix and
190    /// returns [`IngestEnd::Stopped`] without reading further. After each
191    /// verified group it reports the prefix through `on_progress` first, then
192    /// reads `stop_at` with `SeqCst`. A caller that records the prefix with
193    /// `SeqCst`, lowers `stop_at`, and then reads the recorded prefix with
194    /// `SeqCst` knows where the leg stops: at the lowered end when the prefix
195    /// read is short of it, and within one group past that prefix otherwise. A steal lowers
196    /// it to the split, so the leg stops there on its open stream. A
197    /// chunk-group-aligned end cuts the pre-order bao encoding of `range`
198    /// between two items, so the stopped prefix verifies on its own.
199    fn ingest_stream<'a, R>(
200        &'a self,
201        range: &'a AlignedRange,
202        reader: R,
203        on_progress: Option<&'a (dyn Fn(u64) + Send + Sync)>,
204        claimed_total: u64,
205        stop_at: Option<&'a AtomicU64>,
206    ) -> IngestFuture<'a, R>
207    where
208        R: BaoRangeReader + 'a;
209
210    /// Persist the store's current in-memory present-range snapshot to its
211    /// durable record. The single-writer flush point: several
212    /// `ingest_stream` calls can run concurrently on one store (the
213    /// multi-source scheduler), so no checkpoint writes the record — callers
214    /// flush it explicitly instead. `drive` calls this once after its gap loop,
215    /// before `finalize`, so the single-source path keeps its resume durability
216    /// without per-checkpoint fsyncs.
217    ///
218    /// The snapshot is taken when this is called; the returned future writes
219    /// it. An implementation that touches the disk does so off the runtime
220    /// workers, because the paid pulls that share the runtime pay from their
221    /// decode loops.
222    ///
223    /// # Errors
224    ///
225    /// Any I/O failure persisting the record.
226    fn flush_present_record(&self) -> SourceFuture<'_, ()>;
227
228    /// The size a verified final chunk proved, if any leg has proved one. A
229    /// store keyed by a fixed size knows that size from the start, so the
230    /// default is [`total_bytes`](decdn_bao_range::RangedStore::total_bytes).
231    /// [`crate::acquire`] completes once a size is proven and every byte below
232    /// it is present.
233    fn proven(&self) -> Option<u64> {
234        Some(self.total_bytes())
235    }
236
237    /// Move the planner's bound ([`total_bytes`](decdn_bao_range::RangedStore::total_bytes))
238    /// to `bound`. [`crate::acquire`] grows the bound this way while no size is
239    /// proven. A store keyed by a fixed size keeps it, so the default does
240    /// nothing.
241    fn set_bound(&self, bound: u64) {
242        let _ = bound;
243    }
244}
245
246/// The current pool no longer accepts funds, and the funding recovery step
247/// opened a new pool and made it the current pool in the buyer's local pool
248/// store (ADR 003 § Funding recovery).
249///
250/// Lanes key on the pool, so no lane of the fetch can pay from the new pool.
251/// The caller runs the remaining work again against it, resuming the partial
252/// data it holds, so the second pass pays only for missing bytes. The closed
253/// pool keeps whatever balance it held until its owner reclaims it.
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub struct PoolReplaced {
256    /// The pool that refused the top-up (`Closing` or `Closed`). It may still
257    /// hold a balance to reclaim.
258    pub closed: PoolId,
259    /// The new pool, now the current pool in the local pool store.
260    pub opened: PoolId,
261}
262
263impl std::fmt::Display for PoolReplaced {
264    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265        write!(
266            f,
267            "pool {} no longer accepts funds, so new pool {} replaces it; the remaining work \
268             runs again against the new pool",
269            self.closed, self.opened
270        )
271    }
272}
273
274impl std::error::Error for PoolReplaced {}
275
276/// What one funding recovery step did ([`Funder::recover`]).
277#[derive(Debug, Clone, Copy, PartialEq, Eq)]
278#[non_exhaustive]
279pub enum Recovery {
280    /// The current pool's deposit is this total, in micro-USDC: the step
281    /// topped it up, or it already held its sized deposit and still accepts
282    /// funds, so the step settles at it while the nodes' views catch up.
283    ToppedUp(U256),
284    /// The current pool no longer accepts funds, and a new pool replaces it.
285    Replaced(PoolReplaced),
286    /// The buyer has no way to add funds: it is a delegated signer with no
287    /// funder to ask, its funding is turned off, or its top-up added nothing.
288    /// The fetch ends "funding needed".
289    Unavailable,
290}
291
292/// The injected funding recovery seam (ADR 003 § Funding recovery). Wraps the
293/// deployment's funding path (a self-owned pool's `PoolFunder` and the node's
294/// `NodeFunder`, each over its own chain handle), so the acquire loop and
295/// [`BlobSource`] never name a contract instance.
296///
297/// The acquire loop calls [`recover`](Funder::recover) only when its candidate
298/// set is exhausted with at least one source priced out, and only when the
299/// progress rule allows a step: the first step of a fetch always, a further
300/// step only after a newly verified byte. That rule, not the funder, bounds how
301/// often one fetch adds funds.
302pub trait Funder: Send + Sync {
303    /// Run one funding recovery step. `remaining` is the pool's spendable
304    /// deposit as the caller sees it (deposit less every lane's spend).
305    ///
306    /// A self-funded owner tops its current pool up toward its sized deposit.
307    /// A pool that is `Closing` or `Closed` refuses the top-up, and the owner
308    /// opens a new pool at its normal deposit and makes it current instead
309    /// ([`Recovery::Replaced`]). A pool that already holds its sized deposit and
310    /// still accepts funds needs no top-up: the step settles at its deposit
311    /// ([`Recovery::ToppedUp`]), so the caller asks the nodes again. A buyer with
312    /// no way to add funds returns [`Recovery::Unavailable`].
313    ///
314    /// # Errors
315    ///
316    /// If the on-chain `topUp` or `openPool` is rejected at submit or reverts:
317    /// the funds did not move. If a submit fails in transport or its receipt is
318    /// not obtained, the funds may have moved, and the error carries
319    /// [`crate::buyer_pool::TopUpUnconfirmed`] for a `topUp` or
320    /// [`crate::buyer_pool::OpenUnconfirmed`] for an `openPool`. If a mined
321    /// transaction cannot be credited locally, the funds **are** escrowed and
322    /// the error names the tx ([`crate::buyer_pool::escrowed_but_untracked`]).
323    /// A step refused because an earlier `topUp` of the run is unconfirmed
324    /// carries that [`crate::buyer_pool::TopUpUnconfirmed`] too.
325    fn recover(&self, remaining: U256) -> SourceFuture<'_, Recovery>;
326
327    /// The funder's own view of what the pool has spent across every lane,
328    /// including lanes the acquire loop does not drive: vouchers to providers
329    /// outside the fetch (or the run). The deposit gate takes the larger of
330    /// this and the committed amounts of the loop's own lanes, so a pool shared
331    /// across many providers gates on its true remaining deposit.
332    ///
333    /// `None` by default: the funder has no view beyond the loop's lanes.
334    fn pool_spent(&self) -> Option<U256> {
335        None
336    }
337}
338
339/// Current unix time in microseconds (the requester-echoed
340/// [`decdn_protocol::client::StreamRequest::timestamp_us`]). Mirrors the CLI's
341/// `micros_now` (`crates/cli/src/commands/fetch.rs`) and the node's `now_micros`
342/// (`crates/node/src/node_origin/mod.rs`) — each caller of `open_progressive_pull`
343/// owns its own copy rather than sharing one across crate boundaries, and
344/// `PeerSource` needs the same one-liner here.
345fn micros_now() -> u64 {
346    u64::try_from(
347        SystemTime::now()
348            .duration_since(UNIX_EPOCH)
349            .map_or(0, |d| d.as_micros()),
350    )
351    .unwrap_or(u64::MAX)
352}
353
354/// The paid `BlobSource` (#1608): wraps
355/// [`crate::open_progressive_pull`] behind [`BlobSource`], scoping every
356/// [`open`](BlobSource::open) to exactly the requested [`AlignedRange`] via the
357/// `byte_len`-bounded pull: the wire `StreamRequest` carries the field and the
358/// OPEN-side plumbing scopes each pull to the requested range.
359///
360/// One `PeerSource` serves one gap-driven fetch against one upstream peer — it
361/// holds the pull context (`endpoint`, `target`, `ctx`, `ledger`, …) but not a
362/// [`Funder`]: funding recovery is the acquire loop's concern, not the source's.
363///
364/// Borrows its `endpoint`/`slash_domain` (the driver, which owns these for the
365/// whole fetch, outlives every `open`/`finish` call), but holds the pool
366/// context behind a SHARED `Arc<Mutex<PoolContext>>` rather than a `&'a`
367/// borrow. That shared handle is what resolves the #1608 borrow conflict: the
368/// acquire loop mutates the context (crediting a funding recovery step's new
369/// deposit) while this source also reads it to open each pull. A
370/// `&'a PoolContext` borrow would freeze it for the whole fetch and forbid that
371/// write; the `Arc<Mutex<..>>` lets both see one state. `open` locks it only to
372/// CLONE the context out, then drops the guard before awaiting, so no lock is
373/// ever held across an `.await`. An open's snapshot can predate a deposit that
374/// is about to land: it under-states the deposit, never over-states it.
375///
376/// With a [`Connections`] map, every open takes a new stream on the target's
377/// shared connection. Without one, every open dials its own connection.
378pub struct PeerSource<'a> {
379    endpoint: &'a Endpoint,
380    target: EndpointAddr,
381    ctx: Arc<Mutex<PoolContext>>,
382    ledger: Arc<PoolLedger>,
383    slash_domain: &'a Eip712Domain,
384    expected_signer: Address,
385    namespace_id: [u8; 32],
386    max_blob_size_bytes: u64,
387    max_rate_per_mb: u64,
388    deadlines: PullDeadlines,
389    /// The command's shared connection per node, or `None` to dial per open.
390    connections: Option<Connections>,
391}
392
393impl std::fmt::Debug for PeerSource<'_> {
394    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395        // `PoolContext` (a signing key) and `Eip712Domain` are not `Debug`,
396        // so this prints only the non-sensitive routing/policy fields.
397        f.debug_struct("PeerSource")
398            .field("target", &self.target)
399            .field("expected_signer", &self.expected_signer)
400            .field("namespace_id", &self.namespace_id)
401            .field("max_blob_size_bytes", &self.max_blob_size_bytes)
402            .field("max_rate_per_mb", &self.max_rate_per_mb)
403            .field("deadlines", &self.deadlines)
404            .finish_non_exhaustive()
405    }
406}
407
408impl<'a> PeerSource<'a> {
409    /// Build a source for one gap-driven fetch against `target`, paid out of
410    /// `ledger` over `ctx`'s lane. `namespace_id`, `max_rate_per_mb`, and
411    /// `deadlines` are the same buyer-side policy knobs
412    /// [`crate::open_progressive_pull`] takes directly — see its docs.
413    /// `max_blob_size_bytes` is the cap on bytes held for one blob (#1895): each
414    /// stream carries at most that much content, and the driver reads it back via
415    /// [`BlobSource::max_blob_size_bytes`] to end a fill that would hold more;
416    /// `0` = unlimited.
417    ///
418    /// `connections` is the command's map of one connection per node: with
419    /// `Some`, every open takes a new stream on `target`'s shared connection
420    /// and dials only when `target` has no live one, on the map's dial
421    /// runtime (see [`Connections`]). With `None`, every open dials its own
422    /// connection on the caller's runtime, so the caller's runtime must
423    /// outlive the endpoint's close.
424    #[must_use]
425    #[allow(clippy::too_many_arguments)]
426    pub const fn new(
427        endpoint: &'a Endpoint,
428        target: EndpointAddr,
429        ctx: Arc<Mutex<PoolContext>>,
430        ledger: Arc<PoolLedger>,
431        slash_domain: &'a Eip712Domain,
432        expected_signer: Address,
433        namespace_id: [u8; 32],
434        max_blob_size_bytes: u64,
435        max_rate_per_mb: u64,
436        deadlines: PullDeadlines,
437        connections: Option<Connections>,
438    ) -> Self {
439        Self {
440            endpoint,
441            target,
442            ctx,
443            ledger,
444            slash_domain,
445            expected_signer,
446            namespace_id,
447            max_blob_size_bytes,
448            max_rate_per_mb,
449            deadlines,
450            connections,
451        }
452    }
453
454    /// Open `[byte_offset, +byte_len)` of `hash` (`byte_len == 0` = to end)
455    /// as a raw pull, unaligned, for a caller that reads only the signed
456    /// header and then aborts the pull.
457    ///
458    /// # Errors
459    ///
460    /// The same faults as [`BlobSource::open`].
461    #[doc(hidden)]
462    pub fn open_span(
463        &self,
464        hash: [u8; 32],
465        byte_offset: u64,
466        byte_len: u64,
467    ) -> SourceFuture<'_, (UpstreamPullHeader, UpstreamPull)> {
468        Box::pin(self.open_pull(hash, byte_offset, byte_len))
469    }
470
471    /// Open the whole blob (`byte_len == 0`, "to end") from offset 0.
472    ///
473    /// For a caller that must read the signed `total_bytes` before it can size
474    /// the store a drive fills. Hand the result to a [`PrimedSource`] primed at
475    /// `align_range(0, 0, total_bytes)` and the drive's first leg adopts this
476    /// pull rather than opening the same range a second time (#2063).
477    ///
478    /// # Errors
479    ///
480    /// The same faults as [`BlobSource::open`].
481    #[doc(hidden)]
482    pub fn open_whole(&self, hash: [u8; 32]) -> SourceFuture<'_, (UpstreamPullHeader, PullReader)> {
483        Box::pin(async move {
484            let pull = self.open_pull(hash, 0, 0).await?;
485            Ok((pull.0, PullReader::new(pull.1)))
486        })
487    }
488
489    /// Open `[byte_offset, +byte_len)` of `hash` (`byte_len == 0` = to end):
490    /// on the target's shared connection when the source has a
491    /// [`Connections`] map, else on a connection of its own.
492    async fn open_pull(
493        &self,
494        hash: [u8; 32],
495        byte_offset: u64,
496        byte_len: u64,
497    ) -> anyhow::Result<(UpstreamPullHeader, UpstreamPull)> {
498        // Snapshot the shared context, then drop the guard before the await —
499        // `open_progressive_pull` needs `&PoolContext` for its whole call, and no
500        // std `Mutex` guard may be held across an `.await`.
501        let ctx = {
502            self.ctx
503                .lock()
504                .map_err(|_| anyhow::anyhow!("pool context lock poisoned"))?
505                .clone()
506        };
507        if let Some(connections) = &self.connections {
508            self.open_shared(connections, &ctx, hash, byte_offset, byte_len)
509                .await
510        } else {
511            crate::open_progressive_pull(
512                self.endpoint,
513                self.target.clone(),
514                &ctx,
515                Arc::clone(&self.ledger),
516                self.slash_domain,
517                self.expected_signer,
518                hash,
519                self.namespace_id,
520                byte_offset,
521                micros_now(),
522                self.max_blob_size_bytes,
523                self.max_rate_per_mb,
524                self.deadlines,
525                byte_len,
526            )
527            .await
528        }
529    }
530
531    /// Open the pull on the target's connection in `connections`, dialling
532    /// one when the target has none. `deadlines.open` bounds the dial, and
533    /// again the stream open that follows it.
534    ///
535    /// The pull holds the connection while it runs. A transport fault on the
536    /// open or on a later read (a connection reset, a closed connection, an
537    /// idle timeout, an open or read that falls under its deadline) unpins
538    /// this connection from the map, so the next open dials again. Any other
539    /// failure (a refusal, a bad response, a hash mismatch) leaves the
540    /// connection pinned for the next open.
541    ///
542    /// A connection that was pinned before this open and is closed under it
543    /// is a stale pin: the target reaped it idle between legs, or its budget
544    /// ran out. The open then redials once and opens again. No byte is paid
545    /// before the header, so the retry costs nothing. A connection this open
546    /// dialled itself that fails is the target's failure, and is not retried.
547    async fn open_shared(
548        &self,
549        connections: &Connections,
550        ctx: &PoolContext,
551        hash: [u8; 32],
552        byte_offset: u64,
553        byte_len: u64,
554    ) -> anyhow::Result<(UpstreamPullHeader, UpstreamPull)> {
555        debug_assert_eq!(
556            connections.alpn(),
557            decdn_protocol::ALPN_CLIENT,
558            "a pull opens on a cdn/client/v1 map"
559        );
560        let lease = self.lease(connections).await?;
561        let pinned = lease.is_pinned();
562        let warm = lease.into_warm();
563        let err = match self
564            .open_on(connections, warm.clone(), ctx, hash, byte_offset, byte_len)
565            .await
566        {
567            Ok(opened) => return Ok(opened),
568            Err(err) => err,
569        };
570        if !(pinned && warm.is_closed()) {
571            return Err(err);
572        }
573        tracing::debug!(
574            peer = %self.target.id,
575            close = ?warm.connection().close_reason(),
576            error = %err,
577            "pooled connection closed under the open; redialling once"
578        );
579        let fresh = self.lease(connections).await?.into_warm();
580        self.open_on(connections, fresh, ctx, hash, byte_offset, byte_len)
581            .await
582            .with_context(|| format!("after a redial; the pinned connection failed: {err:#}"))
583    }
584
585    /// The target's connection in `connections`, dialled when it has none,
586    /// under `deadlines.open`.
587    async fn lease(&self, connections: &Connections) -> anyhow::Result<crate::connection::Lease> {
588        let open = self.deadlines.open;
589        tokio::time::timeout(open, connections.get_or_dial(&self.target))
590            .await
591            .map_err(|_| anyhow::Error::new(crate::PullTimeout { after: open }))?
592    }
593
594    /// Open the pull as a stream on `warm`, which the pull holds and unpins
595    /// from `connections` on a transport fault.
596    async fn open_on(
597        &self,
598        connections: &Connections,
599        warm: Arc<crate::WarmConnection>,
600        ctx: &PoolContext,
601        hash: [u8; 32],
602        byte_offset: u64,
603        byte_len: u64,
604    ) -> anyhow::Result<(UpstreamPullHeader, UpstreamPull)> {
605        let conn = warm.connection().clone();
606        crate::open_progressive_pull_on(
607            &conn,
608            crate::connection::ConnOwner::Shared {
609                warm,
610                map: connections.clone(),
611            },
612            ctx,
613            Arc::clone(&self.ledger),
614            self.slash_domain,
615            self.expected_signer,
616            hash,
617            self.namespace_id,
618            byte_offset,
619            micros_now(),
620            self.max_blob_size_bytes,
621            self.max_rate_per_mb,
622            self.deadlines,
623            byte_len,
624        )
625        .await
626    }
627}
628
629impl BlobSource for PeerSource<'_> {
630    type Reader = PullReader;
631
632    fn open(
633        &self,
634        hash: [u8; 32],
635        range: AlignedRange,
636    ) -> SourceFuture<'_, (UpstreamPullHeader, Self::Reader)> {
637        Box::pin(async move {
638            // One line per paid leg, so a stalled leg can be matched to the
639            // node's own serve of the same range (#2211).
640            tracing::debug!(
641                peer = %self.target.id,
642                hash = %blake3::Hash::from_bytes(hash).to_hex(),
643                byte_offset = range.fetch_start(),
644                byte_len = range.fetch_len(),
645                "opening a paid leg"
646            );
647            let (header, pull) = self
648                .open_pull(hash, range.fetch_start(), range.fetch_len())
649                .await?;
650            Ok((header, PullReader::new(pull)))
651        })
652    }
653
654    fn finish(&self, reader: Self::Reader) -> SourceFuture<'_, VoucherProgress> {
655        Box::pin(async move { reader.into_inner().finish().await })
656    }
657
658    fn stop(&self, reader: Self::Reader) -> SourceFuture<'_, VoucherProgress> {
659        Box::pin(async move { reader.into_inner().stop().await })
660    }
661
662    fn max_blob_size_bytes(&self) -> u64 {
663        self.max_blob_size_bytes
664    }
665}
666
667/// How long a primed pull may wait for its adopting open. A pull opened ahead of
668/// its drive is only worth adopting straight away: an idle one is a stream the
669/// peer is holding bytes on, and a peer that sees no voucher for long enough
670/// drops it. A primed pull older than this is dropped and the open goes to the
671/// wrapped source. The age runs from when the pull opened, which the caller
672/// passes to [`PrimedSource::prime`].
673pub(crate) const PRIMED_MAX_IDLE: Duration = Duration::from_secs(2);
674
675/// A pull opened ahead of the drive, waiting for the open it answers.
676struct Primed<R> {
677    hash: [u8; 32],
678    range: AlignedRange,
679    header: UpstreamPullHeader,
680    reader: R,
681    at: tokio::time::Instant,
682}
683
684/// A [`BlobSource`] that hands one already-open pull to the drive (#2063).
685///
686/// A caller that opens a pull before the drive (to read the signed
687/// `total_bytes` the store is sized from, or to learn whether the peer serves
688/// at all) would otherwise drop it and let the drive open the same range again.
689/// The node treats every open as real: it signs, claims a fill, and starts an
690/// origin draw, so the thrown-away open costs a duplicate draw and delays the
691/// real one. [`prime`](Self::prime) parks the live pull here instead, and the
692/// drive's first [`open`](BlobSource::open) of exactly that hash and range takes
693/// it. Any other open, and an open that finds the primed pull opened longer than
694/// [`PRIMED_MAX_IDLE`] ago, goes to the wrapped source.
695///
696/// Adoption is by exact [`AlignedRange`] equality, never by containment: a
697/// pull's [`finish`](BlobSource::finish) drains and pays to its stream end, so a
698/// longer pull in place of a shorter leg would pay for bytes the leg never asked
699/// for. Call [`clear`](Self::clear) once the drive returns, so a pull no open
700/// took is closed at once rather than left idle.
701#[doc(hidden)]
702pub struct PrimedSource<S: BlobSource> {
703    inner: S,
704    primed: Mutex<Option<Primed<S::Reader>>>,
705}
706
707impl<S: BlobSource> std::fmt::Debug for PrimedSource<S> {
708    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
709        let primed = self.slot().is_some();
710        f.debug_struct("PrimedSource")
711            .field("primed", &primed)
712            .finish_non_exhaustive()
713    }
714}
715
716impl<S: BlobSource> PrimedSource<S> {
717    /// The parked-pull slot. The slot is a plain swap with no invariant a panic
718    /// could break, so a poisoned lock is used as is.
719    fn slot(&self) -> std::sync::MutexGuard<'_, Option<Primed<S::Reader>>> {
720        self.primed
721            .lock()
722            .unwrap_or_else(std::sync::PoisonError::into_inner)
723    }
724
725    /// Wrap `inner` with nothing primed.
726    #[must_use]
727    pub const fn new(inner: S) -> Self {
728        Self {
729            inner,
730            primed: Mutex::new(None),
731        }
732    }
733
734    /// Park a live pull of `range` of `hash`, opened at `opened_at`, for the next
735    /// open of exactly that range. Replaces (and so closes) any pull still
736    /// parked. The pull's age runs from `opened_at`, not from this call, so a
737    /// caller that holds the pull a while before parking it cannot hand the
738    /// drive a pull the peer has stopped holding.
739    pub fn prime(
740        &self,
741        hash: [u8; 32],
742        range: AlignedRange,
743        header: UpstreamPullHeader,
744        reader: S::Reader,
745        opened_at: tokio::time::Instant,
746    ) {
747        let parked = Primed {
748            hash,
749            range,
750            header,
751            reader,
752            at: opened_at,
753        };
754        let replaced = self.slot().replace(parked);
755        if replaced.is_some() {
756            tracing::debug!("a primed pull no open took is replaced and closed");
757        }
758    }
759
760    /// Close a parked pull that no open took.
761    pub fn clear(&self) {
762        if self.slot().take().is_some() {
763            tracing::debug!("a primed pull no open took is closed");
764        }
765    }
766
767    /// The wrapped source.
768    pub const fn inner(&self) -> &S {
769        &self.inner
770    }
771
772    /// Take the parked pull if it answers an open of `range` of `hash`. A stale
773    /// parked pull is dropped here whatever the open asks for; a fresh one for
774    /// another range stays parked.
775    fn take(
776        &self,
777        hash: [u8; 32],
778        range: &AlignedRange,
779    ) -> Option<(UpstreamPullHeader, S::Reader)> {
780        let mut slot = self.slot();
781        let parked = slot.take()?;
782        if parked.at.elapsed() > PRIMED_MAX_IDLE {
783            tracing::debug!("a primed pull went stale before its open and is closed");
784            return None;
785        }
786        if parked.hash == hash && parked.range == *range {
787            return Some((parked.header, parked.reader));
788        }
789        *slot = Some(parked);
790        None
791    }
792}
793
794impl<S: BlobSource> BlobSource for PrimedSource<S> {
795    type Reader = S::Reader;
796
797    fn open(
798        &self,
799        hash: [u8; 32],
800        range: AlignedRange,
801    ) -> SourceFuture<'_, (UpstreamPullHeader, Self::Reader)> {
802        match self.take(hash, &range) {
803            Some(adopted) => Box::pin(async move { Ok(adopted) }),
804            None => self.inner.open(hash, range),
805        }
806    }
807
808    fn finish(&self, reader: Self::Reader) -> SourceFuture<'_, VoucherProgress> {
809        self.inner.finish(reader)
810    }
811
812    fn stop(&self, reader: Self::Reader) -> SourceFuture<'_, VoucherProgress> {
813        self.inner.stop(reader)
814    }
815
816    fn max_blob_size_bytes(&self) -> u64 {
817        self.inner.max_blob_size_bytes()
818    }
819}
820
821// ---------------------------------------------------------------------------
822// Test doubles: a scripted BlobSource + a fake Funder for the driver tests.
823// Feature-gated so a production caller cannot name them, but compiled outside
824// `cfg(test)` under `test-util`, so they must stay anti-panic clean.
825// ---------------------------------------------------------------------------
826
827#[cfg(any(test, feature = "test-util"))]
828mod doubles {
829    use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
830    use std::sync::{Arc, Mutex};
831    use std::time::Duration;
832
833    use super::{Recovery, SourceFuture, StashedFault};
834    use crate::{UpstreamPullHeader, VoucherProgress};
835    use alloy::primitives::U256;
836    use bao_tree::io::outboard::PreOrderMemOutboard;
837    use bytes::Bytes;
838    use decdn_bao_range::{AlignedRange, IROH_BLOCK_SIZE, encode_verified_range};
839
840    /// Fixed quote a [`ScriptedSource`] reports so the driver can price vouchers
841    /// deterministically in tests.
842    const SCRIPTED_RATE_PER_MB: u64 = 1;
843    const SCRIPTED_INTERVAL_BYTES: u64 = 1024 * 1024;
844
845    /// A healthy buyer context paying `provider`, with `deposit` on the pool.
846    #[cfg(test)]
847    pub(crate) fn ctx_with(provider: u8, deposit: U256) -> crate::PoolContext {
848        use alloy::primitives::{Address, B256};
849        use alloy::signers::local::PrivateKeySigner;
850
851        let signer = PrivateKeySigner::random();
852        crate::PoolContext {
853            pool_id: B256::ZERO,
854            provider: Address::repeat_byte(provider),
855            deposit,
856            client_signer: Arc::new(signer),
857            voucher_domain: decdn_incentive::bind_node_id_domain(1, Address::ZERO),
858            prior_bytes_delivered: U256::ZERO,
859            prior_amount: U256::ZERO,
860            client_binding: None,
861            capability: None,
862        }
863    }
864
865    /// Builds a typed fault to park mid-range. Boxed so a source can be re-opened
866    /// (an `anyhow::Error` is not `Clone`, so it is regenerated per open).
867    type FaultFn = Arc<dyn Fn() -> anyhow::Error + Send + Sync>;
868
869    /// One leg's `(fetch_start, opened_at, finished_at)` on the runtime's
870    /// clock; `finished_at` is `None` for a leg that faulted or was dropped.
871    type LegTimes = (u64, tokio::time::Instant, Option<tokio::time::Instant>);
872
873    /// A scripted [`BlobSource`](super::BlobSource) that yields the real bao wire
874    /// for any requested range of a fixed blob, and can truncate a range's wire
875    /// and park a typed fault to simulate a mid-range peer failure.
876    #[derive(Clone)]
877    pub struct ScriptedSource {
878        root: [u8; 32],
879        blob: Bytes,
880        outboard: Bytes,
881        fault: Option<(usize, FaultFn)>,
882        /// When set, `fault` fires on the readers that reach it while this
883        /// count is above zero, and never again. Shared by clones.
884        faults_left: Option<Arc<AtomicU32>>,
885        /// When set, the size every header signs in place of the blob's own.
886        signed_size: Option<u64>,
887        /// Refuse every open whose 0-based index (in call order) lies in the
888        /// range, with the fault its `FaultFn` produces, before any byte flows:
889        /// a node that refuses one more stream while it serves the others. A
890        /// refused open is still recorded in `opened`.
891        refuse_open: Option<(std::ops::Range<usize>, FaultFn)>,
892        /// Refuse every open whose range touches one of these discovery
893        /// blocks, with the fault its `FaultFn` produces, before any byte
894        /// flows: a partial holder asked for a block it does not hold. A
895        /// refused open is still recorded in `opened`.
896        refuse_blocks: Option<(Vec<u32>, FaultFn)>,
897        /// Hold the fault of the open with each 0-based index (in call order)
898        /// until its gate reads `true`: a refusal waits inside `open`, and a
899        /// parked mid-range fault waits once the reader has delivered every
900        /// byte before it. A test orders one leg's fault after another event
901        /// this way, whatever the machine's speed.
902        fault_gates: Vec<(usize, tokio::sync::watch::Receiver<bool>)>,
903        /// When each `open` started and each clean `finish` ended, on the
904        /// runtime's clock, as `(fetch_start, opened_at, finished_at)`. A leg
905        /// that faulted or was dropped has no `finished_at`. Shared by clones.
906        timeline: Arc<Mutex<Vec<LegTimes>>>,
907        /// Every range this source was `open`ed for, in call order, as
908        /// `(fetch_start, fetch_len)`. Shared behind an `Arc<Mutex<..>>` so a
909        /// clone handed to the driver records into the same log the test
910        /// inspects — the ledger the #1608 money assertion reads to prove the
911        /// driver pulled ONLY the gaps of `missing_ranges` and never a held
912        /// range.
913        opened: Arc<Mutex<Vec<(u64, u64)>>>,
914        /// Total WIRE bytes this source's readers actually delivered (summed
915        /// across every reader). Unlike [`opened`](Self::opened) — which records
916        /// the requested `fetch_len` at `open` time, before a byte streams —
917        /// this counts bytes that truly left the source, so it is the honest
918        /// proxy for "bytes fetched and paid for". A leg stopped or dropped
919        /// mid-stream stops incrementing this the instant it stops reading,
920        /// which is exactly what lets the multi-source no-double-pay assertion
921        /// see that a stolen tail was fetched by ONE source, not two.
922        delivered: Arc<AtomicU64>,
923        /// When set, every reader waits on its first read until the gate reads
924        /// `true`: a source held back until the test releases it.
925        gate: Option<tokio::sync::watch::Receiver<bool>>,
926        /// One-time stall injected on the FIRST read of every reader this source
927        /// yields. Models a slow-to-start peer; a fast peer (no stall) then
928        /// reliably finishes its own segment and steals the slow peer's tail,
929        /// forcing the steal path deterministically without wall-clock racing on
930        /// per-byte timing.
931        first_read_stall: Option<Duration>,
932        /// Delay injected inside `finish`, AFTER the range's bytes are fully
933        /// delivered but BEFORE `fill_gap` returns. Holds the completed range in
934        /// the scheduler's `in_flight` (delivered, not yet cleared) so a peer's
935        /// steal of an already-present range is deterministic — the exact
936        /// completed-but-uncleared window the present-bytes backstop must close.
937        finish_stall: Option<Duration>,
938        /// Wedge every reader after it has delivered `n` wire bytes: it sleeps
939        /// for the given duration instead of yielding the next chunk. Models a
940        /// source that opens, delivers a prefix, then stops making progress
941        /// WITHOUT erroring — the only shape that reaches the scheduler's stall
942        /// watchdog. A source that returns `Err` takes the fault path instead,
943        /// so `with_fault_after` cannot stand in for this.
944        stall_after: Option<(u64, Duration)>,
945        /// Optional ledger to advance on a clean `finish`, modelling payment: a
946        /// real pull pays vouchers for the WIRE bytes it drains, and the driver's
947        /// completion is PAID-frontier based (`content_paid_frontier`), so a double
948        /// that never advanced `committed` would leave the driver's paid frontier at
949        /// zero and it would never complete. `None` keeps the pre-payment "unpaid
950        /// double" behaviour for tests that do not drive `fill_gap` to completion.
951        ledger: Option<Arc<crate::PoolLedger>>,
952        /// How many pulls were stopped before the end of their range. Shared
953        /// by clones.
954        stops: Arc<AtomicU32>,
955        /// A delay before every `read_bytes` of every reader: a source with a
956        /// steady delivery rate, on the runtime's clock.
957        read_delay: Option<Duration>,
958        /// The cap on bytes held for the blob this source reports to the
959        /// driver ([`super::BlobSource::max_blob_size_bytes`]); `0` is none.
960        max_blob_size: u64,
961    }
962
963    impl std::fmt::Debug for ScriptedSource {
964        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
965            f.debug_struct("ScriptedSource")
966                .field("root", &blake3::Hash::from_bytes(self.root))
967                .field("total_bytes", &self.total_bytes())
968                .field("fault_after", &self.fault.as_ref().map(|(after, _)| *after))
969                .field("opened_ranges", &self.opened_ranges())
970                .finish_non_exhaustive()
971        }
972    }
973
974    impl ScriptedSource {
975        /// Build a source over `blob`, computing its root and outboard once.
976        ///
977        /// # Errors
978        ///
979        /// If the blob length does not fit the bao tree math (only on absurdly
980        /// large inputs a test never uses).
981        pub fn new(blob: impl Into<Bytes>) -> anyhow::Result<Self> {
982            let blob = blob.into();
983            let ob = PreOrderMemOutboard::create(&blob, IROH_BLOCK_SIZE);
984            Ok(Self {
985                root: *ob.root.as_bytes(),
986                blob,
987                outboard: ob.data.into(),
988                fault: None,
989                faults_left: None,
990                signed_size: None,
991                refuse_open: None,
992                refuse_blocks: None,
993                fault_gates: Vec::new(),
994                timeline: Arc::new(Mutex::new(Vec::new())),
995                opened: Arc::new(Mutex::new(Vec::new())),
996                delivered: Arc::new(AtomicU64::new(0)),
997                gate: None,
998                first_read_stall: None,
999                finish_stall: None,
1000                stall_after: None,
1001                ledger: None,
1002                stops: Arc::new(AtomicU32::new(0)),
1003                read_delay: None,
1004                max_blob_size: 0,
1005            })
1006        }
1007
1008        #[cfg(test)]
1009        /// Report a cap of `bytes` held for the blob to the driver, as a
1010        /// buyer's `max_blob_size_bytes` does.
1011        #[must_use]
1012        pub(crate) const fn capped(mut self, bytes: u64) -> Self {
1013            self.max_blob_size = bytes;
1014            self
1015        }
1016
1017        #[cfg(test)]
1018        /// Delay every `finish` by `stall` after its range is fully delivered,
1019        /// holding the completed range in the scheduler's `in_flight` so a peer's
1020        /// steal of an already-present range fires deterministically (see
1021        /// `finish_stall`).
1022        #[must_use]
1023        pub(crate) const fn slow_finish(mut self, stall: Duration) -> Self {
1024            self.finish_stall = Some(stall);
1025            self
1026        }
1027
1028        #[cfg(test)]
1029        /// Wedge every reader once it has delivered `after_bytes`: the next read
1030        /// sleeps for `stall` rather than returning bytes, so the source stops
1031        /// making verified progress without ever erroring (see
1032        /// [`stall_after`](Self::stall_after)). Pair a `stall` well above the
1033        /// scheduler's lane watchdog (`LANE_WATCHDOG`) with a nonzero
1034        /// `after_bytes` to trip the watchdog deterministically.
1035        #[must_use]
1036        pub(crate) const fn stall_after(mut self, after_bytes: u64, stall: Duration) -> Self {
1037            self.stall_after = Some((after_bytes, stall));
1038            self
1039        }
1040
1041        #[cfg(test)]
1042        /// Sleep `per_read` before every `read_bytes` of every reader, so the
1043        /// source delivers at a steady rate a test sets.
1044        #[must_use]
1045        pub(crate) const fn throttled(mut self, per_read: Duration) -> Self {
1046            self.read_delay = Some(per_read);
1047            self
1048        }
1049
1050        #[cfg(test)]
1051        /// Hold every reader's first read until `gate` reads `true`, so a test
1052        /// decides when this source starts to deliver.
1053        #[must_use]
1054        pub(crate) fn gated_on(mut self, gate: tokio::sync::watch::Receiver<bool>) -> Self {
1055            self.gate = Some(gate);
1056            self
1057        }
1058
1059        #[cfg(test)]
1060        /// Inject a one-time `stall` on the first read of every reader this
1061        /// source yields, so a competing fast source finishes first and steals
1062        /// this one's tail — the deterministic trigger the no-double-pay test
1063        /// needs.
1064        #[must_use]
1065        pub(crate) const fn slow_to_start(mut self, stall: Duration) -> Self {
1066            self.first_read_stall = Some(stall);
1067            self
1068        }
1069
1070        #[cfg(test)]
1071        /// Total WIRE bytes actually delivered across every reader (see
1072        /// `delivered`). The honest "fetched and paid" proxy
1073        /// the no-double-pay assertion reads.
1074        #[must_use]
1075        pub(crate) fn delivered_bytes(&self) -> u64 {
1076            self.delivered.load(Ordering::SeqCst)
1077        }
1078
1079        /// Model payment: on every clean `finish`, advance `ledger`'s committed
1080        /// watermark by the leg's drained WIRE bytes (at `SCRIPTED_RATE_PER_MB`),
1081        /// exactly as a real pull's acked vouchers do. Required by any test that
1082        /// drives a gap to completion, since the driver's `Done` is paid-frontier
1083        /// based. Pass the SAME `Arc<PoolLedger>` the driver is handed.
1084        #[must_use]
1085        pub fn paying(mut self, ledger: Arc<crate::PoolLedger>) -> Self {
1086            self.ledger = Some(ledger);
1087            self
1088        }
1089
1090        /// Stamp `reader`'s leg finished and, on a paying double, commit one
1091        /// voucher for `wire` bytes: a successful issue commits optimistically
1092        /// (implicit acceptance, ADR 005), advancing `committed.bytes` by the
1093        /// paid WIRE bytes so the driver's paid frontier tracks payment.
1094        async fn settle(
1095            &self,
1096            reader: &ScriptedReader,
1097            wire: u64,
1098        ) -> anyhow::Result<VoucherProgress> {
1099            if let Some(leg) = reader.leg
1100                && let Ok(mut timeline) = self.timeline.lock()
1101                && let Some(entry) = timeline.get_mut(leg)
1102            {
1103                entry.2 = Some(tokio::time::Instant::now());
1104            }
1105            let Some(ledger) = &self.ledger else {
1106                // Unpaid double: no lane, nothing to drain, no watermark.
1107                return Ok(VoucherProgress::default());
1108            };
1109            if wire > 0 {
1110                ledger
1111                    .issue(
1112                        wire,
1113                        SCRIPTED_RATE_PER_MB,
1114                        crate::EpochAction::Keep,
1115                        |_next, _chain| async { Ok(()) },
1116                    )
1117                    .await?;
1118            }
1119            Ok(VoucherProgress::from_cumulative(
1120                ledger.committed(),
1121                U256::ZERO,
1122            ))
1123        }
1124
1125        #[cfg(test)]
1126        /// How many pulls this source stopped before the end of their range
1127        /// ([`BlobSource::stop`](crate::BlobSource::stop)).
1128        #[must_use]
1129        pub(crate) fn stopped_pulls(&self) -> u32 {
1130            self.stops.load(Ordering::SeqCst)
1131        }
1132
1133        /// The blob's BLAKE3 root — the `hash` the driver opens against.
1134        #[must_use]
1135        pub const fn root(&self) -> [u8; 32] {
1136            self.root
1137        }
1138
1139        /// Total blob length.
1140        #[must_use]
1141        pub const fn total_bytes(&self) -> u64 {
1142            self.blob.len() as u64
1143        }
1144
1145        /// Every range `open` was called for, in call order, as
1146        /// `(fetch_start, fetch_len)`. The #1608 money assertion checks this
1147        /// equals exactly the contiguous gaps of `missing_ranges` — no held
1148        /// range is ever opened, so no held byte is ever re-pulled or re-paid.
1149        #[must_use]
1150        pub(crate) fn opened_ranges(&self) -> Vec<(u64, u64)> {
1151            self.opened.lock().map(|o| o.clone()).unwrap_or_default()
1152        }
1153
1154        #[cfg(test)]
1155        /// Refuse the open with 0-based index `nth`, in call order, with the
1156        /// fault `make` produces. Every other open is served as usual.
1157        #[must_use]
1158        pub(crate) fn refusing_open(
1159            mut self,
1160            nth: usize,
1161            make: impl Fn() -> anyhow::Error + Send + Sync + 'static,
1162        ) -> Self {
1163            self.refuse_open = Some((nth..nth.saturating_add(1), Arc::new(make)));
1164            self
1165        }
1166
1167        #[cfg(test)]
1168        /// Hold the fault of the open with 0-based index `nth`, in call order,
1169        /// until `gate` reads `true` (see `fault_gates`). An open with no fault
1170        /// is served as usual.
1171        #[must_use]
1172        pub(crate) fn holding_fault(
1173            mut self,
1174            nth: usize,
1175            gate: tokio::sync::watch::Receiver<bool>,
1176        ) -> Self {
1177            self.fault_gates.push((nth, gate));
1178            self
1179        }
1180
1181        #[cfg(test)]
1182        /// Refuse every open from 0-based index `first` on, in call order,
1183        /// with the fault `make` produces: a node that serves the streams it
1184        /// already has and refuses any more.
1185        #[must_use]
1186        pub(crate) fn refusing_opens_from(
1187            mut self,
1188            first: usize,
1189            make: impl Fn() -> anyhow::Error + Send + Sync + 'static,
1190        ) -> Self {
1191            self.refuse_open = Some((first..usize::MAX, Arc::new(make)));
1192            self
1193        }
1194
1195        #[cfg(test)]
1196        /// Refuse every open whose range touches one of the discovery
1197        /// `blocks`, with the fault `make` produces: a partial holder that
1198        /// cannot serve those blocks by pull-through.
1199        #[must_use]
1200        pub(crate) fn refusing_blocks(
1201            mut self,
1202            blocks: &[u32],
1203            make: impl Fn() -> anyhow::Error + Send + Sync + 'static,
1204        ) -> Self {
1205            self.refuse_blocks = Some((blocks.to_vec(), Arc::new(make)));
1206            self
1207        }
1208
1209        #[cfg(test)]
1210        /// Each leg's `(fetch_start, opened_at, finished_at)`, in open order
1211        /// (see `timeline`).
1212        #[must_use]
1213        pub(crate) fn timeline(&self) -> Vec<LegTimes> {
1214            self.timeline.lock().map(|t| t.clone()).unwrap_or_default()
1215        }
1216
1217        #[cfg(test)]
1218        /// Total content bytes opened across every `open` call (the sum of each
1219        /// opened range's `fetch_len`). Equals the gap bytes, NOT the whole blob,
1220        /// when the driver skips held ranges.
1221        #[must_use]
1222        pub(crate) fn opened_bytes(&self) -> u64 {
1223            self.opened
1224                .lock()
1225                .map_or(0, |o| o.iter().map(|(_, len)| *len).sum())
1226        }
1227
1228        /// After `wire_bytes` of a range's wire, truncate it and park the fault
1229        /// `make` produces — the exact shape a stalled/refusing peer leaves.
1230        #[must_use]
1231        pub fn with_fault_after(
1232            mut self,
1233            wire_bytes: usize,
1234            make: impl Fn() -> anyhow::Error + Send + Sync + 'static,
1235        ) -> Self {
1236            self.fault = Some((wire_bytes, Arc::new(make)));
1237            self
1238        }
1239
1240        /// [`Self::with_fault_after`], but only the first reader that reaches
1241        /// `wire_bytes` faults: a peer that blips once and then recovers.
1242        #[must_use]
1243        pub fn fault_once_after(
1244            self,
1245            wire_bytes: usize,
1246            make: impl Fn() -> anyhow::Error + Send + Sync + 'static,
1247        ) -> Self {
1248            self.fault_times_after(1, wire_bytes, make)
1249        }
1250
1251        /// [`Self::with_fault_after`], but only the first `times` readers that
1252        /// reach `wire_bytes` fault: a peer that refuses a few times and then
1253        /// serves.
1254        #[must_use]
1255        pub fn fault_times_after(
1256            self,
1257            times: u32,
1258            wire_bytes: usize,
1259            make: impl Fn() -> anyhow::Error + Send + Sync + 'static,
1260        ) -> Self {
1261            let mut this = self.with_fault_after(wire_bytes, make);
1262            this.faults_left = Some(Arc::new(AtomicU32::new(times)));
1263            this
1264        }
1265
1266        /// Sign `total_bytes` in every header in place of the blob's own size:
1267        /// a peer that reports a wrong size. Its wire stays the blob's own, so
1268        /// a leg that holds the final chunk fails to verify under the signed
1269        /// size.
1270        #[must_use]
1271        pub const fn signing_size(mut self, total_bytes: u64) -> Self {
1272            self.signed_size = Some(total_bytes);
1273            self
1274        }
1275
1276        /// The header-less bao wire for `range` (content plus interleaved proof,
1277        /// the same bytes `UpstreamPull::next_chunk` yields).
1278        fn wire_for(&self, range: &AlignedRange) -> anyhow::Result<Bytes> {
1279            let s = usize::try_from(range.fetch_start())?;
1280            let e = usize::try_from(range.fetch_end())?;
1281            let data = self
1282                .blob
1283                .get(s..e)
1284                .ok_or_else(|| anyhow::anyhow!("scripted range out of bounds"))?;
1285            let combined = encode_verified_range(self.root, range, data, self.outboard.clone())?;
1286            // Strip the 8-byte little-endian size header: the wire the pull yields
1287            // is header-less (the node frames the tree itself).
1288            let wire = combined
1289                .get(8..)
1290                .ok_or_else(|| anyhow::anyhow!("combined wire shorter than its 8-byte header"))?;
1291            Ok(Bytes::copy_from_slice(wire))
1292        }
1293    }
1294
1295    impl super::BlobSource for ScriptedSource {
1296        type Reader = ScriptedReader;
1297
1298        fn open(
1299            &self,
1300            hash: [u8; 32],
1301            range: AlignedRange,
1302        ) -> SourceFuture<'_, (UpstreamPullHeader, Self::Reader)> {
1303            Box::pin(async move {
1304                if hash != self.root {
1305                    anyhow::bail!("scripted source opened for a foreign hash");
1306                }
1307                let index = self.opened.lock().map_or(0, |mut log| {
1308                    log.push((range.fetch_start(), range.fetch_len()));
1309                    log.len().saturating_sub(1)
1310                });
1311                let leg = self.timeline.lock().ok().map(|mut t| {
1312                    t.push((range.fetch_start(), tokio::time::Instant::now(), None));
1313                    t.len().saturating_sub(1)
1314                });
1315                let fault_gate = self
1316                    .fault_gates
1317                    .iter()
1318                    .find(|(nth, _)| *nth == index)
1319                    .map(|(_, gate)| gate.clone());
1320                if let Some((refused, make)) = &self.refuse_open
1321                    && refused.contains(&index)
1322                {
1323                    if let Some(mut gate) = fault_gate {
1324                        // A closed gate channel reads as open.
1325                        let _ = gate.wait_for(|open| *open).await;
1326                    }
1327                    return Err(make());
1328                }
1329                if let Some((blocks, make)) = &self.refuse_blocks {
1330                    let block_bytes = decdn_protocol::discovery_block_bytes();
1331                    let first = range.fetch_start() / block_bytes;
1332                    let last = range.fetch_end().saturating_sub(1) / block_bytes;
1333                    if blocks
1334                        .iter()
1335                        .any(|&b| (first..=last).contains(&u64::from(b)))
1336                    {
1337                        return Err(make());
1338                    }
1339                }
1340                // Serve the request clamped to this blob's end, as a node
1341                // does: the requester's range may be aligned under another
1342                // size than this blob's.
1343                let served = decdn_bao_range::align_range_clamped(
1344                    range.fetch_start(),
1345                    range.fetch_len(),
1346                    self.total_bytes(),
1347                )?;
1348                let mut wire = self.wire_for(&served)?;
1349                let mut fault = None;
1350                if let Some((after, make)) = &self.fault
1351                    && *after < wire.len()
1352                    && self.faults_left.as_ref().is_none_or(|left| {
1353                        left.try_update(Ordering::SeqCst, Ordering::SeqCst, |n| n.checked_sub(1))
1354                            .is_ok()
1355                    })
1356                {
1357                    wire = wire.slice(..*after);
1358                    fault = Some(make());
1359                }
1360                let fault_gate = fault.as_ref().and(fault_gate);
1361                let header = UpstreamPullHeader {
1362                    total_bytes: self.signed_size.unwrap_or_else(|| self.total_bytes()),
1363                    rate_per_mb: SCRIPTED_RATE_PER_MB,
1364                    interval_bytes: SCRIPTED_INTERVAL_BYTES,
1365                    // No real network round trip in this scripted double.
1366                };
1367                // Wire bytes this leg will drain (post-fault-truncation). `finish`
1368                // is reached only on a CLEAN drain (the driver skips it on an
1369                // `ingest_stream` fault), so on the paying path this is the full
1370                // range's wire — the exact delta an accepted voucher would cover.
1371                let wire_len = wire.len() as u64;
1372                Ok((
1373                    header,
1374                    ScriptedReader {
1375                        wire,
1376                        fault,
1377                        leg,
1378                        wire_len,
1379                        delivered: Arc::clone(&self.delivered),
1380                        gate: self.gate.clone(),
1381                        fault_gate,
1382                        first_read_stall: self.first_read_stall,
1383                        stall_after: self.stall_after,
1384                        read_so_far: 0,
1385                        read_delay: self.read_delay,
1386                    },
1387                ))
1388            })
1389        }
1390
1391        fn finish(&self, reader: Self::Reader) -> SourceFuture<'_, VoucherProgress> {
1392            Box::pin(async move {
1393                // Hold the completed-but-unpaid range open (delivered, `fill_gap`
1394                // not yet returned) so a peer can steal it while it is present.
1395                if let Some(stall) = self.finish_stall {
1396                    tokio::time::sleep(stall).await;
1397                }
1398                let wire = reader.wire_len;
1399                self.settle(&reader, wire).await
1400            })
1401        }
1402
1403        fn stop(&self, reader: Self::Reader) -> SourceFuture<'_, VoucherProgress> {
1404            Box::pin(async move {
1405                self.stops.fetch_add(1, Ordering::SeqCst);
1406                // A stopped pull pays for the wire it received, not the range.
1407                let wire = reader.wire_len.saturating_sub(reader.wire.len() as u64);
1408                self.settle(&reader, wire).await
1409            })
1410        }
1411
1412        fn max_blob_size_bytes(&self) -> u64 {
1413            self.max_blob_size
1414        }
1415    }
1416
1417    /// The reader a [`ScriptedSource`] yields: a fixed wire buffer, optionally
1418    /// holding a parked typed fault. Mirrors the production `PullReader`'s two
1419    /// behaviours the decode loop depends on — feed bytes, then reveal the fault.
1420    #[derive(Debug)]
1421    pub struct ScriptedReader {
1422        wire: Bytes,
1423        fault: Option<anyhow::Error>,
1424        /// This leg's index in its source's timeline, stamped when it finishes.
1425        leg: Option<usize>,
1426        /// Waited on before the first read (see `ScriptedSource::gated_on`);
1427        /// `None` once passed.
1428        gate: Option<tokio::sync::watch::Receiver<bool>>,
1429        /// Waited on by the first read the remaining wire cannot fill, so the
1430        /// parked fault is revealed only once it reads `true` (see
1431        /// `ScriptedSource::holding_fault`); `None` once passed.
1432        fault_gate: Option<tokio::sync::watch::Receiver<bool>>,
1433        /// The wire byte count this reader was handed (before consumption), used by
1434        /// `ScriptedSource`'s [`BlobSource::finish`](crate::BlobSource::finish) to advance a paying ledger by
1435        /// this leg's spend. A stopped pull pays for this less the unread wire.
1436        wire_len: u64,
1437        /// Shared with the parent [`ScriptedSource`]: bumped by the bytes each
1438        /// `read_bytes` actually yields, so a mid-stream drop stops counting the
1439        /// instant it happens.
1440        delivered: Arc<AtomicU64>,
1441        /// A one-time stall consumed on the first `read_bytes` (see
1442        /// `ScriptedSource::slow_to_start`); `None` after it fires once.
1443        first_read_stall: Option<Duration>,
1444        /// Wedge this reader once it has delivered the byte threshold (see
1445        /// [`ScriptedSource::stall_after`]); `None` after it fires once.
1446        stall_after: Option<(u64, Duration)>,
1447        /// Wire bytes this reader has yielded so far, against `stall_after`'s
1448        /// threshold.
1449        read_so_far: u64,
1450        /// See `ScriptedSource::throttled`.
1451        read_delay: Option<Duration>,
1452    }
1453
1454    impl ScriptedReader {
1455        /// Wait until the fault gate reads `true`, once (see `fault_gate`).
1456        async fn pass_fault_gate(&mut self) {
1457            if let Some(mut gate) = self.fault_gate.take() {
1458                // A closed gate channel reads as open.
1459                let _ = gate.wait_for(|open| *open).await;
1460            }
1461        }
1462    }
1463
1464    impl iroh_io::AsyncStreamReader for ScriptedReader {
1465        async fn read_bytes(&mut self, len: usize) -> std::io::Result<Bytes> {
1466            // A slow-to-start peer: sleep once before the first byte so a fast
1467            // peer reliably wins the race, finishes its own segment, and steals
1468            // this reader's tail — the deterministic steal trigger. The steal
1469            // lowers this reader's end, so it then delivers up to the split.
1470            if let Some(mut gate) = self.gate.take() {
1471                // A closed gate channel reads as open.
1472                let _ = gate.wait_for(|open| *open).await;
1473            }
1474            if let Some(stall) = self.first_read_stall.take() {
1475                tokio::time::sleep(stall).await;
1476            }
1477            if let Some(delay) = self.read_delay {
1478                tokio::time::sleep(delay).await;
1479            }
1480            // A source that delivered a prefix and then wedged: it holds the
1481            // connection open, returns no error, and simply stops. Only the
1482            // progress-relative watchdog can end this.
1483            if let Some((after, stall)) = self.stall_after
1484                && self.read_so_far >= after
1485            {
1486                self.stall_after = None;
1487                tokio::time::sleep(stall).await;
1488            }
1489            // Model a real network read's yield point. A synchronous in-memory
1490            // reader never pends, so under the cooperative single-thread runtime
1491            // the first-polled multi-source worker would drain every segment
1492            // before a peer worker is ever polled — starving the fan-out. One
1493            // yield per read lets concurrent workers interleave, exactly as I/O
1494            // waits would. Behavior-neutral for the single-source driver tests
1495            // (a yield only reschedules the same task).
1496            tokio::task::yield_now().await;
1497            if self.wire.len() < len {
1498                self.pass_fault_gate().await;
1499            }
1500            let take = self.wire.len().min(len);
1501            let chunk = self.wire.split_to(take);
1502            self.delivered
1503                .fetch_add(chunk.len() as u64, Ordering::SeqCst);
1504            self.read_so_far = self.read_so_far.saturating_add(chunk.len() as u64);
1505            Ok(chunk)
1506        }
1507
1508        async fn read<const L: usize>(&mut self) -> std::io::Result<[u8; L]> {
1509            if self.wire.len() < L {
1510                self.pass_fault_gate().await;
1511                return Err(std::io::Error::new(
1512                    std::io::ErrorKind::UnexpectedEof,
1513                    "scripted reader exhausted before a fixed-size bao read",
1514                ));
1515            }
1516            let got = self.wire.split_to(L);
1517            let mut out = [0u8; L];
1518            out.copy_from_slice(&got);
1519            Ok(out)
1520        }
1521    }
1522
1523    impl StashedFault for ScriptedReader {
1524        fn take_fault(&mut self) -> Option<anyhow::Error> {
1525            self.fault.take()
1526        }
1527    }
1528
1529    /// A fake [`Funder`](super::Funder): records the `remaining` deposit of each
1530    /// recovery step and answers with scripted [`Recovery`] outcomes, so the
1531    /// funding recovery step is testable without a chain handle.
1532    #[derive(Debug)]
1533    pub struct FakeFunder {
1534        /// The outcomes still to answer, in order. The last one repeats.
1535        script: Mutex<Vec<Recovery>>,
1536        calls: Mutex<Vec<U256>>,
1537        pool_spent: Option<U256>,
1538    }
1539
1540    impl FakeFunder {
1541        /// A funder whose every recovery step returns `outcome`.
1542        #[must_use]
1543        pub fn new(outcome: Recovery) -> Self {
1544            Self::scripted(vec![outcome])
1545        }
1546
1547        /// A funder that answers its recovery steps with `outcomes` in order,
1548        /// then repeats the last one. An empty script answers
1549        /// [`Recovery::Unavailable`].
1550        #[must_use]
1551        pub fn scripted(outcomes: Vec<Recovery>) -> Self {
1552            let mut script = outcomes;
1553            script.reverse();
1554            Self {
1555                script: Mutex::new(script),
1556                calls: Mutex::new(Vec::new()),
1557                pool_spent: None,
1558            }
1559        }
1560
1561        /// This funder, reporting `spent` as the pool's spend across every
1562        /// lane ([`Funder::pool_spent`](super::Funder::pool_spent)).
1563        #[must_use]
1564        pub const fn with_pool_spent(mut self, spent: U256) -> Self {
1565            self.pool_spent = Some(spent);
1566            self
1567        }
1568
1569        /// The `remaining` deposits passed to
1570        /// [`Funder::recover`](super::Funder::recover), in call order.
1571        #[must_use]
1572        pub fn calls(&self) -> Vec<U256> {
1573            self.calls.lock().map(|c| c.clone()).unwrap_or_default()
1574        }
1575    }
1576
1577    impl super::Funder for FakeFunder {
1578        fn recover(&self, remaining: U256) -> SourceFuture<'_, Recovery> {
1579            Box::pin(async move {
1580                if let Ok(mut calls) = self.calls.lock() {
1581                    calls.push(remaining);
1582                }
1583                let outcome = match self.script.lock() {
1584                    Ok(mut script) if script.len() > 1 => script.pop(),
1585                    Ok(script) => script.last().copied(),
1586                    Err(_) => None,
1587                };
1588                Ok(outcome.unwrap_or(Recovery::Unavailable))
1589            })
1590        }
1591
1592        fn pool_spent(&self) -> Option<U256> {
1593            self.pool_spent
1594        }
1595    }
1596}
1597
1598/// A store double for the record-flush tests of the driver and the scheduler.
1599#[cfg(test)]
1600mod flush_double;
1601
1602#[cfg(test)]
1603pub(crate) use flush_double::FlushCountingStore;
1604
1605#[cfg(any(test, feature = "test-util"))]
1606pub use doubles::{FakeFunder, ScriptedReader, ScriptedSource};
1607
1608#[cfg(test)]
1609pub(crate) use doubles::ctx_with;
1610
1611#[cfg(test)]
1612#[allow(clippy::unwrap_used, clippy::cast_possible_truncation)] // tests
1613mod tests;