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