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;