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;