Skip to main content

decdn_client/
lib.rs

1//! The deCDN client: fetch content-addressed blobs from deCDN nodes, pay for
2//! them per megabyte from a USDC payment pool, and verify every byte against
3//! its BLAKE3 hash as it arrives.
4//!
5//! The `decdn` CLI's `fetch` and `bundle pull` are built on this crate. So is
6//! the node's own cache-miss pull: a node that misses in its cache is a paying
7//! client of the node upstream of it.
8//!
9//! # Choose an entry point
10//!
11//! | You want to | Use |
12//! |---|---|
13//! | Save one blob, or a set of blobs, to files as fast as possible, and resume a partial download | [`Downloader`] |
14//! | Read one blob in order, and pay only for what your reader reaches | [`Streamer`] |
15//!
16//! The [`Downloader`] stripes a blob
17//! across every holder at once, and writes each verified range at its offset in
18//! a `.partial` file beside the destination. A rerun fetches only the ranges
19//! that file does not hold yet. The [`Streamer`] fetches at most one read-ahead
20//! window ([`PullConfig::read_ahead_bytes`]) ahead of its reader, so a reader
21//! that stops early stops the spend within that window.
22//!
23//! # Before you fetch
24//!
25//! A fetch needs a funded payment pool, an iroh endpoint, and one paid lane per
26//! node that holds the blob. The `download` and `stream` examples in this
27//! crate's `examples/` directory run the whole sequence:
28//!
29//! 1. **Load the buyer key.** Vouchers and pool transactions are signed with a
30//!    [`PrivateKeySigner`].
31//! 2. **Reuse or open a pool.** Read the pool the buyer store already records
32//!    ([`decdn_incentive::BuyerPoolStore::get_by_owner`]). If there is none,
33//!    approve the deposit ([`buyer_pool::ensure_allowance`]), open a pool
34//!    ([`buyer_pool::open_pool`]), and record the returned state at once: the
35//!    deposit is escrowed from the moment `open_pool` returns.
36//! 3. **Bring up an endpoint** with [`endpoint::client_endpoint`].
37//! 4. **Find the holders.** Read the registered nodes
38//!    ([`discovery::bootstrap_nodes`]), pick a sample
39//!    ([`discovery::select_candidates`]), and probe each one
40//!    ([`probe::probe_once`]). Verify every answer
41//!    ([`probe::verify_probe_response`]) before it counts, and drop an answer
42//!    whose `has_blob` and coverage disagree. A probe answer's size
43//!    ([`discovery::Probed::total_bytes`]) is an unsigned hint: pass it as the
44//!    fetch's first size claim, which the fetch grows or shrinks as verified
45//!    bytes land.
46//! 5. **Build one lane per holder.** Pin a [`PoolContext`] to the holder's
47//!    provider address with [`buyer_pool::self_owned_lane_ctx`], resuming at
48//!    what the lane has already paid, and bind it to your endpoint
49//!    ([`PoolContext::bind_endpoint`]). A node that misses the blob in its
50//!    cache serves an unbound lane nothing. Create its ledger with
51//!    [`PoolContext::new_ledger`], and wrap both in a [`PeerSource`] inside a
52//!    [`StreamCandidate::new`]. Cap the lane's rate at the rate the node signed
53//!    in its probe answer ([`effective_rate_ceiling`]).
54//! 6. **Fetch** with [`Downloader::fetch_to_paths`] or [`Streamer::open`], over
55//!    a [`StaticSources`] of the lanes. Every lane streams at once unless
56//!    [`Downloader::max_lanes`] caps it.
57//! 7. **Record what each lane paid**, whatever the outcome: build a
58//!    [`VoucherProgress`] from the ledger's settlement, and apply the
59//!    [`buyer_pool::ProgressWrite`] it calls for to the buyer store.
60//!
61//! # What the crate guarantees
62//!
63//! - **Every byte is verified.** Delivery is BLAKE3/bao verified streaming.
64//!   The decoder checks each chunk group against the hash as it lands, so a
65//!   corrupt range aborts the pull at that group. Nothing unverified reaches a
66//!   [`VerifiedReader`] or a finished file.
67//! - **You pay only for verified bytes.** You pay per chunk
68//!   ([`decdn_protocol::CHUNK_BYTES`]) once the decoder has verified it. A
69//!   lying node is paid for the bytes before its first bad group and for
70//!   nothing after, never for the size it claimed.
71//! - **Quotes are checked against your ceiling.** A stream response whose
72//!   signed rate exceeds the `max_rate_per_mb` given to [`PeerSource::new`] is
73//!   refused before any payment ([`RateAboveCeiling`]).
74//! - **Recovery is free.** Every lane draws on one shared pool. A source that
75//!   faults cools and returns, and its range goes to another holder meanwhile.
76//!   A fetch ends on done, a fatal fault, or the stop policy
77//!   ([`StopPolicy`]). No delivered byte is fetched or paid for twice.
78//!
79//! # What stays yours
80//!
81//! - **Persistence.** The faces never write the buyer store. Record every
82//!   lane's payment after each fetch (step 7). A lane you do not record resumes
83//!   from a stale watermark next time, and its provider rejects the vouchers.
84//! - **Funding policy.** A source whose next voucher the deposit cannot cover,
85//!   or that refuses the pool as `Unfunded`, waits for the deposit to rise.
86//!   Once no other source serves and a fresh discovery finds nothing new, the
87//!   fetch runs one funding recovery step through your [`Funder`]
88//!   ([`Funder::recover`]): it may top the pool up, open a new pool
89//!   ([`PoolReplaced`]), or report that it has no way to add funds. A further
90//!   step needs a newly verified byte since the last one. When no step is
91//!   allowed or possible, the fetch fails with a [`NoAffordableSource`],
92//!   "funding needed".
93//! - **Delegated credentials.** A delegate that pays under someone else's
94//!   pool hands the face a [`CredentialSlot`] and watches its
95//!   [`FundingEvent`]s. Swap a new key and capability into the slot when the
96//!   capability runs low: the fetch retires the old lanes at a voucher
97//!   boundary and goes on under the new key. Without a swap, a delegated
98//!   fetch ends with a typed [`FundingNeeded`].
99//! - **Which holders to use.** Discovery gives candidates; ordering and
100//!   admission ([`discovery::admit_sources`]) are the caller's choice.
101//!
102//! # Mistakes to avoid
103//!
104//! - **One candidate per provider.** Each [`StreamCandidate`] pays one
105//!   `(signer, provider)` lane. Two candidates for one provider sign competing
106//!   vouchers on the same lane.
107//! - **Always set a rate ceiling.** A `max_rate_per_mb` of `0` means no
108//!   ceiling, so a node can quote low on the probe and high on the stream.
109//!   Pass [`effective_rate_ceiling`] of the probed rate and any absolute cap you
110//!   hold.
111//! - **Never resume a lane below its on-chain watermark.** A voucher at or below
112//!   the watermark redeems nothing, so the provider streams bytes it can never
113//!   cash. A lane the buyer store has not seen resumes from the chain's
114//!   `getWatermark`.
115//! - **Keep the stream drive running.** [`Streamer::open`] returns a reader and
116//!   a [`StreamDrive`]. Run your read inside [`StreamDrive::alongside`]: the
117//!   drive pays and drains open legs, and it must not wait behind a blocked
118//!   consumer.
119//! - **Progress is in content bytes.** A [`ProgressCallback`] on the faces
120//!   reports verified content bytes against the blob's size.
121//!
122//! # How delivery works
123//!
124//! Each lane sends a [`StreamRequest`], verifies the signed [`StreamResponse`],
125//! and then reads `ChunkData` and pays as it goes. It releases one hash-chain
126//! preimage per delivered `CHUNK_BYTES` chunk, and signs a voucher to open a
127//! chain, to roll one, and to settle a residual shorter than a chunk.
128//!
129//! The `ChunkData` payload is bao's interleaved verified-stream encoding, not
130//! raw bytes. Every consumer feeds it to a `bao-tree` verifying decoder as it
131//! arrives. The decoder checks every chunk group against the requested root. A
132//! range fetched from any offset therefore verifies on its own, with no
133//! dependency on earlier bytes (ADR 038).
134//!
135//! # Stability
136//!
137//! The documented items are the SDK, and the public-API snapshot in
138//! `tests/public_api.rs` guards them. Their enums and the error types a fetch
139//! returns are `#[non_exhaustive]`, so a new variant or field is not a
140//! breaking change. Items hidden from these docs are the pull engine's seams
141//! that the node and the `decdn` CLI drive directly. They carry no stability
142//! promise.
143
144/// Buyer-side `PaymentPool` open kernel (#940), shared by the node service
145/// and the CLI.
146pub mod buyer_pool;
147/// Zero-config tunables for the consumption faces over the paid pull engine
148/// ([`config::PullConfig`], #1848): every knob defaults, so a caller overrides
149/// only what it must, and construction needs no network or chain access.
150pub mod config;
151/// A caller-owned QUIC connection kept warm across many hash fetches
152/// ([`connection::WarmConnection`]): one dial amortized over every hash, one
153/// bi-stream per hash (no wire change), closed once on the handle's own `Drop`;
154/// and the command-wide map of one such connection per node
155/// ([`connection::Connections`]).
156pub mod connection;
157/// Range-keyed discovery coverage-map primitive plus the two
158/// objective-specific planners over it (#1506): [`coverage_plan::plan_covered_runs`]
159/// (node — concentrate + sticky) and `coverage_plan::spread_segments` (client —
160/// spread for parallelism). Pure, no I/O.
161pub(crate) mod coverage_plan;
162/// A delegated fetch's voucher credential, which the application swaps when
163/// the capability runs low, and the funding signal that says when.
164pub mod credential;
165/// Client-side node discovery (#936): read + select the active node set from
166/// `CapacityBond.getRegisteredNodes`, then rank probed blob-holders.
167pub mod discovery;
168/// The `Downloader` consumption face (#1848): fetch a set of content-addressed
169/// blobs (a bundle, or a single blob) to files in a directory, out-of-order and
170/// at full throughput, so every byte is bao-verified and a resumed fetch
171/// re-pulls only the missing ranges.
172pub mod downloader;
173/// The #1608 gap-driven fetch driver: [`driver::drive`] fills only the
174/// [`missing_ranges`](decdn_bao_range::RangedStore::missing_ranges) of a request,
175/// paying the minimum, by folding the resume / reseed loop behind the
176/// [`pacer::Pacer`] axis.
177pub mod driver;
178/// One-shot client `Endpoint` construction: relay + discovery resolution for
179/// the `cdn/client/v1` and `cdn/probe/v1` dial paths (#935/#936).
180pub mod endpoint;
181/// What a failed lane, lane build, or discovery means for the acquire loop
182/// (ADR 039 § Failure handling: reassign-only tail): a pure classifier over
183/// `anyhow::Error`.
184#[doc(hidden)]
185pub mod fault;
186mod first_open;
187/// Command-wide health of each provider (ADR 039 § Failure handling:
188/// reassign-only tail): cooling backoff on a delivery fault, parking on an
189/// unaffordable price.
190#[doc(hidden)]
191pub mod health;
192mod ledger;
193/// Run-scoped registry of live per-lane voucher ledgers:
194/// [`ledgers::LaneLedgers`] maps each `(pool_id, signer, provider)` lane to
195/// the one [`ledgers::LaneHandle`] every concurrent fetch on that lane shares.
196#[doc(hidden)]
197pub mod ledgers;
198/// The pure pacing axis (#1608): [`pacer::Pacer`] / [`pacer::BudgetPacer`] decide
199/// draw / wait / done / refuse for the gap-driven driver, with no I/O.
200#[doc(hidden)]
201pub mod pacer;
202/// Persisted per-peer knowledge base: registry-fed identity plus interaction-fed
203/// latency and price, keyed by iroh [`iroh::PublicKey`], one JSON file per peer.
204#[doc(hidden)]
205pub mod peer_store;
206/// Reusable `cdn/probe/v1` client.
207pub mod probe;
208/// Sub-frame byte-progress observation (#1797): a `ProgressReader` that tallies bytes
209/// off the QUIC stream beneath the message decode, and the `ThroughputFloor` that judges
210/// those bytes against a minimum rate over a trailing window.
211mod progress;
212/// Wallet-filled HTTP provider builder for opening/settling payment channels.
213pub mod provider;
214/// Client-side [`decdn_bao_range::RangedStore`] backend (#1621): a
215/// `.partial` + sidecar store built on `bao-tree`/`decdn-bao-range` only.
216#[doc(hidden)]
217pub mod ranged_store;
218/// The `APP_ERR_RATE_LIMITED` (`0x10`) transport shed, typed for the pull
219/// orchestrator (ADR 013 §Application Error Codes).
220pub(crate) mod rate_limited;
221/// The funding recovery step at the exhausted candidate set (ADR 003 § Funding
222/// recovery): the per-fetch [`recovery::RecoveryGate`] and its progress rule.
223pub mod recovery;
224mod scheduler;
225/// Pure segmentation and tail-steal helpers for the multi-source scheduler
226/// (ADR 039 § Dynamic segmentation and tail-stealing): no I/O, no async.
227mod segment;
228/// When a command gives up (ADR 039 § Failure handling: reassign-only tail):
229/// the progress clock and the stop policy that watches it.
230pub mod stop;
231/// The `Streamer` consumption face (#1848): stream one blob's verified,
232/// contiguous front to a consumer as it arrives, paced by consumption and bounded
233/// to one read-ahead window ahead of the read cursor. A fetch-like single-blob
234/// face over the same engine, with no chunk dedup.
235pub mod streamer;
236// Docs live in `sink.rs` as `//!`. Deliberately NOT documented here as well:
237// rustdoc resolves intra-doc links on a `mod` item in THIS file's scope, so the
238// module's own links (`content_paid_frontier`, …) would go unresolved
239// and fail the doc gate (`CARGO_BUILD_WARNINGS=deny`).
240pub mod sink;
241/// The sourcing and funding seams of a fetch (#1608): [`source::BlobSource`]
242/// (raw-bao byte source for a range) and [`source::Funder`] (the injected
243/// funding recovery seam), plus scripted test doubles.
244pub mod source;
245/// The sources of one blob (ADR 039 § Source set and selection):
246/// [`source_set::SourceSet`] tracks
247/// each holder's health, lane, and retry timing behind the injected
248/// [`source_set::SourceProvider`] seam.
249pub mod source_set;
250
251// The SDK surface: the two faces, the setup sequence the crate guide walks
252// through, and every type their signatures name.
253pub use config::PullConfig;
254pub use connection::Connections;
255pub use credential::{
256    CREDENTIAL_SWAP_WAIT, CapabilityCause, Credential, CredentialMismatch, CredentialSlot,
257    FundingEvent, FundingNeeded,
258};
259pub use downloader::{DownloadTarget, Downloader};
260pub use driver::PoolExhausted;
261pub use ledger::{Cumulative, PoolLedger};
262pub use recovery::RecoveryGate;
263pub use sink::{BlobCache, NoCache, SinkFuture};
264pub use source::{Funder, PeerSource, PoolReplaced, Recovery, SourceFuture};
265pub use source_set::{Holder, NoAffordableSource, NoNodeWillServe, NoSourceHasBlob, StaticSources};
266pub use stop::{ClockHold, GaveUp, ProgressClock, StopPolicy};
267pub use streamer::{LiveReader, StreamCandidate, StreamDrive, Streamer, VerifiedReader};
268
269// The engine seams the node and the `decdn` CLI drive directly. They are
270// public so those crates can reach them, hidden from the docs and the
271// public-API snapshot, and carry no stability promise.
272#[doc(hidden)]
273pub use connection::WarmConnection;
274#[doc(hidden)]
275pub use coverage_plan::{CoveredRun, SourceCoverage, plan_covered_runs};
276#[doc(hidden)]
277pub use credential::QuoteMax;
278#[doc(hidden)]
279pub use decdn_bao_range::RangedStore;
280#[doc(hidden)]
281pub use driver::{LegNoProgress, PacingWait, SharedPool, WaitReason, drive, first_leg};
282#[doc(hidden)]
283pub use fault::{FatalScope, Fault, HealExhausted, LaneBuildFault, classify};
284#[doc(hidden)]
285pub use first_open::first_open;
286#[doc(hidden)]
287pub use health::{Health, PeerHealth};
288#[doc(hidden)]
289pub use ledger::{ChainCommit, EpochAction, Metered, Rebase, Released};
290#[doc(hidden)]
291pub use ledgers::{LaneHandle, LaneLedgers};
292#[doc(hidden)]
293pub use pacer::{
294    BudgetPacer, DownstreamFrontier, PULL_WINDOW_FLOOR, PaceDecision, PaceState, Pacer, RampPacer,
295    VERIFY_LAG_BYTES, WindowPacer,
296};
297#[doc(hidden)]
298pub use peer_store::{PeerRecord, PeerStore, StoreConfig};
299#[doc(hidden)]
300pub use ranged_store::ClientRangedStore;
301#[doc(hidden)]
302pub use rate_limited::UpstreamRateLimited;
303#[doc(hidden)]
304pub use recovery::{RECOVERY_SETTLE, Stepped};
305#[doc(hidden)]
306pub use scheduler::{
307    AcquireEnv, AcquireTarget, ConsumptionPacing, GROWTH_RETRY, GrowFor, LANE_WATCHDOG, LaneLease,
308    LaneWiden, acquire,
309};
310#[doc(hidden)]
311pub use source::{
312    BaoRangeReader, BlobSource, IngestEnd, IngestFuture, IngestStore, PrimedSource, SourceStream,
313};
314#[doc(hidden)]
315pub use source_set::{LaneRange, SourceProvider, SourceSet};
316
317pub(crate) use ledger::StreamProof;
318
319#[cfg(any(test, feature = "test-util"))]
320pub use source::{FakeFunder, ScriptedReader, ScriptedSource};
321
322use std::sync::Arc;
323use std::time::Duration;
324
325use alloy::dyn_abi::Eip712Domain;
326use alloy::primitives::{Address, B256, Signature, U256};
327use alloy::signers::SignerSync;
328use alloy::signers::local::PrivateKeySigner;
329#[cfg(any(test, feature = "test-util"))]
330use bao_tree::BaoTree;
331#[cfg(any(test, feature = "test-util"))]
332use bao_tree::io::BaoContentItem;
333#[cfg(any(test, feature = "test-util"))]
334use bao_tree::io::fsm::{ResponseDecoder, ResponseDecoderNext};
335use bytes::Bytes;
336#[cfg(any(test, feature = "test-util"))]
337use decdn_bao_range::align_range;
338use decdn_bao_range::align_range_clamped;
339#[cfg(any(test, feature = "test-util"))]
340use decdn_bao_range::{AlignedRange, IROH_BLOCK_SIZE};
341use decdn_incentive::{
342    BuyerPoolState, EPHEMERAL_BINDING_NONCE, SignedCapability, SignedVoucher, StreamSlashData,
343    Voucher, binding_signing_hash, signed_to_wire_voucher,
344};
345use decdn_protocol::client::{
346    ClientBinding, ClientMessage, StreamError, StreamRequest, StreamRequestExt, StreamResponse,
347    StreamResponseExt, VoucherRejectReason, WatermarkBundle, WireCapability,
348};
349use decdn_protocol::{
350    CHUNK_BYTES, ChunkPreimage, decode_message, encode_message, read_frame, write_frame,
351};
352use iroh::endpoint::{RecvStream, SendStream};
353use iroh::{Endpoint, EndpointAddr};
354#[cfg(any(test, feature = "test-util"))]
355use sink::{PullReader, StashedFault};
356
357/// Per-lane context the requester needs to sign pool vouchers.
358///
359/// A voucher is scoped to a `(pool_id, signer, provider)` lane. `pool_id` is the
360/// on-chain deposit, the signer is `client_signer.address()` (the capability key
361/// the pool owner delegated spend to — the owner's own key in the single-user
362/// case), and `provider` is the delivering node this stream pays.
363///
364/// The `prior_*` fields capture the lane's cumulative state from earlier streams
365/// so a **reused** lane resumes correctly: vouchers are cumulative across the
366/// lane's lifetime, so the node's `last_bytes_delivered` / `last_amount` are
367/// non-zero after the first stream. Starting a fresh stream from zero would be
368/// rejected (`AmountRegression` / `BytesRegression`). For a brand-new lane pass
369/// `U256::ZERO` for both.
370///
371/// Build one with [`Self::new`] or [`Self::for_pool`], then pin it with
372/// [`Self::with_provider`] and bind it with [`Self::bind_endpoint`].
373#[derive(Clone)]
374#[cfg_attr(not(feature = "test-util"), non_exhaustive)]
375pub struct PoolContext {
376    /// On-chain `poolId` the vouchers draw from.
377    pub pool_id: B256,
378    /// The delivering node this stream pays — the voucher's `provider` field. A
379    /// capability voucher is scoped to one provider and is invalid if redeemed
380    /// against another.
381    pub provider: Address,
382    /// On-chain pool deposit (informational here; the node enforces it).
383    pub deposit: U256,
384    /// Client key that signs vouchers — its address is the lane's capability
385    /// `signer`.
386    pub client_signer: Arc<PrivateKeySigner>,
387    /// `PaymentPool` EIP-712 domain.
388    pub voucher_domain: Eip712Domain,
389    /// Cumulative bytes paid for on this lane before this stream.
390    pub prior_bytes_delivered: U256,
391    /// Cumulative amount paid on this lane before this stream.
392    pub prior_amount: U256,
393    /// Optional ADR 005 client identity binding (address + `BindNodeId`
394    /// signature over the requester's own iroh `NodeId`, see
395    /// [`sign_client_binding`]). Attached to every `cdn/client/v1` request's
396    /// `ext` so the serving node can recover the buyer address, confirm it owns
397    /// the pool (`pull_authorized`), and reactively populate from its configured
398    /// origin (#1115). `None` ⇒ no binding is sent (an unconfigured
399    /// `capacity_bond` on the client, or an on-chain/registered requester).
400    pub client_binding: Option<ClientBinding>,
401    /// Optional pool owner capability delegating spend to `client_signer`
402    /// (ADR 003 §Capability delegation, `issue_self_capability` /
403    /// `buyer_pool::open_pool`). Attached to every `cdn/client/v1` request's
404    /// `ext` alongside `client_binding` so the serving node can register the
405    /// signer on that signer's first on-chain redemption. Node-agnostic — the
406    /// same `SignedCapability` is valid at every node this context streams
407    /// from, since it grants spend against the pool rather than a specific
408    /// provider. `None` ⇒ no capability is sent (the signer is already
409    /// registered on-chain, or this stream reuses a lane a prior stream this
410    /// session already delivered the capability for).
411    pub capability: Option<SignedCapability>,
412}
413
414impl PoolContext {
415    /// A context that signs vouchers on `pool_id` with `client_signer` under
416    /// `voucher_domain`, against a deposit of `deposit`. It pays no provider
417    /// yet, starts its lane at zero, and carries no binding or capability:
418    /// [`Self::with_provider`], [`Self::bind_endpoint`] and
419    /// [`Self::with_capability`] add each. A delegated signer, which holds no
420    /// pool row of its own, starts here.
421    #[must_use]
422    pub const fn new(
423        pool_id: B256,
424        deposit: U256,
425        client_signer: Arc<PrivateKeySigner>,
426        voucher_domain: Eip712Domain,
427    ) -> Self {
428        Self {
429            pool_id,
430            // A pool fans out to many providers; the fetch target is pinned
431            // per-pull via [`Self::with_provider`], not stored in the pool.
432            provider: Address::ZERO,
433            deposit,
434            client_signer,
435            voucher_domain,
436            prior_bytes_delivered: U256::ZERO,
437            prior_amount: U256::ZERO,
438            client_binding: None,
439            capability: None,
440        }
441    }
442
443    /// [`Self::new`] for a pool the buyer store records: its `pool_id` and
444    /// committed deposit.
445    #[must_use]
446    pub const fn for_pool(
447        state: &BuyerPoolState,
448        client_signer: Arc<PrivateKeySigner>,
449        voucher_domain: Eip712Domain,
450    ) -> Self {
451        Self::new(state.pool_id, state.deposit, client_signer, voucher_domain)
452    }
453
454    /// Pin the delivering node this context pays (the voucher's `provider`) and
455    /// seed the lane's prior cumulative totals. A pool serves many providers, so
456    /// the target is chosen per-fetch rather than at pool open.
457    #[must_use]
458    pub const fn with_provider(
459        mut self,
460        provider: Address,
461        prior_bytes_delivered: U256,
462        prior_amount: U256,
463    ) -> Self {
464        self.provider = provider;
465        self.prior_bytes_delivered = prior_bytes_delivered;
466        self.prior_amount = prior_amount;
467        self
468    }
469
470    /// This context's `(pool_id, signer, provider)` lane — the scope of one
471    /// hash chain, and the key every seed is derived under.
472    #[must_use]
473    pub fn lane(&self) -> decdn_incentive::LaneKey {
474        decdn_incentive::LaneKey {
475            pool_id: self.pool_id,
476            signer: self.client_signer.address(),
477            provider: self.provider,
478        }
479    }
480
481    /// The ledger for this lane, seeded from its persisted cumulative and
482    /// nothing else.
483    ///
484    /// A chain is never resumed across a restart — the resume folds the
485    /// frontier the node proved into a signed amount and opens a fresh chain
486    /// (ADR 003 §Resumption folds) — so there is no chain secret to reproduce
487    /// here, no counter to read back, and nothing that could have been
488    /// persisted wrong. What carries across an invocation is the money owed.
489    #[must_use]
490    pub fn new_ledger(&self) -> PoolLedger {
491        PoolLedger::new(Cumulative {
492            bytes: self.prior_bytes_delivered,
493            amount: self.prior_amount,
494        })
495    }
496
497    /// Attach an ADR 005 client identity binding so this context's
498    /// `cdn/client/v1` requests prove pool ownership to the serving node,
499    /// enabling reactive cache-miss origin pull-through (#1115).
500    /// [`Self::bind_endpoint`] signs and attaches one in a single call. A
501    /// hand-built `ClientBinding` whose
502    /// `ethereum_address` and signature don't correspond (or that doesn't own the
503    /// pool) is rejected by the serving node, so this only ever hurts the caller
504    /// itself.
505    #[must_use]
506    pub fn with_client_binding(mut self, binding: ClientBinding) -> Self {
507        self.client_binding = Some(binding);
508        self
509    }
510
511    /// Bind `endpoint` to this context's signer, so a node that misses the blob
512    /// in its cache may pull it from its origin for this buyer. Without a
513    /// binding, such a node refuses the open as `NotFound`. `bind_domain` is
514    /// `decdn_incentive::bind_node_id_domain(chain_id, capacity_bond)`.
515    ///
516    /// # Errors
517    ///
518    /// A signing error from `client_signer`.
519    pub fn bind_endpoint(
520        self,
521        endpoint: &Endpoint,
522        bind_domain: &Eip712Domain,
523    ) -> anyhow::Result<Self> {
524        let own_node_id = B256::from(*endpoint.id().as_bytes());
525        let binding = sign_client_binding(&self.client_signer, own_node_id, bind_domain)?;
526        Ok(self.with_client_binding(binding))
527    }
528
529    /// Attach a pool owner capability so this context's `cdn/client/v1`
530    /// requests carry it to the serving node at session start, letting the
531    /// node register `client_signer` on that signer's first on-chain
532    /// redemption (ADR 003 §Capability delegation). Pass the
533    /// `SignedCapability` produced by `buyer_pool::open_pool` /
534    /// `issue_self_capability`; a capability whose `signer` doesn't match
535    /// `client_signer` or that fails owner-signature recovery only ever hurts
536    /// the caller itself, exactly like [`Self::with_client_binding`].
537    #[must_use]
538    pub const fn with_capability(mut self, capability: SignedCapability) -> Self {
539        self.capability = Some(capability);
540        self
541    }
542}
543
544/// Sign an ADR 005 ephemeral client identity binding: an EIP-712
545/// `BindNodeId(nodeId, nonce = 0)` attestation over the requester's OWN iroh
546/// `NodeId`, signed with the buyer key. The serving node recovers the signer via
547/// `ecrecover` (`verify_binding`) and checks it owns the named channel before
548/// honoring a cache-miss origin pull (`pull_authorized`, ADR 003 §Off-Chain
549/// Ephemeral Binding). Used by the CLI client fetch (#1115) and by node-to-node
550/// pulls, where `node_origin` binds its upstream requests so an upstream can
551/// chain a further reactive origin pull (#1117).
552///
553/// `own_node_id` MUST be the requester's own endpoint `NodeId` — what the peer
554/// authenticates as `conn.remote_id()` — NOT the target node's. `bind_domain` is
555/// `decdn_incentive::bind_node_id_domain(chain_id, capacity_bond)`.
556///
557/// # Errors
558///
559/// Propagates a signing error from the buyer signer.
560#[doc(hidden)]
561pub fn sign_client_binding(
562    signer: &PrivateKeySigner,
563    own_node_id: B256,
564    bind_domain: &Eip712Domain,
565) -> anyhow::Result<ClientBinding> {
566    let hash = binding_signing_hash(own_node_id, EPHEMERAL_BINDING_NONCE, bind_domain);
567    let binding_signature = signer
568        .sign_hash_sync(&hash)
569        .map_err(|e| anyhow::anyhow!("sign client binding: {e}").context(LocalPullFault))?
570        .as_bytes()
571        .to_vec();
572    Ok(ClientBinding {
573        ethereum_address: signer.address().into(),
574        binding_signature,
575    })
576}
577
578/// Build the trailing [`StreamRequestExt`] carrying the context's client
579/// identity binding and/or owner capability, or `None` when the context
580/// carries neither — in which case `encode_stream_request` appends no ext
581/// bytes. Applied by `open_stream`, the single request site, so the capability
582/// rides the same session-start request as the binding.
583fn client_binding_ext(ctx: &PoolContext) -> Option<StreamRequestExt> {
584    if ctx.client_binding.is_none() && ctx.capability.is_none() {
585        return None;
586    }
587    Some(StreamRequestExt {
588        binding: ctx.client_binding.clone(),
589        capability: ctx.capability.as_ref().map(|signed| WireCapability {
590            spending_cap: signed.capability.spending_cap,
591            expiry: signed.capability.expiry,
592            owner_signature: signed.signature.as_bytes().to_vec(),
593        }),
594    })
595}
596
597impl std::fmt::Debug for PoolContext {
598    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
599        f.debug_struct("PoolContext")
600            .field("pool_id", &self.pool_id)
601            .field("provider", &self.provider)
602            .field("deposit", &self.deposit)
603            .finish_non_exhaustive()
604    }
605}
606
607/// The lane's committed voucher watermark, returned by [`UpstreamPull::finish`] /
608/// [`UpstreamPull::abort`] / [`UpstreamPull::progress`] and threaded through the
609/// `test-util` `stream_fetch_tracked` wrapper as an out-param, so the caller can
610/// persist what it paid (#852).
611///
612/// A pull runs through a [`PoolLedger`] seeded from the lane's prior cumulative
613/// state; this watermark is that ledger read back
614/// ([`VoucherProgress::from_ledger`]) — so it
615/// always holds the **absolute** cumulative totals of the last presumed-accepted
616/// voucher (not per-stream deltas), exactly the `(bytes_delivered, amount)` pair
617/// the buyer-pool lane record expects. Because the
618/// copy-back runs on every return path (including the `Err`/timeout arms), the
619/// latest totals survive a mid-stream failure or a paid-but-corrupt delivery.
620///
621/// **Implicit acceptance.** The watermark tracks vouchers the upstream is
622/// presumed to have accepted — continued delivery is acceptance (ADR 005), so a
623/// sent voucher advances it and only an explicit `VoucherRejected` rewinds it.
624///
625/// Read the persistable totals via [`VoucherProgress::advanced`], which yields
626/// `None` when nothing was paid on this stream (so there is nothing to persist).
627#[derive(Clone, Copy, Debug, Default)]
628pub struct VoucherProgress {
629    /// Cumulative lane bytes paid for as of the last committed voucher.
630    bytes_delivered: U256,
631    /// Cumulative lane amount paid as of the last committed voucher.
632    amount: U256,
633    /// Whether the watermark moved past the lane's seed on this stream.
634    advanced: bool,
635    /// The node's watermark the lane's ledger rebased to, behind it on
636    /// `amount` ([`PoolLedger::rebase`]), and no persist has recorded yet. The caller
637    /// overwrites the lane record with it once, then advances to the totals
638    /// above.
639    rebase_anchor: Option<Cumulative>,
640}
641
642impl VoucherProgress {
643    /// Build the watermark from a ledger [`Cumulative`] plus the lane's seed
644    /// amount. `advanced` is set when the cumulative `amount` rose past the seed,
645    /// preserving the "something was paid iff the watermark advanced" contract
646    /// even when the ledger was shared across concurrent streams.
647    ///
648    /// Public because a caller that owns its ledger persists from it directly
649    /// rather than through the `&mut VoucherProgress` out-param — including from a
650    /// `Drop`, where nothing can be awaited and [`PoolLedger::committed`] is the
651    /// only readable source (#1145 review).
652    #[must_use]
653    pub fn from_cumulative(cum: Cumulative, prior_amount: U256) -> Self {
654        Self {
655            bytes_delivered: cum.bytes,
656            amount: cum.amount,
657            advanced: cum.amount > prior_amount,
658            rebase_anchor: None,
659        }
660    }
661
662    /// Attach the watermark the ledger rebased DOWN to and has not persisted
663    /// yet ([`PoolLedger::take_unsaved_rebase`]).
664    #[must_use]
665    pub const fn with_rebase_anchor(mut self, anchor: Option<Cumulative>) -> Self {
666        self.rebase_anchor = anchor;
667        self
668    }
669
670    /// The watermark to overwrite the lane record with before advancing, when
671    /// the ledger rebased down to the node's and no persist has recorded it
672    /// yet. A monotone advance refuses a lower watermark, so without the
673    /// overwrite the next run would sign from the anchor the node refused.
674    #[must_use]
675    pub const fn rebase_anchor(&self) -> Option<Cumulative> {
676        self.rebase_anchor
677    }
678
679    /// The same watermark, read from a live ledger.
680    ///
681    /// Prefer this wherever a ledger is in hand: it reads
682    /// [`PoolLedger::settlement`] rather than a copied-back cumulative, so a
683    /// voucher left armed by an ambiguous send is still reported as owed.
684    ///
685    /// It also TAKES the ledger's unsaved rebase anchor, so call it only where
686    /// the result is persisted: the anchor is handed out once.
687    #[must_use]
688    pub fn from_ledger(ledger: &PoolLedger, prior_amount: U256) -> Self {
689        // Take the anchor first: every settlement read after it is at or above it.
690        let anchor = ledger.take_unsaved_rebase();
691        Self::from_cumulative(ledger.settlement(), prior_amount).with_rebase_anchor(anchor)
692    }
693
694    /// The cumulative `(bytes_delivered, amount)` to persist via the lane record,
695    /// or `None` if nothing new was paid on this stream (nothing to record).
696    #[must_use]
697    pub fn advanced(&self) -> Option<(U256, U256)> {
698        self.advanced.then_some((self.bytes_delivered, self.amount))
699    }
700
701    /// The cumulative `(bytes_delivered, amount)` this watermark carries,
702    /// whether or not it advanced past the seed.
703    #[must_use]
704    pub const fn totals(&self) -> (U256, U256) {
705        (self.bytes_delivered, self.amount)
706    }
707}
708
709/// The upstream delivered bytes that failed bao verification against the
710/// requested content root — a paid-but-corrupt delivery (the content-addressing
711/// invariant, ADR 014/038). Under ADR 038 the verifier is the per-chunk-group
712/// `bao-tree` decoder fed as bytes arrive, not a whole-blob re-hash, so this
713/// fires the moment any group's proof mismatches. Returned (via `anyhow`) by
714/// this crate's decoding consumers — [`ClientRangedStore::ingest_stream`] and the
715/// `test-util` in-memory decoder — so callers can `downcast_ref` to classify corruption
716/// (e.g. a reputation `Corruption` outcome) without matching on the error
717/// message string. The `Display` text is kept stable for logs and the existing
718/// requester tests. The node's cache admit reports the same fault as
719/// `CacheError::VerifyFailed` / `CacheError::HashMismatch`, which `decdn-node`'s
720/// `is_bao_corruption` folds together with this type.
721#[derive(Debug)]
722pub struct HashMismatch;
723
724impl std::fmt::Display for HashMismatch {
725    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
726        f.write_str("received bytes do not match requested hash")
727    }
728}
729
730impl std::error::Error for HashMismatch {}
731
732/// Reject a `total_bytes == 0` claim for a non-empty root as the typed
733/// [`HashMismatch`] (#1054).
734///
735/// A 0-byte blob aligns to an empty range: the bao decoder has no chunk group to
736/// anchor and would accept the empty stream for ANY root, and a store sized from a
737/// `total_bytes == 0` claim has no gap to pull and is complete as created — so
738/// nothing downstream ever verifies the empty stream against the root. The only
739/// hash an empty blob can carry is the empty root; a source that claims `0` for any
740/// other hash is a paid-but-wrong delivery. This is the one explicit verify site
741/// the trivial-empty-range bypass has, shared by [`driver::drive`] (before the
742/// empty store is finalized) and the in-memory `fetch_in_memory_once` path so the
743/// two cannot drift. A `total_bytes > 0` claim is untouched: the streaming decoder
744/// verifies it group by group.
745pub(crate) fn reject_empty_claim_for_nonempty_root(
746    total_bytes: u64,
747    hash: [u8; 32],
748) -> anyhow::Result<()> {
749    if total_bytes == 0 && hash != *blake3::hash(&[]).as_bytes() {
750        return Err(anyhow::Error::new(HashMismatch));
751    }
752    Ok(())
753}
754
755/// Typed sentinel for a pull aborted because the bytes that ACTUALLY arrived
756/// crossed the buyer's `max_blob_size_bytes` ceiling (#1895). The peer's signed
757/// `total_bytes` claim never drives a refusal — it is peer-controlled and
758/// unverified (`StreamResponse::validate()` does not bound it) — so the ceiling
759/// binds on cumulative RECEIVED, BLAKE3-verified bytes instead. Returned (not a
760/// bare string) so the pull orchestrator can `downcast_ref` and classify it as a
761/// buyer-side policy rejection — distinct from a hash mismatch or an unreachable
762/// peer — rather than mis-attributing it to the provider's reputation. `Display`
763/// carries `BlobTooLarge` so logs and the requester tests can match on it.
764///
765/// **Units.** `reached` and `ceiling` are ALWAYS the same unit within one error —
766/// the comparison at each enforcement site is apples-to-apples — but that unit
767/// differs by site, so neither field is a raw `max_blob_size_bytes` config value
768/// across all call sites. The receive loop ([`UpstreamPull::next_chunk`]) meters bao
769/// WIRE bytes (content plus interleaved proof, ADR 038) and compares against the
770/// wire size of a ceiling-sized blob. The gap-driven driver (`fill_gap`) meters
771/// CONTENT bytes (the store's delivered frontier) and the resume-offset guard in
772/// [`open_progressive_pull`] meters a CONTENT offset, both against the configured
773/// `max_blob_size_bytes` directly. [`acquire`] meters CONTENT bytes against
774/// [`AcquireEnv::max_blob_bytes`]: it clamps a size claim to the cap and
775/// reports a blob whose bytes run past it once every byte below the cap is
776/// present and no leg proved a size within it. Every one is a faithful "the
777/// byte position crossed the ceiling" report.
778#[derive(Debug)]
779#[cfg_attr(not(feature = "test-util"), non_exhaustive)]
780pub struct BlobTooLarge {
781    /// The byte position that crossed `ceiling` — wire bytes taken off the stream
782    /// (receive loop), the store's content frontier (gap-driven driver), or a
783    /// content resume offset already past it. Same unit as `ceiling` (see the
784    /// type's **Units** note).
785    pub reached: u64,
786    /// The ceiling `reached` crossed, in the same unit as `reached`.
787    pub ceiling: u64,
788}
789
790/// The requested range starts at or past the end of the blob the node signed
791/// for, so no byte of it can be served (#1120). A node that signs a size of
792/// zero for a bounded open of `[0, len)` raises it too.
793///
794/// Typed rather than a bare string because it is the ONE signal that proves a
795/// caller's partial download does not belong to this blob — a stale `.partial`
796/// left under the same `--output` by a fetch of a different or larger blob, or
797/// one that completed but was killed before its rename. A resumable client keys
798/// its discard-and-refetch on this and nothing else: classifying by exclusion
799/// ("any error that is not a voucher rejection") would sweep in ordinary stalls
800/// and resets and destroy a perfectly good prefix the user has already paid for.
801///
802/// Note this is a statement about the *offset*, not about the peer: an honest
803/// node answers this way to a range cut from a wrong size. A node's reputation
804/// must not score it. The acquire loop classifies it as a
805/// [`Fault::Source`]: the source cools and its range
806/// moves to other sources.
807#[derive(Debug)]
808#[doc(hidden)]
809pub struct ResumeOffsetPastEnd {
810    /// Whole-blob size the node signed for.
811    pub total_bytes: u64,
812    /// The offset we asked to resume from.
813    pub byte_offset: u64,
814}
815
816impl std::fmt::Display for ResumeOffsetPastEnd {
817    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
818        write!(
819            f,
820            "cannot resume at byte {}: the blob is only {} bytes",
821            self.byte_offset, self.total_bytes
822        )
823    }
824}
825
826impl std::error::Error for ResumeOffsetPastEnd {}
827
828impl std::fmt::Display for BlobTooLarge {
829    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
830        write!(
831            f,
832            "reached byte {}, crossing the {}-byte size ceiling (BlobTooLarge)",
833            self.reached, self.ceiling
834        )
835    }
836}
837
838impl std::error::Error for BlobTooLarge {}
839
840/// Typed sentinel for a server that signed an open-stage `StreamResponse`
841/// (`ok == true`) quoting a per-MB `rate_per_mb` above the buyer's effective
842/// ceiling (#1375). The buyer aborts **before paying**, so the profitable
843/// "quote low on the probe, quote high on the stream, then deliver" bait-and-switch
844/// never gets paid.
845///
846/// The ceiling is the lower of two bounds the caller supplies: the probe rate the
847/// candidate was selected on (a stream quote may not exceed what the probe
848/// advertised) and an optional absolute buyer config (`max_rate_per_mb`). Because
849/// the probe-relative bound is always applied by the node pull path, a *completed*
850/// delivery is by construction priced at or below the probe rate — so it can never
851/// be rate-manipulation (`SlashJudge` requires `stream.ratePerMb > probe.ratePerMb`),
852/// and the challengeable case is exactly this abort.
853///
854/// Carries the operator's own signed `response` — already verified against
855/// `expected_signer` by `verify_response` before construction, so it still
856/// recovers to the delivering node and a caller could replay it (paired with the
857/// same node's `ProbeResponse`) to `SlashJudge` with no re-signing.
858///
859/// **Slashability caveat.** The evidence is only rate-*manipulation* evidence when
860/// the **probe-relative** bound was the one exceeded: `SlashJudge` requires
861/// `stream.ratePerMb > probe.ratePerMb`. The ceiling here is
862/// `min(probe_rate, config_absolute)` ([`effective_rate_ceiling`]), so when the
863/// buyer's absolute `config` bound is the binding one the quote may sit at or below
864/// the probe rate and be perfectly honest. A challenger must therefore still
865/// confirm `quote > probeRate` before submitting; this type does not assert it.
866///
867/// **Consumption.** The signed response is retained *on the error* so a caller can
868/// act on it, but the node's pull-failure handler does not itself submit a
869/// challenge — auto-slashing is deferred (the same footing as the daemon never
870/// auto-challenging an [`UpstreamRefused`], whose evidence is exercised only by the
871/// e2e harness today). `evidence()` is the only accessor; the field is private and
872/// the sole constructor derives it from the verified response, so the
873/// verified-signature invariant cannot be bypassed by a hand-built value.
874pub struct RateAboveCeiling {
875    quoted_rate_per_mb: u64,
876    ceiling_rate_per_mb: u64,
877    response: StreamResponse,
878}
879
880impl RateAboveCeiling {
881    /// Build the abort error for an open-stage quote above `ceiling`. Crate-private
882    /// and the SINGLE construction path (`open_progressive_pull` routes through
883    /// it), so `quoted_rate_per_mb` is always
884    /// *derived* from `response.body.rate_per_mb` and cannot desync from the
885    /// retained evidence — the same "derive, don't trust the caller to set it
886    /// consistently" discipline as [`UpstreamRefused::open`]. Callers MUST have run
887    /// `verify_response` on `response` first (the field docs' signature invariant).
888    fn over_ceiling(response: StreamResponse, ceiling_rate_per_mb: u64) -> anyhow::Error {
889        anyhow::Error::new(Self {
890            quoted_rate_per_mb: response.body.rate_per_mb,
891            ceiling_rate_per_mb,
892            response,
893        })
894    }
895
896    /// The per-MB rate the server quoted in the signed `StreamResponse`.
897    #[must_use]
898    pub const fn quoted_rate_per_mb(&self) -> u64 {
899        self.quoted_rate_per_mb
900    }
901
902    /// The effective ceiling the quote exceeded (min of probe rate and config).
903    #[must_use]
904    pub const fn ceiling_rate_per_mb(&self) -> u64 {
905        self.ceiling_rate_per_mb
906    }
907
908    /// The operator's own signed `StreamResponse`, verified against
909    /// `expected_signer` before this value was built. See the type docs for the
910    /// slashability caveat (only `quote > probeRate` is manipulation).
911    #[must_use]
912    pub const fn evidence(&self) -> &StreamResponse {
913        &self.response
914    }
915}
916
917impl std::fmt::Debug for RateAboveCeiling {
918    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
919        // Elide the signed body + 65-byte `slash_sig` to a length, as `UpstreamRefused`
920        // does, so a logged abort stays readable.
921        f.debug_struct("RateAboveCeiling")
922            .field("quoted_rate_per_mb", &self.quoted_rate_per_mb)
923            .field("ceiling_rate_per_mb", &self.ceiling_rate_per_mb)
924            .field("evidence_slash_sig_len", &self.response.slash_sig.len())
925            .finish()
926    }
927}
928
929impl std::fmt::Display for RateAboveCeiling {
930    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
931        write!(
932            f,
933            "server quoted {} per MB, exceeding the buyer ceiling {} per MB (RateAboveCeiling)",
934            self.quoted_rate_per_mb, self.ceiling_rate_per_mb
935        )
936    }
937}
938
939impl std::error::Error for RateAboveCeiling {}
940
941/// Combine the two buyer rate ceilings into one effective bound, treating `0` as
942/// "unbounded" on each input (the probe-relative bound and the absolute config
943/// bound are both optional). Returns `0` only when BOTH are unbounded.
944#[must_use]
945pub const fn effective_rate_ceiling(probe_relative: u64, config_absolute: u64) -> u64 {
946    match (probe_relative, config_absolute) {
947        (0, c) => c,
948        (p, 0) => p,
949        (p, c) if p < c => p,
950        (_, c) => c,
951    }
952}
953
954/// Typed sentinel for the buyer's own per-candidate pull deadline firing (#857).
955/// Returned (not a bare string) so the pull orchestrator can `downcast_ref` and
956/// recognize that the timeout is OUR local deadline — a possibly mis-sized
957/// configuration value — not evidence the provider is unreachable, and so must
958/// not tar the provider's local reputation score. `Display` keeps the
959/// stable `timed out` text for logs (and for the `!contains("timed out")`
960/// negative assertion in `node_to_node_pull_through`'s deadline test).
961///
962/// Since #1134 it is raised by OUR OWN wall clocks only, and the message is
963/// deliberately stage-NEUTRAL because there are THREE of them: the shared
964/// `open_stream` open bound (`PullDeadlines::open`), the optional overall
965/// `hard_cap` the `test-util` wrappers apply, and the stall clock elapsing before
966/// the FIRST byte (`cumulative == 0`) in the streaming loop — where it is
967/// measuring the server's time-to-first-byte, not mid-stream inactivity. `decdn-node`
968/// also wraps `open_progressive_pull` in its per-candidate budget as belt-and-braces,
969/// which raises the same sentinel.
970///
971/// The one thing it is NOT is a mid-stream STALL once bytes have flowed — that is
972/// [`PullStalled`], a fact about the PEER that scores its reputation, whereas this is a
973/// fact about our own possibly mis-sized budget (or an honest slow server's TTFB) and
974/// must not. Keeping the two apart is the whole point of the split, and the `cumulative
975/// == 0` carve-out is where the two loops draw the line (see [`PullStalled`]).
976///
977/// Callers that want to name the stage add a `.context(…)` layer; `downcast_ref`
978/// still recovers the sentinel through it — pinned by
979/// `buyer_side_sentinels_survive_anyhow_downcast` in `decdn-node`'s `node_origin.rs`,
980/// not in this crate.
981#[derive(Debug)]
982#[cfg_attr(not(feature = "test-util"), non_exhaustive)]
983pub struct PullTimeout {
984    /// How long the pull ran before the budget elapsed.
985    pub after: Duration,
986}
987
988impl std::fmt::Display for PullTimeout {
989    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
990        write!(f, "upstream pull timed out after {:?}", self.after)
991    }
992}
993
994impl std::error::Error for PullTimeout {}
995
996/// Typed sentinel for the upstream rejecting a voucher we presented mid-stream
997/// (#857) — e.g. a stale nonce (#852), deposit exhaustion, or a wrong-channel
998/// mismatch. This is OUR payment-side fault, not the provider's, so the pull
999/// orchestrator `downcast_ref`s it to skip the candidate WITHOUT recording a
1000/// reputation observation (mirroring the buyer channel-open-failure arm). Named
1001/// with the `Upstream` prefix to disambiguate from the protocol-level
1002/// `StreamError::VoucherRejected` reason enum, whose `reason` it carries verbatim
1003/// (the `Copy` `VoucherRejectReason`, not a lossy stringification) so a future
1004/// caller can branch on retry-vs-top-up-vs-abandon without re-parsing a message.
1005/// `Display` keeps the stable `voucher rejected` text for logs.
1006///
1007/// `bundle` mirrors the wire [`WatermarkBundle`] verbatim (issue #1481): `Some`
1008/// only for a watermark-gated reason ([`VoucherRejectReason::is_watermark_gated`]),
1009/// and only when the node verified the rejected voucher recovered to the pool
1010/// capability's pinned `signer` before attaching it. A caller that sees `Some`
1011/// can self-heal when the bundle advances its ledger — reseed to the bundle's
1012/// watermark ([`crate::ledger::Cumulative::from`]) and resume from
1013/// `bytes_delivered` — or, on `Underpaid`, rebase down to it, rather than
1014/// treating the rejection as terminal. `SpendingCapExhausted`
1015/// with `bundle: None` means there is no signer-verified watermark to resume
1016/// from (or, more commonly, that a wallet-less delegate simply has no local
1017/// means to add deposit) — the caller must surface that to the app rather
1018/// than loop. `CapabilityExpired` and `PoolExhausted` are never
1019/// watermark-gated (no bundle is ever attached) and are always terminal —
1020/// the fix is a fresh capability or an owner top-up, not a resync.
1021#[derive(Debug)]
1022#[cfg_attr(not(feature = "test-util"), non_exhaustive)]
1023pub struct UpstreamVoucherRejected {
1024    /// Why the seller refused the voucher.
1025    pub reason: VoucherRejectReason,
1026    /// The seller's signer-verified watermark, when it sent one, so the buyer
1027    /// can resync its cumulative state and retry. `None` means there is
1028    /// nothing to resync from and the caller must surface the failure.
1029    pub bundle: Option<WatermarkBundle>,
1030    /// The ledger generation the rejected voucher was signed under
1031    /// (`StreamProof::Voucher`), when the stream knows it. `None` for a
1032    /// rejected reveal, or a rejection read before this stream claimed
1033    /// anything; [`PoolLedger::rebase`] treats it as current.
1034    pub proof_generation: Option<u64>,
1035}
1036
1037impl std::fmt::Display for UpstreamVoucherRejected {
1038    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1039        // The `{:?}` rendering of the reason is load-bearing: the loopback tests
1040        // assert `.contains("CapExceeded")` / `.contains("Expired")`-style substrings
1041        // on this string. A custom `Display` for `VoucherRejectReason` would have to
1042        // reproduce the variant names verbatim, so keep the Debug rendering here.
1043        write!(f, "voucher rejected: {:?}", self.reason)
1044    }
1045}
1046
1047impl std::error::Error for UpstreamVoucherRejected {}
1048
1049/// Typed sentinel for an upstream *refusing* delivery up front — a
1050/// `StreamResponse` with `body.ok == false` (#1144). Carries the wire
1051/// [`StreamError`] verbatim (mirroring [`UpstreamVoucherRejected`]'s typed
1052/// `reason`, not a lossy stringification) so the pull orchestrator can
1053/// `downcast_ref` and give each refusal the verdict it deserves, rather than
1054/// folding every one into "unreachable".
1055///
1056/// That distinction is the whole point. A refusal is proof the peer is *reachable
1057/// and answering*, so no class is evidence that it is degraded (ADR 005 §Open-time
1058/// refusal classes):
1059///
1060/// - `NotFound` — "not now". It is NODE-scoped, not blob-scoped
1061///   (`ServeRejectReason::wire_error` collapses a miss, a load shed, and several
1062///   pool and signer states onto it), so it earns only a short suppression.
1063/// - `Declined` — "not this hash during this fetch", DURABLE for this (peer, hash):
1064///   asking again soon gets the same answer, so the consumer drops the peer for the
1065///   hash rather than burning a candidate slot on it.
1066/// - `Unfunded` — "not at the requester's current funding". It says nothing about
1067///   the peer; it counts toward the requester's funding recovery.
1068/// - `VoucherRejected` — a payment fault on this lane. Listed because it genuinely
1069///   arrives here: it is the code designated for MID-stream, and the mid-stream receive
1070///   arms wrap any `StreamError` into this sentinel (#1145 review).
1071///
1072/// `node_origin::classify_refusal` is the consumer and matches this exhaustively, so a new
1073/// wire code breaks that build rather than silently inheriting a verdict.
1074///
1075/// Only the wire class is recoverable here, never the finer server-side
1076/// `ServeRejectReason` — that collapse is intentional and must not be reversed.
1077/// `Display` keeps the stable `delivery refused: {code}` text that logs, the CLI's
1078/// cache-miss annotation, and the loopback tests match on.
1079/// The two refusal shapes are a private inner enum, not `pub` variants: enum
1080/// variant fields cannot be made private, so exposing them would let any caller
1081/// build an `Open { .. }` directly and bypass the crate-private `open`
1082/// constructor's error-from-response derivation — reintroducing exactly the
1083/// doc-comment-only invariant this type exists to remove (#1377). The newtype
1084/// keeps the only paths to a value the two constructors, so the "present response
1085/// ⇒ open-stage, verified" invariant is a property of the type rather than a
1086/// convention.
1087#[derive(Clone)]
1088pub struct UpstreamRefused {
1089    kind: Kind,
1090}
1091
1092#[derive(Clone)]
1093enum Kind {
1094    /// Open-stage refusal: the upstream signed a [`StreamResponse`] with
1095    /// `body.ok == false`, already verified against `expected_signer` by
1096    /// [`verify_response`] before the value is built, so `error` is derived
1097    /// *from* the response's trailing [`StreamResponseExt`] and `response`
1098    /// always recovers to `expected_signer`.
1099    Open {
1100        error: StreamError,
1101        // Boxed (issue #1481 review): `StreamError::VoucherRejected` grew an
1102        // optional `WatermarkBundle`, which pushed the unboxed variant past
1103        // clippy's `large_enum_variant` threshold relative to `MidStream`.
1104        response: Box<StreamResponse>,
1105    },
1106    /// Mid-stream refusal: a bare [`ClientMessage::StreamError`] frame that
1107    /// arrived after the open stage. It carries no signature (#1042), so there
1108    /// is never a signed `StreamResponse` to retain — `None` by construction.
1109    MidStream { error: StreamError },
1110}
1111
1112impl UpstreamRefused {
1113    /// Build the open-stage refusal for a `body.ok == false` response, deriving
1114    /// the wire `error` *from* the response's trailing [`StreamResponseExt`] so
1115    /// the two can never disagree (#1377 invariant 2). The one construction site
1116    /// [`open_progressive_pull`]'s open stage uses, so every caller classifies a
1117    /// refusal the same way.
1118    ///
1119    /// Returns `anyhow::Error` rather than `Self` because the `None`-error arm is
1120    /// a protocol violation, not a refusal: callers MUST have run
1121    /// [`verify_response`] first, whose `StreamResponseExt::validate` rejects
1122    /// `ok == false` with no error code (`MissingStreamError`). Reaching the
1123    /// `None` arm means that invariant was bypassed, so it is surfaced as the
1124    /// violation it is rather than defaulted to an invented code — which would
1125    /// launder a malformed refusal into a plausible-looking one.
1126    ///
1127    /// Takes the whole `response` rather than just its `error` so the operator's
1128    /// own signed refusal survives on the value ([`Self::evidence`], #1042) —
1129    /// `body` is exactly the field set the `SlashJudge` EIP-712 `StreamResponse`
1130    /// typehash covers and `slash_sig` is the operator's secp256k1 signature over
1131    /// it.
1132    ///
1133    /// A refusal (`ok == false`) is **not** on-chain slash evidence: rate
1134    /// manipulation requires `ok == true` and blacklist violation requires a
1135    /// served claim (ADR 014), so a node may sign refusals freely. The retention
1136    /// is kept on its own merits — it is an attributable, non-repudiable record
1137    /// of *why* a paid pull was declined, which the caller can log, surface, or
1138    /// present in a dispute without re-signing anything.
1139    fn open(response: StreamResponse, ext: &StreamResponseExt) -> anyhow::Error {
1140        // `ok == true` is not a refusal at all — building an `Open` from it would
1141        // mint an evidence-carrying refusal with `ok == true`, violating invariant
1142        // 3. `validate` already rejects `(ok: true, error: Some)` as
1143        // `StreamErrorWithOk`, so reaching here with `ok == true` means that
1144        // invariant was bypassed; surface it as the protocol violation it is rather
1145        // than fold it into a typed refusal. Makes invariant 3 a construction
1146        // property, not just a call-site precondition (#1377 review).
1147        if response.body.ok {
1148            return anyhow::anyhow!(
1149                "open-stage refusal constructed from an ok == true response \
1150                 (StreamResponse::validate invariant bypassed)"
1151            );
1152        }
1153        match ext.error.clone() {
1154            Some(error) => anyhow::Error::new(Self {
1155                kind: Kind::Open {
1156                    error,
1157                    response: Box::new(response),
1158                },
1159            }),
1160            None => anyhow::anyhow!(
1161                "delivery refused but the validated response carried no error code \
1162                 (StreamResponseExt::validate invariant bypassed)"
1163            ),
1164        }
1165    }
1166
1167    /// Build the refusal for a **mid-stream** [`ClientMessage::StreamError`]
1168    /// frame — the refusal that arrives *after* the open stage, in reply to a
1169    /// delivery chunk or a voucher.
1170    ///
1171    /// Such a frame carries no signature (#1042), so [`Self::evidence`] is `None`
1172    /// here **by construction** and this is the only place that decision is made.
1173    /// The four mid-stream receive sites route through it so none can drift into
1174    /// synthesising a `StreamResponse` — which would hand an observer an unsigned
1175    /// artifact the evidence contract promises always recovers to the delivering
1176    /// node (#1378). The open-stage counterpart is the crate-private `open`.
1177    ///
1178    /// Public because callers outside the crate (and their tests) legitimately
1179    /// build mid-stream refusals; it is safe to expose precisely because it
1180    /// *cannot* carry a signed response — the encapsulation that matters guards
1181    /// the attributable `open` ([`Self::evidence`]), which stays crate-private
1182    /// so a caller cannot mint a refusal attributed to an operator.
1183    #[must_use]
1184    pub const fn mid_stream(error: StreamError) -> Self {
1185        Self {
1186            kind: Kind::MidStream { error },
1187        }
1188    }
1189
1190    /// The wire code the upstream signed. Always a `StreamError` as it appeared
1191    /// on the wire — never a server-side `ServeRejectReason`, whose seven-way
1192    /// collapse onto `NotFound` is deliberate and one-way
1193    /// (`handlers::client::wire_error`). Total over both shapes.
1194    #[must_use]
1195    pub const fn error(&self) -> &StreamError {
1196        match &self.kind {
1197            Kind::Open { error, .. } | Kind::MidStream { error } => error,
1198        }
1199    }
1200
1201    /// The upstream's own signed [`StreamResponse`], present iff the refusal
1202    /// arrived at the **open stage**. `None` for a mid-stream frame.
1203    ///
1204    /// An attributable record, not loose diagnostics — already verified against
1205    /// `expected_signer` by `verify_response` before this value is built, so a
1206    /// present value always recovers to `expected_signer`: the operator address
1207    /// the caller bound this pull to, which is what `SlashJudge._checkRegistered`
1208    /// resolves `nodeId` against.
1209    ///
1210    /// Note it is not by itself *slash* evidence — no offense admits a refusal
1211    /// (`ok == false`) as its stream leg (ADR 014). What it gives the caller is a
1212    /// non-repudiable statement of why the pull was declined, signed by the
1213    /// operator and usable verbatim, with no re-signing.
1214    #[must_use]
1215    pub fn evidence(&self) -> Option<&StreamResponse> {
1216        match &self.kind {
1217            Kind::Open { response, .. } => Some(response.as_ref()),
1218            Kind::MidStream { .. } => None,
1219        }
1220    }
1221}
1222
1223impl std::fmt::Debug for UpstreamRefused {
1224    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1225        // Elide the 65-byte `slash_sig` (and the full signed body) to a length so
1226        // `tracing::error!(?err)` stays readable — the derived `Debug` would dump
1227        // the entire attestation on every logged refusal.
1228        let mut dbg = f.debug_struct("UpstreamRefused");
1229        dbg.field("error", self.error());
1230        match self.evidence() {
1231            Some(resp) => dbg.field("evidence_slash_sig_len", &resp.slash_sig.len()),
1232            None => dbg.field("evidence", &Option::<()>::None),
1233        };
1234        dbg.finish()
1235    }
1236}
1237
1238impl std::fmt::Display for UpstreamRefused {
1239    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1240        // The `{:?}` rendering is load-bearing, exactly as in
1241        // `UpstreamVoucherRejected`: the loopback tests assert
1242        // `.contains("NotFound")` on this string, and `StreamError` has no
1243        // `Display`. What renders here is always a WIRE code — never a node-side
1244        // `ServeRejectReason` such as `UnknownChannel`, which collapses to
1245        // `NotFound` before it leaves the server (see `wire_error`). Matching on a
1246        // reject-reason name would therefore never fire.
1247        write!(f, "delivery refused: {:?}", self.error())
1248    }
1249}
1250
1251impl std::error::Error for UpstreamRefused {}
1252
1253/// Typed sentinel for an upstream that went SILENT mid-stream — bytes were flowing, and
1254/// then no byte of progress for the stall budget (#1134). Distinct from [`PullTimeout`],
1255/// which is a bound of OURS elapsing, and the distinction is load-bearing:
1256///
1257/// A whole-transfer deadline cannot tell "the provider is dead" from "this blob is big" or
1258/// "this link is slow", so `PullTimeout` must NOT tar the provider — it fires on perfectly
1259/// healthy transfers. A stall deadline resets on every byte received, so it fires ONLY when
1260/// a provider stops delivering while we wait. That IS evidence the peer is unreachable, and
1261/// `classify_pull_failure` scores it as such in the local per-peer EWMA (ADR 008).
1262///
1263/// # What the bound rests on
1264///
1265/// Two things, and it is worth being precise about both, because scoring a peer on a bound
1266/// that does not hold is how an honest node's local reputation gets unfairly defamed.
1267///
1268/// **The non-empty-`ChunkData` invariant (#1088).** With empty frames banned, "a frame
1269/// arrived" and "bytes made progress" are the same statement, so a peer cannot hold the
1270/// floor open with padding. The receive loop ([`UpstreamPull::next_chunk`]) has no
1271/// independent progress check behind that floor: it is safe because no frame a peer can
1272/// send makes zero progress, not because it verifies that it did. The floor is
1273/// structural rather than advisory — `ChunkData`'s field is private, and its constructor
1274/// and decode gate both reject an empty payload — so it cannot be relaxed by forgetting
1275/// to call a validator (#1145 review).
1276///
1277/// **At least one byte having already arrived.** The reset is what makes a stall the peer's
1278/// fault, so before the FIRST byte there has been no reset and the argument does not apply:
1279/// the clock is measuring the server's time-to-first-byte, which scales with blob size
1280/// (the serve path materialises the whole bao wire via `export_bao_range` before it can emit
1281/// chunk #1). Both loops therefore raise [`PullTimeout`] — exonerating — when the budget
1282/// elapses at `cumulative == 0`, and this sentinel only once bytes have flowed (#1145
1283/// review). Without that split, a 1 GiB blob off a cold disk would score an honest server as
1284/// unreachable for the crime of being big.
1285#[derive(Debug)]
1286#[cfg_attr(not(feature = "test-util"), non_exhaustive)]
1287pub struct PullStalled {
1288    /// The inactivity budget that elapsed after bytes had been flowing.
1289    pub after: Duration,
1290}
1291
1292impl std::fmt::Display for PullStalled {
1293    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1294        write!(f, "upstream stalled: no progress for {:?}", self.after)
1295    }
1296}
1297
1298impl std::error::Error for PullStalled {}
1299
1300/// Marker for a pull failure that is OURS, not the upstream's — a broken local
1301/// signer, an encode fault, a bad range computation (#1145 review).
1302///
1303/// Everything else that reaches `classify_pull_failure`'s catch-all is attributable
1304/// to the peer: a failed dial, a dropped connection, a bad signature, an unexpected
1305/// frame. Scoring the peer `Unreachable` there is correct, and is in fact the
1306/// PRIMARY way a dead node is detected — so the catch-all must stay as it is.
1307///
1308/// The exception was this class. A node whose own signer is broken cannot pay
1309/// anybody, and would walk the candidate list tarring every honest
1310/// provider it met with an `Unreachable` — a local EWMA hit; ADR 008 scoring is
1311/// local-only — on the strength of its own fault. Attach this marker at a
1312/// local-fault site and the classifier exonerates the peer and warns about us instead.
1313///
1314/// # It also decides what a CLIENT is told (#1560)
1315///
1316/// Reputation is not the only consequence. A second consumer — the node crate's
1317/// `record_pool_open_failure` — reads this marker to choose between answering a
1318/// downstream client `StreamError::NotFound` ("we could not obtain this blob") and
1319/// `Declined` ("this node will not serve this hash during this fetch"). That path
1320/// involves no peer and no reputation at all. One marked failure still answers
1321/// `NotFound`: a wallet that cannot fund a deposit, because the funding problem is
1322/// this node's, not the requester's.
1323///
1324/// So attaching this marker at a NEW site changes serve-path refusals, not just scoring.
1325/// Attach it when the failure means *this node* cannot serve anyone — a broken signer, an
1326/// unreadable or unwritable store, a poisoned lock, an unfunded wallet. Do NOT attach it to
1327/// a condition that is specific to one peer, one channel, or one blob, however much it is
1328/// "our side" of the exchange: a wedged channel to a single provider is ours and is still a
1329/// clean miss, because the node can serve every other request perfectly well.
1330///
1331/// It is a marker, so it composes: `.context(LocalPullFault)` on any error.
1332#[derive(Debug)]
1333#[doc(hidden)]
1334pub struct LocalPullFault;
1335
1336impl std::fmt::Display for LocalPullFault {
1337    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1338        f.write_str("local buyer-side fault (not attributable to the upstream)")
1339    }
1340}
1341
1342impl std::error::Error for LocalPullFault {}
1343
1344/// The bounds on a pull, each matched to the stage it governs (#1134, #1797).
1345///
1346/// A single whole-transfer deadline is mostly useless as a health signal: it has to be
1347/// sized against `blob size × link speed`, so it kills legitimate large or slow-but-healthy
1348/// transfers while a value small enough to catch a dead peer quickly cannot serve a big blob
1349/// at all. The operator ends up tuning a number that has nothing to do with node health.
1350///
1351/// Split by stage instead:
1352///
1353/// - **`window` + `floor_bps`** bound the STREAMING stage by THROUGHPUT. The requester
1354///   counts bytes read off the QUIC stream (sub-frame, so the measure is frame-size
1355///   independent) and trips when the bytes across the trailing `window` fall below
1356///   `floor_bps · window`. This catches both a wedge (throughput drops to zero) and a
1357///   slow drip (throughput below the floor), and is indifferent to transfer size and link
1358///   speed. `floor_bps == 0` leaves pure idle detection: at least one byte per window.
1359///   This is the primary mechanism. The abort is requester-local policy and does not score
1360///   the peer.
1361/// - **`hard_cap`** is an optional overall wall-clock escape hatch, `None` by
1362///   default. It exists for a caller that must bound total runtime regardless; it
1363///   is not how a stalled peer is detected.
1364///
1365/// - **`open`** bounds the OPEN stage (dial → request → verified `StreamResponse`)
1366///   by WALL CLOCK. That stage is bounded work whose duration does not depend on
1367///   the blob, so a slow one really is a stall and a wall clock is the right tool.
1368///
1369/// Every stage must carry a bound of its own. It is not enough for a caller to
1370/// wrap the whole pull in a timeout and call the open "bounded": the handshake
1371/// happens *inside* [`open_progressive_pull`], and the production callers run
1372/// with no overall cap, so leaving the bound to the caller would leave the
1373/// `StreamResponse` read with no bound at all — a peer that accepts a connection
1374/// and then says nothing would hang the pull forever. `open` exists so that
1375/// cannot be expressed.
1376/// # The relational invariant
1377///
1378/// `hard_cap`, when set, must STRICTLY EXCEED `open + window`. Both clocks below run inside
1379/// the cap's, and in the worst case consecutively — the open can legitimately consume its
1380/// whole budget before the streaming stage begins, and the floor needs one full `window` to
1381/// fire — so a cap that does not outlast both means the cap always fires first and the
1382/// throughput floor can never trip under it. The pull then looks fully configured while its
1383/// peer-health signal is dead.
1384///
1385/// [`Self::capped`] is fallible and the fields are private BECAUSE of that (#1145 review).
1386/// The invariant belongs on this type: with `pub` fields, callers build it with a struct
1387/// literal and the only check is a hardcoded `timeout > 2 × window` in the CLI's
1388/// `ClientFetchArgs::validate`, correct only because those call sites set `open` from the same
1389/// knob as `window`. Adding an
1390/// `--open-timeout-ms` flag would have made it silently wrong, in the direction that reopens
1391/// the hole. The invariant belongs to the type that has the values.
1392#[derive(Debug, Clone, Copy)]
1393pub struct PullDeadlines {
1394    /// Wall-clock bound on the open stage: dial, request, and the signed
1395    /// `StreamResponse`. Bounded work — a slow one is a stall.
1396    open: Duration,
1397    /// Trailing window over which the streaming-stage throughput floor is measured.
1398    window: Duration,
1399    /// Minimum bytes-per-second the streaming stage must sustain over `window`. `0`
1400    /// disables the throughput test and leaves pure idle detection (one byte per window).
1401    floor_bps: u64,
1402    /// Optional overall wall-clock cap on the whole exchange. `None` = uncapped;
1403    /// `open` and `window` between them are what keep an uncapped pull from hanging.
1404    hard_cap: Option<Duration>,
1405}
1406
1407/// A [`PullDeadlines`] whose bounds cannot do their job. Carries the values so a CLI
1408/// can render the arithmetic back to the user rather than just saying "invalid".
1409#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1410#[non_exhaustive]
1411pub enum DeadlineError {
1412    /// A zero budget elapses on its first poll, so the stage it bounds can never run.
1413    ZeroBudget,
1414    /// The cap does not outlast `open + window`, so it always fires first and the
1415    /// throughput floor — the signal that a healthy stream is still making progress —
1416    /// can never fire.
1417    CapCannotOutlastItsStages {
1418        /// The open-stage budget.
1419        open: Duration,
1420        /// The throughput-floor window that follows it.
1421        window: Duration,
1422        /// The whole-pull cap, which must exceed `open + window`.
1423        hard_cap: Duration,
1424    },
1425}
1426
1427impl std::fmt::Display for DeadlineError {
1428    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1429        match self {
1430            Self::ZeroBudget => write!(f, "a deadline of zero elapses before any work can run"),
1431            Self::CapCannotOutlastItsStages {
1432                open,
1433                window,
1434                hard_cap,
1435            } => write!(
1436                f,
1437                "the overall cap ({hard_cap:?}) must exceed the open bound ({open:?}) plus the \
1438                 throughput-floor window ({window:?}): both run inside it and in the worst case \
1439                 consecutively, so below that the cap always elapses first and a stalled \
1440                 provider can never be detected"
1441            ),
1442        }
1443    }
1444}
1445
1446impl std::error::Error for DeadlineError {}
1447
1448impl PullDeadlines {
1449    /// The recommended shape: a wall clock on the open, inactivity on the stream,
1450    /// and no overall cap — so a pull of any size completes as long as the upstream
1451    /// keeps feeding it bytes.
1452    ///
1453    /// # Errors
1454    ///
1455    /// [`DeadlineError::ZeroBudget`] for a zero `open` or `window`. `floor_bps` may be zero
1456    /// (idle-detection mode: one byte per window).
1457    ///
1458    /// A zero `open` or `window` must fail here, not pass. One might argue it is a bound that
1459    /// fires too EAGERLY — loud, immediately obvious — rather than one that silently never
1460    /// fires. That is exactly backwards: a zero window makes the throughput floor demand
1461    /// progress over no time at all, so the streaming stage can never satisfy it.
1462    ///
1463    /// The only thing standing between config and that state was a `> 0` check in a resolver
1464    /// in another crate — the same advisory-invariant shape `ChunkData` had before #1088, and
1465    /// the reason this type owns its bounds at all.
1466    pub const fn new(
1467        open: Duration,
1468        window: Duration,
1469        floor_bps: u64,
1470    ) -> Result<Self, DeadlineError> {
1471        if open.is_zero() || window.is_zero() {
1472            return Err(DeadlineError::ZeroBudget);
1473        }
1474        Ok(Self {
1475            open,
1476            window,
1477            floor_bps,
1478            hard_cap: None,
1479        })
1480    }
1481
1482    /// As [`Self::new`], plus an overall wall-clock cap on the whole exchange.
1483    ///
1484    /// # Errors
1485    ///
1486    /// [`DeadlineError::CapCannotOutlastItsStages`] if `hard_cap` does not strictly exceed
1487    /// `open + window`, and [`DeadlineError::ZeroBudget`] for a zero `open` or `window`. See
1488    /// the type's own docs for why this is the one constructor that must be fallible.
1489    pub fn capped(
1490        open: Duration,
1491        window: Duration,
1492        floor_bps: u64,
1493        hard_cap: Duration,
1494    ) -> Result<Self, DeadlineError> {
1495        if open.is_zero() || window.is_zero() {
1496            return Err(DeadlineError::ZeroBudget);
1497        }
1498        if hard_cap <= open.saturating_add(window) {
1499            return Err(DeadlineError::CapCannotOutlastItsStages {
1500                open,
1501                window,
1502                hard_cap,
1503            });
1504        }
1505        Ok(Self {
1506            open,
1507            window,
1508            floor_bps,
1509            hard_cap: Some(hard_cap),
1510        })
1511    }
1512
1513    /// The open-stage wall clock.
1514    #[must_use]
1515    pub const fn open(&self) -> Duration {
1516        self.open
1517    }
1518
1519    /// The streaming-stage throughput-floor window.
1520    #[must_use]
1521    pub const fn window(&self) -> Duration {
1522        self.window
1523    }
1524
1525    /// The streaming-stage throughput floor, in bytes per second. `0` = idle detection only.
1526    #[must_use]
1527    pub const fn floor_bps(&self) -> u64 {
1528        self.floor_bps
1529    }
1530
1531    /// The overall cap, if any.
1532    #[must_use]
1533    pub const fn hard_cap(&self) -> Option<Duration> {
1534        self.hard_cap
1535    }
1536
1537    /// The single-deadline shape: one budget serving as the open bound, the
1538    /// streaming window, AND the overall cap, with the throughput floor disabled
1539    /// (`floor_bps == 0`, idle detection only). **Test-only — do not reach for this in
1540    /// production.** That conflation is exactly what #1134 set out to remove, and the
1541    /// name reads far more like a legitimate policy choice than it is.
1542    ///
1543    /// Note what it quietly costs, beyond re-introducing the size-coupled deadline:
1544    /// because `hard_cap == window`, and the cap's clock starts at the top of the whole
1545    /// exchange while the streaming window starts only once the open has completed, **the
1546    /// hard cap always elapses first — so [`PullStalled`] can never fire under it.** A pull
1547    /// built this way silently cannot detect a stalled peer, and so cannot score one.
1548    /// Every loopback test using this helper is exercising a pull with the stall
1549    /// signal disabled; the stall path is covered by
1550    /// `node_origin_mid_stream_silence_does_not_score_stalled_upstream`, which builds its
1551    /// deadlines explicitly.
1552    ///
1553    /// That is a statement about the `test-util` `stream_fetch*` wrappers, where
1554    /// `with_hard_cap` wraps the whole exchange. [`open_progressive_pull`] and
1555    /// [`driver::drive`] never consult `hard_cap` at all, so a `whole_transfer` used there
1556    /// would leave `PullStalled` perfectly able to fire — which is not a reprieve, just a
1557    /// different reason not to reach for this (#1145 review).
1558    ///
1559    /// It is also the reason this constructor stays infallible while [`Self::capped`] is not:
1560    /// it deliberately builds the very state `capped` refuses.
1561    ///
1562    /// TEST-ONLY, and now unrepresentable in production by construction: gated behind the
1563    /// `test-util` feature (#1145 review), so a production caller cannot name it and reach for
1564    /// the disabled-floor / uncapped state — it wants [`Self::new`] (floor-bounded) or
1565    /// [`Self::capped`] (floor-bounded with a leak guard). Used by the loopback helper
1566    /// `stream_fetch` and directly by the `client_loopback` suite, whose blobs are small
1567    /// enough that none of this matters.
1568    #[cfg(any(test, feature = "test-util"))]
1569    #[must_use]
1570    pub const fn whole_transfer(timeout: Duration) -> Self {
1571        Self {
1572            open: timeout,
1573            window: timeout,
1574            floor_bps: 0,
1575            hard_cap: Some(timeout),
1576        }
1577    }
1578}
1579
1580/// Fetch `hash` from `target` over `cdn/client/v1` into memory, paying as bytes
1581/// arrive and verifying each bao chunk group as it lands.
1582///
1583/// `expected_signer` is the delivering node's Ethereum address, used to verify
1584/// the response `slash_sig`. `byte_offset` resumes a partial fetch. Use
1585/// [`stream_fetch_tracked`] instead if you need to persist the voucher watermark
1586/// the channel reached (#852); this convenience wrapper discards it.
1587///
1588/// TEST-ONLY, like the whole `stream_fetch*` family: gated behind the `test-util`
1589/// feature alongside [`PullDeadlines::whole_transfer`], the single-deadline shape
1590/// it passes (#1145 review). The wrappers are thin in-memory drivers of the one
1591/// receive loop, [`UpstreamPull`], for suites that want the decoded bytes back;
1592/// production callers stream into a store through [`driver::drive`] /
1593/// [`PeerSource`].
1594///
1595/// # Errors
1596///
1597/// Fails on connect/transport errors, an invalid or zero-rate response, a
1598/// `slash_sig` that does not recover to `expected_signer`, a mismatched echoed
1599/// field, a mid-stream `VoucherRejected`, a hash mismatch, or a timeout.
1600#[cfg(any(test, feature = "test-util"))]
1601#[allow(clippy::too_many_arguments)]
1602pub async fn stream_fetch(
1603    endpoint: &Endpoint,
1604    target: EndpointAddr,
1605    ctx: &PoolContext,
1606    slash_domain: &Eip712Domain,
1607    expected_signer: Address,
1608    hash: [u8; 32],
1609    byte_offset: u64,
1610    timestamp_us: u64,
1611    timeout: Duration,
1612) -> anyhow::Result<Bytes> {
1613    stream_fetch_tracked(
1614        endpoint,
1615        target,
1616        ctx,
1617        slash_domain,
1618        expected_signer,
1619        hash,
1620        // `stream_fetch` models a node-to-node pull; a suite that routes on a
1621        // namespace calls `stream_fetch_tracked` directly.
1622        decdn_protocol::client::NO_NAMESPACE,
1623        byte_offset,
1624        timestamp_us,
1625        // The single-deadline shape (#1134): this helper's callers are
1626        // loopback tests moving tiny blobs, for which one budget serving as both
1627        // the overall cap and the stall bound is harmless. Production paths take
1628        // `PullDeadlines` directly and split the two.
1629        PullDeadlines::whole_transfer(timeout),
1630        // No buyer-side blob-size or rate ceiling on this loopback helper; a suite
1631        // that exercises either passes it through `stream_fetch_tracked`.
1632        0,
1633        0,
1634        &mut VoucherProgress::default(),
1635    )
1636    .await
1637}
1638
1639/// Like [`stream_fetch`], but drives the fetch over a caller-owned
1640/// [`WarmConnection`] instead of dialling a fresh connection (#1848). Two
1641/// calls with the same `warm` reuse one dialled connection — one bi-stream per
1642/// hash — which is exactly what the connection-reuse test asserts.
1643///
1644/// TEST-ONLY, like the rest of the `stream_fetch*` family. Runs a single attempt
1645/// against a one-shot [`PoolLedger`] seeded from `ctx.prior_*` (the loopback
1646/// blobs it serves never trigger the wallet-less resume loop).
1647///
1648/// # Errors
1649///
1650/// The same set as [`stream_fetch`].
1651#[cfg(any(test, feature = "test-util"))]
1652#[allow(clippy::too_many_arguments)]
1653pub async fn stream_fetch_on(
1654    warm: &WarmConnection,
1655    ctx: &PoolContext,
1656    slash_domain: &Eip712Domain,
1657    expected_signer: Address,
1658    hash: [u8; 32],
1659    byte_offset: u64,
1660    timestamp_us: u64,
1661    timeout: Duration,
1662) -> anyhow::Result<Bytes> {
1663    let ledger = Arc::new(ctx.new_ledger());
1664    fetch_in_memory_once_on(
1665        warm,
1666        ctx,
1667        ledger,
1668        slash_domain,
1669        expected_signer,
1670        hash,
1671        // Models a node-to-node pull, matching `stream_fetch`.
1672        decdn_protocol::client::NO_NAMESPACE,
1673        byte_offset,
1674        timestamp_us,
1675        0,
1676        0,
1677        PullDeadlines::whole_transfer(timeout),
1678        None,
1679    )
1680    .await
1681}
1682
1683/// Like `stream_fetch`, but reports the channel's acked voucher watermark via
1684/// the `progress` out-param so the caller can persist what it paid (#852).
1685///
1686/// `progress` is an out-param: on return it holds the cumulative
1687/// `(bytes_delivered, amount)` of the last *acked* voucher. Internally the pull
1688/// runs against a one-shot [`PoolLedger`] seeded from `ctx.prior_*`; the
1689/// ledger's snapshot is copied back into `progress` on every return path — `Ok`,
1690/// `Err`, or timeout — so the caller can record progress even for a mid-stream
1691/// failure or a paid-but-corrupt delivery. See [`VoucherProgress`].
1692///
1693/// # Errors
1694///
1695/// Same as `stream_fetch`.
1696#[cfg(any(test, feature = "test-util"))]
1697#[allow(clippy::too_many_arguments)]
1698pub async fn stream_fetch_tracked(
1699    endpoint: &Endpoint,
1700    target: EndpointAddr,
1701    ctx: &PoolContext,
1702    slash_domain: &Eip712Domain,
1703    expected_signer: Address,
1704    hash: [u8; 32],
1705    namespace_id: [u8; 32],
1706    byte_offset: u64,
1707    timestamp_us: u64,
1708    deadlines: PullDeadlines,
1709    max_blob_size_bytes: u64,
1710    max_rate_per_mb: u64,
1711    progress: &mut VoucherProgress,
1712) -> anyhow::Result<Bytes> {
1713    stream_fetch_tracked_with_progress(
1714        endpoint,
1715        target,
1716        ctx,
1717        slash_domain,
1718        expected_signer,
1719        hash,
1720        namespace_id,
1721        byte_offset,
1722        timestamp_us,
1723        deadlines,
1724        max_blob_size_bytes,
1725        max_rate_per_mb,
1726        progress,
1727        None,
1728    )
1729    .await
1730}
1731
1732/// Delivery-progress callback: invoked with `(received, expected)` after each
1733/// verified leaf lands, so a caller (e.g. `decdn fetch`) can render a progress
1734/// bar. Both slots are **content** bytes: `received` is the content position the
1735/// verifying decoder has reached and `expected` is the blob's content
1736/// `total_bytes`, constant across the pull — so a bar keyed on the two fills to
1737/// exactly 100%. `drive` / `acquire` (the resumable [`crate::driver`]
1738/// path the CLI `fetch` and `bundle pull` use) additionally emit the
1739/// already-present resume base (`base_present`) once before streaming begins; the
1740/// `test-util` `stream_fetch_tracked_with_progress` wrapper reports the same
1741/// unit from its in-memory decoder.
1742///
1743/// It must not panic (it runs inside the hot receive loop) and must be `Send +
1744/// Sync` so the pull future stays spawnable.
1745pub type ProgressCallback = dyn Fn(u64, u64) + Send + Sync;
1746
1747/// Like [`stream_fetch_tracked`], but also reports per-chunk delivery progress
1748/// through `on_progress` (see [`ProgressCallback`]) — the byte-progress hook the
1749/// watermark-only [`VoucherProgress`] out-param does not provide. `None` behaves
1750/// exactly like [`stream_fetch_tracked`].
1751///
1752/// # Errors
1753///
1754/// Same as `stream_fetch`.
1755#[cfg(any(test, feature = "test-util"))]
1756#[allow(clippy::too_many_arguments)]
1757pub async fn stream_fetch_tracked_with_progress(
1758    endpoint: &Endpoint,
1759    target: EndpointAddr,
1760    ctx: &PoolContext,
1761    slash_domain: &Eip712Domain,
1762    expected_signer: Address,
1763    hash: [u8; 32],
1764    namespace_id: [u8; 32],
1765    byte_offset: u64,
1766    timestamp_us: u64,
1767    deadlines: PullDeadlines,
1768    max_blob_size_bytes: u64,
1769    max_rate_per_mb: u64,
1770    progress: &mut VoucherProgress,
1771    on_progress: Option<&ProgressCallback>,
1772) -> anyhow::Result<Bytes> {
1773    // One-shot ledger seeded from the channel's prior cumulative state. A single
1774    // (non-shared) pull owns its ledger; concurrent shared-channel pulls use
1775    // `stream_fetch_shared` with a caller-owned ledger instead.
1776    let ledger = Arc::new(ctx.new_ledger());
1777    let result = with_hard_cap(
1778        deadlines.hard_cap,
1779        fetch_inner(
1780            endpoint,
1781            target,
1782            ctx,
1783            slash_domain,
1784            expected_signer,
1785            hash,
1786            namespace_id,
1787            byte_offset,
1788            timestamp_us,
1789            max_blob_size_bytes,
1790            max_rate_per_mb,
1791            deadlines,
1792            &ledger,
1793            on_progress,
1794        ),
1795    )
1796    .await;
1797    // Copy the watermark back into `progress` on EVERY return path (Ok, Err,
1798    // timeout) BEFORE returning, so the latest totals survive a mid-stream failure
1799    // or a paid-but-corrupt delivery (#852).
1800    //
1801    // Read it from the LEDGER rather than a copied-back cumulative, so the figure
1802    // is `settlement` and not `committed` — matching the two sibling persist paths
1803    // (`UpstreamPull::progress`, the `node_origin` drop guard). A voucher left
1804    // armed by an ambiguous send is owed, and the error arm here is exactly where
1805    // that happens. On the `Ok` arm nothing is armed, so the two agree.
1806    *progress = VoucherProgress::from_ledger(&ledger, ctx.prior_amount);
1807    result
1808}
1809
1810/// Apply the optional overall wall-clock cap of a [`PullDeadlines`] to `fut`.
1811///
1812/// `None` runs the pull uncapped — which is safe precisely because the `stall`
1813/// bound inside bounds every streaming read. A pull with no cap cannot hang; it
1814/// can only take as long as the upstream keeps feeding it bytes, which is the
1815/// point (#1134).
1816#[cfg(any(test, feature = "test-util"))]
1817async fn with_hard_cap<F>(hard_cap: Option<Duration>, fut: F) -> anyhow::Result<Bytes>
1818where
1819    F: std::future::Future<Output = anyhow::Result<Bytes>>,
1820{
1821    match hard_cap {
1822        Some(cap) => tokio::time::timeout(cap, fut)
1823            .await
1824            .map_err(|_| anyhow::Error::new(PullTimeout { after: cap }))?,
1825        None => fut.await,
1826    }
1827}
1828
1829/// Like `stream_fetch`, but issues vouchers through a caller-owned shared
1830/// [`PoolLedger`] so multiple concurrent pulls on ONE payment channel coordinate.
1831///
1832/// The bug this fixes: each `stream_fetch`/`stream_fetch_tracked` call seeds its
1833/// own voucher state from `ctx.prior_*`, so N concurrent pulls on the same channel
1834/// each compute the next cumulative `amount` independently and collide — the node
1835/// accepts exactly one and rejects the rest as `AmountRegression`. Passing every
1836/// concurrent caller the SAME `&Arc<PoolLedger>` (shared across
1837/// `tokio::spawn`/`join!`) serializes their voucher issuance through the
1838/// ledger's mutex: each issue advances the cumulative `amount`/`bytes_delivered`
1839/// in turn, the channel advances monotonically, and all pulls succeed.
1840///
1841/// The caller owns the ledger's lifetime and persists what the channel paid from it
1842/// directly (this entrypoint does not surface a [`VoucherProgress`] — the shared ledger
1843/// IS the watermark). Persist via [`PoolLedger::settlement`], NOT `snapshot`:
1844/// `snapshot`/`committed` report only ACKED vouchers, so a voucher left in the ack wait
1845/// (the drop the node's `SettleOnDrop` guard handles) is under-reported and its deposit
1846/// stranded — `settlement` adds the in-flight voucher back (#1122/#1145). `snapshot` is
1847/// also `async`, so a `Drop` guard cannot call it at all.
1848///
1849/// # Errors
1850///
1851/// Same as `stream_fetch`.
1852#[cfg(any(test, feature = "test-util"))]
1853#[allow(clippy::too_many_arguments)]
1854pub async fn stream_fetch_shared(
1855    endpoint: &Endpoint,
1856    target: EndpointAddr,
1857    ctx: &PoolContext,
1858    ledger: &Arc<PoolLedger>,
1859    slash_domain: &Eip712Domain,
1860    expected_signer: Address,
1861    hash: [u8; 32],
1862    byte_offset: u64,
1863    timestamp_us: u64,
1864    deadlines: PullDeadlines,
1865    max_blob_size_bytes: u64,
1866    max_rate_per_mb: u64,
1867) -> anyhow::Result<Bytes> {
1868    with_hard_cap(
1869        deadlines.hard_cap,
1870        fetch_inner(
1871            endpoint,
1872            target,
1873            ctx,
1874            slash_domain,
1875            expected_signer,
1876            hash,
1877            // Shared-channel pulls model the daemon as buyer on a node-to-node
1878            // fill; the requester already discovered the holder, so no namespace.
1879            decdn_protocol::client::NO_NAMESPACE,
1880            byte_offset,
1881            timestamp_us,
1882            max_blob_size_bytes,
1883            max_rate_per_mb,
1884            deadlines,
1885            ledger,
1886            // Shared concurrent pulls interleave many blobs on one channel; a
1887            // single unified byte-progress readout would be meaningless, so this
1888            // path never reports progress.
1889            None,
1890        ),
1891    )
1892    .await
1893}
1894
1895/// Where an [`open_stream`] gets its QUIC connection.
1896enum ConnSource<'a> {
1897    /// Dial a fresh one-shot connection to `target`. The pull owns it and closes
1898    /// it on its terminal method (`finish`/`abort`/drop).
1899    Dial {
1900        endpoint: &'a Endpoint,
1901        target: EndpointAddr,
1902        /// The runtime the dial and the connection's driver run on; `None` runs
1903        /// them on the caller's (see [`connection::dial`]).
1904        runtime: Option<&'a tokio::runtime::Handle>,
1905    },
1906    /// Reuse a caller-owned [`WarmConnection`]'s connection. The pull borrows it
1907    /// and leaves it open for the next hash; the [`WarmConnection`] closes it once.
1908    Reuse(&'a iroh::endpoint::Connection),
1909}
1910
1911/// The OPEN stage of a `cdn/client/v1` pull, the single request site behind
1912/// [`open_progressive_pull`]: get the connection from `source` (dial a fresh one,
1913/// or reuse a warm one), open the bi-stream, send the [`StreamRequest`], then read
1914/// and verify the signed [`StreamResponse`]. Returns the live connection, its
1915/// streams, and the verified response for the caller to stream from.
1916///
1917/// **Bounded as a whole by `open`** (#1134), and that bound lives HERE rather than
1918/// in the caller for a reason worth stating: the production pull paths run with no
1919/// overall wall-clock cap, so that a blob of any size can complete as long as bytes
1920/// keep arriving. Leaving the open to "whatever the caller wraps us in" therefore
1921/// means leaving it *unbounded* — and this is precisely the stage where a peer can
1922/// accept a connection, take our request, and then say nothing at all. Such a peer
1923/// would hang the pull until the QUIC idle timeout.
1924///
1925/// The stall bound cannot cover this stage: it measures inactivity BETWEEN bytes,
1926/// and here no byte has arrived yet. A wall clock is the right tool because the
1927/// open is bounded work whose duration does not scale with the blob.
1928///
1929/// A `slash_sig` that does not recover to `expected_signer`, a mismatched echoed
1930/// field, or a zero rate fails here (see [`verify_response`]) — before any byte is
1931/// paid for. A refusal (`body.ok == false`) is returned to the caller intact, since
1932/// only the caller knows how to classify it.
1933#[allow(clippy::too_many_arguments)]
1934async fn open_stream(
1935    source: ConnSource<'_>,
1936    ctx: &PoolContext,
1937    slash_domain: &Eip712Domain,
1938    expected_signer: Address,
1939    hash: [u8; 32],
1940    namespace_id: [u8; 32],
1941    byte_offset: u64,
1942    byte_len: u64,
1943    timestamp_us: u64,
1944    open: Duration,
1945) -> anyhow::Result<(
1946    iroh::endpoint::Connection,
1947    SendStream,
1948    RecvStream,
1949    StreamResponse,
1950    StreamResponseExt,
1951)> {
1952    tokio::time::timeout(open, async move {
1953        let conn = match source {
1954            ConnSource::Dial {
1955                endpoint,
1956                target,
1957                runtime,
1958            } => connection::dial(endpoint, target, runtime, "connect failed").await?,
1959            // Already dialled and warm — reuse the handle. A fresh `open_bi` below
1960            // gives this hash its own stream.
1961            ConnSource::Reuse(conn) => conn.clone(),
1962        };
1963        let (mut send, mut recv) = conn
1964            .open_bi()
1965            .await
1966            .map_err(|e| rate_limited::transport_error("open_bi failed", e))?;
1967
1968        let req = StreamRequest {
1969            hash,
1970            // The serving node routes on this for its pull-through authorized-origin
1971            // gate and origin-directory fallback (ADR 005 §Namespace routing). A
1972            // client fetch passes the namespace it published under; a node-to-node
1973            // pull to a DHT-discovered holder passes `NO_NAMESPACE` (0) — the holder
1974            // already has the bytes (ADR 002 §Retrieval by namespace) — while a pull
1975            // to a directory-discovered cold origin passes the served namespace so
1976            // that origin's gate resolves and it can fill from its own backend.
1977            namespace_id,
1978            pool_id: ctx.pool_id.into(),
1979            byte_offset,
1980            // `0` = whole-tail fetch; a caller requesting a bounded middle gap
1981            // (the gap-driven driver, #1608, via `PeerSource`) passes the gap's
1982            // length so the server scopes both the serve and the payment to it.
1983            byte_len,
1984            timestamp_us,
1985        };
1986        // Two-phase encode (ADR 005): attach the client identity binding when the
1987        // context carries one, so the serving node can prove channel ownership and
1988        // authorize a cache-miss origin pull (#1115). Absent ⇒ no ext bytes: the
1989        // base frame alone (unbound node-to-node / registered-client path).
1990        let ext = client_binding_ext(ctx);
1991        let payload = decdn_protocol::encode_stream_request(&req, ext.as_ref())
1992            .map_err(|e| anyhow::anyhow!("encode stream request: {e}").context(LocalPullFault))?;
1993        write_frame(&mut send, &payload)
1994            .await
1995            .map_err(|e| rate_limited::transport_error("write stream request", e))?;
1996
1997        let (resp, resp_ext) = read_stream_response(&mut recv).await?;
1998        verify_response(
1999            &resp,
2000            &resp_ext,
2001            slash_domain,
2002            expected_signer,
2003            hash,
2004            ctx.pool_id,
2005            timestamp_us,
2006        )?;
2007        Ok((conn, send, recv, resp, resp_ext))
2008    })
2009    .await
2010    .map_err(|_| anyhow::Error::new(PullTimeout { after: open }))?
2011}
2012
2013/// Wallet-less resume (issue #1481 §5): the maximum number of times a fetch will reopen a fresh
2014/// stream after a rejection carrying a watermark-gated bundle
2015/// ([`VoucherRejectReason::is_watermark_gated`]). Bounds a node that keeps rejecting (a buggy or
2016/// adversarial peer echoing a bundle that never lets the client catch up) to a handful of round
2017/// trips rather than looping forever; a healthy self-heal needs exactly one.
2018///
2019/// Public because the STREAMING fetch (#1120) drives its own reopen loop — it
2020/// owns the output file and must rewind it before each retry, which this crate
2021/// cannot do for it — and both loops must agree on the bound.
2022#[doc(hidden)]
2023pub const MAX_RESUME_ATTEMPTS: u32 = 3;
2024
2025/// In-memory fetch with wallet-less resume (issue #1481 §5): if a mid-stream
2026/// voucher rejection carries a signer-verified [`WatermarkBundle`] for one of
2027/// the four regression/exhaustion reasons, reseed `ledger` from it and reopen
2028/// the pull — at the **same** `byte_offset` the caller originally requested,
2029/// not `bundle.bytes_delivered` — instead of surfacing the rejection as
2030/// terminal.
2031///
2032/// `bundle.bytes_delivered` is deliberately NOT used as the retry's wire
2033/// `byte_offset`, even though that is what makes a `WatermarkBundle` look
2034/// resumable at a glance. The two are different axes: `bytes_delivered` is
2035/// the CHANNEL's cumulative payment counter (used to reconstruct each
2036/// voucher's EIP-712 `bytesDelivered`, ADR 005 — [`Voucher`] carries no such
2037/// field on the wire), not a position within THIS blob's byte range. A
2038/// channel can fund many blobs; jumping the wire offset ahead to the
2039/// channel's cumulative would, for a caller that requested `byte_offset == 0`,
2040/// silently return a TRUNCATED tail instead of the full blob the caller is
2041/// relying on getting back. The bytes already decoded in the failed attempt
2042/// were dropped with it, so there is nothing to legitimately splice a jump
2043/// with. Retrying at the caller's original offset is always safe and is the
2044/// only thing this bundle field is actually needed for: fixing the ledger's
2045/// amount/bytes BASELINE so the resumed stream's vouchers verify against what
2046/// the node now expects, not re-deriving where in the blob to resume.
2047///
2048/// This is the retry loop for this case on the in-memory path —
2049/// [`UpstreamPull::pay_one`] never retries itself, because the stream it holds
2050/// is already dead by the time a mid-stream `VoucherRejected` reaches it (the
2051/// node finishes its send side before replying,
2052/// `handlers/client/wire.rs::write_reject`); a fresh stream can only be opened
2053/// by whoever owns the connection, which is here.
2054///
2055/// Every `stream_fetch*` wrapper goes through here, so a resumable rejection is
2056/// retried transparently and the caller only ever sees the FINAL outcome
2057/// (success, or the original terminal error once [`MAX_RESUME_ATTEMPTS`] is
2058/// exhausted or the reason/bundle isn't eligible).
2059///
2060/// The production callers do not use this wrapper: the gap-driven
2061/// [`driver::drive`] reaches the same reseed loop around [`open_progressive_pull`].
2062///
2063/// A bundle-less rejection, a non-gated reason, or a bundle that fails
2064/// [`WatermarkBundle::validate`] (malformed `last_signature` length) is
2065/// never treated as resumable and is returned to the caller unchanged on
2066/// the first attempt.
2067#[cfg(any(test, feature = "test-util"))]
2068#[allow(clippy::too_many_arguments)]
2069async fn fetch_inner(
2070    endpoint: &Endpoint,
2071    target: EndpointAddr,
2072    ctx: &PoolContext,
2073    slash_domain: &Eip712Domain,
2074    expected_signer: Address,
2075    hash: [u8; 32],
2076    namespace_id: [u8; 32],
2077    byte_offset: u64,
2078    timestamp_us: u64,
2079    max_blob_size_bytes: u64,
2080    max_rate_per_mb: u64,
2081    deadlines: PullDeadlines,
2082    ledger: &Arc<PoolLedger>,
2083    on_progress: Option<&ProgressCallback>,
2084) -> anyhow::Result<Bytes> {
2085    for attempt in 0..=MAX_RESUME_ATTEMPTS {
2086        let result = fetch_in_memory_once(
2087            endpoint,
2088            target.clone(),
2089            ctx,
2090            Arc::clone(ledger),
2091            slash_domain,
2092            expected_signer,
2093            hash,
2094            namespace_id,
2095            byte_offset,
2096            timestamp_us,
2097            max_blob_size_bytes,
2098            max_rate_per_mb,
2099            deadlines,
2100            on_progress,
2101        )
2102        .await;
2103        let Err(e) = result else {
2104            return result;
2105        };
2106        if attempt == MAX_RESUME_ATTEMPTS {
2107            return Err(e);
2108        }
2109        // A bundle that proves no desync (see `heal_watermark_desync`) is not worth
2110        // the resume budget: surface the real error instead.
2111        let healed = match rejection_watermark(&e, ctx) {
2112            Some(watermark) => heal_watermark_desync(&e, watermark, ledger).await,
2113            None => None,
2114        };
2115        let Some(healed) = healed else {
2116            return Err(e);
2117        };
2118        tracing::debug!(
2119            attempt,
2120            byte_offset,
2121            ?healed,
2122            "voucher rejection carried an authenticated watermark bundle; retrying at the same \
2123             byte_offset"
2124        );
2125    }
2126    // Unreachable: the loop above always returns on both the `Ok` and every `Err`
2127    // branch (either directly or after `attempt == MAX_RESUME_ATTEMPTS`
2128    // triggers on the final iteration). Kept as a typed bail rather than
2129    // `unreachable!()`/`panic!()` per the workspace anti-panic policy.
2130    Err(anyhow::anyhow!("resume loop exited without returning"))
2131}
2132
2133/// One attempt of the in-memory `stream_fetch*` path: open a progressive pull,
2134/// feed its wire bytes through the bao verifying decoder AS THEY ARRIVE, and
2135/// return the decoded plaintext span `[byte_offset, total_bytes)`.
2136///
2137/// This is the same receive loop the production callers drive
2138/// ([`UpstreamPull::next_chunk`] under a [`PullReader`]) with an in-memory `Vec`
2139/// standing in for the ranged store / cache admit; there is no second receive
2140/// implementation. A corrupt chunk group therefore aborts the pull at that group
2141/// (ADR 038 §Receive side), before the rest of the stream is pulled or paid for —
2142/// so a peer that signs an inflated `total_bytes` and streams framed garbage can
2143/// bill at most the bytes that arrived before the first unverifiable group, never
2144/// its claim.
2145///
2146/// # Errors
2147///
2148/// Everything [`open_progressive_pull`] raises, plus [`HashMismatch`] for a
2149/// paid-but-corrupt delivery (including a non-empty root claimed against an empty
2150/// blob, #1054), the typed pull faults [`PullReader`] stashes (stall, refusal,
2151/// voucher rejection), a truncated stream, or a short delivery at `finish`.
2152#[cfg(any(test, feature = "test-util"))]
2153#[allow(clippy::too_many_arguments)]
2154async fn fetch_in_memory_once(
2155    endpoint: &Endpoint,
2156    target: EndpointAddr,
2157    ctx: &PoolContext,
2158    ledger: Arc<PoolLedger>,
2159    slash_domain: &Eip712Domain,
2160    expected_signer: Address,
2161    hash: [u8; 32],
2162    namespace_id: [u8; 32],
2163    byte_offset: u64,
2164    timestamp_us: u64,
2165    max_blob_size_bytes: u64,
2166    max_rate_per_mb: u64,
2167    deadlines: PullDeadlines,
2168    on_progress: Option<&ProgressCallback>,
2169) -> anyhow::Result<Bytes> {
2170    let (header, pull) = open_progressive_pull(
2171        endpoint,
2172        target,
2173        ctx,
2174        ledger,
2175        slash_domain,
2176        expected_signer,
2177        hash,
2178        namespace_id,
2179        byte_offset,
2180        timestamp_us,
2181        max_blob_size_bytes,
2182        max_rate_per_mb,
2183        deadlines,
2184        // Whole tail: the in-memory wrapper has no store to compute gaps against.
2185        0,
2186        // One long-lived test runtime: the dial runs on it.
2187        None,
2188    )
2189    .await?;
2190    let total_bytes = header.total_bytes;
2191    // A 0-byte blob (#1054) aligns to an empty range the decoder cannot anchor, so
2192    // the empty stream must be proven against the empty root explicitly rather than
2193    // accepted for an arbitrary one (see [`reject_empty_claim_for_nonempty_root`]).
2194    if total_bytes == 0 {
2195        reject_empty_claim_for_nonempty_root(total_bytes, hash)?;
2196        pull.finish().await?;
2197        return Ok(Bytes::new());
2198    }
2199    let aligned = align_range(byte_offset, 0, total_bytes)
2200        .map_err(|e| anyhow::anyhow!("range alignment: {e}").context(LocalPullFault))?;
2201    let reader = PullReader::new(pull);
2202    let (plaintext, reader) =
2203        decode_to_vec(hash, total_bytes, &aligned, reader, on_progress).await?;
2204    // Wire completeness (the full promised bao wire size arrived before
2205    // `StreamEnd`) and a clean close; the watermark it returns is read back from
2206    // the shared ledger by the caller, so it is not needed here.
2207    reader.into_inner().finish().await?;
2208    // The decoded buffer spans the aligned superset `[fetch_start, total_bytes)`;
2209    // trim back to the caller's requested span. Guard the bound explicitly:
2210    // `Bytes::slice` panics out of range, and a short decode must surface as a
2211    // clean error.
2212    let lead = usize::try_from(byte_offset.saturating_sub(aligned.fetch_start()))?;
2213    let bytes = Bytes::from(plaintext);
2214    if lead > bytes.len() {
2215        anyhow::bail!("decoded range shorter than requested span");
2216    }
2217    Ok(bytes.slice(lead..))
2218}
2219
2220/// Like [`fetch_in_memory_once`], but opens the pull on a caller-owned
2221/// [`WarmConnection`] so the in-memory `stream_fetch_on` wrapper can prove
2222/// connection reuse across hashes (#1848).
2223///
2224/// # Errors
2225///
2226/// The same set as [`fetch_in_memory_once`].
2227#[cfg(any(test, feature = "test-util"))]
2228#[allow(clippy::too_many_arguments)]
2229async fn fetch_in_memory_once_on(
2230    warm: &WarmConnection,
2231    ctx: &PoolContext,
2232    ledger: Arc<PoolLedger>,
2233    slash_domain: &Eip712Domain,
2234    expected_signer: Address,
2235    hash: [u8; 32],
2236    namespace_id: [u8; 32],
2237    byte_offset: u64,
2238    timestamp_us: u64,
2239    max_blob_size_bytes: u64,
2240    max_rate_per_mb: u64,
2241    deadlines: PullDeadlines,
2242    on_progress: Option<&ProgressCallback>,
2243) -> anyhow::Result<Bytes> {
2244    let (header, pull) = open_progressive_pull_on(
2245        warm.connection(),
2246        connection::ConnOwner::Borrowed,
2247        ctx,
2248        ledger,
2249        slash_domain,
2250        expected_signer,
2251        hash,
2252        namespace_id,
2253        byte_offset,
2254        timestamp_us,
2255        max_blob_size_bytes,
2256        max_rate_per_mb,
2257        deadlines,
2258        // Whole tail: the in-memory wrapper has no store to compute gaps against.
2259        0,
2260    )
2261    .await?;
2262    let total_bytes = header.total_bytes;
2263    if total_bytes == 0 {
2264        reject_empty_claim_for_nonempty_root(total_bytes, hash)?;
2265        pull.finish().await?;
2266        return Ok(Bytes::new());
2267    }
2268    let aligned = align_range(byte_offset, 0, total_bytes)
2269        .map_err(|e| anyhow::anyhow!("range alignment: {e}").context(LocalPullFault))?;
2270    let reader = PullReader::new(pull);
2271    let (plaintext, reader) =
2272        decode_to_vec(hash, total_bytes, &aligned, reader, on_progress).await?;
2273    reader.into_inner().finish().await?;
2274    let lead = usize::try_from(byte_offset.saturating_sub(aligned.fetch_start()))?;
2275    let bytes = Bytes::from(plaintext);
2276    if lead > bytes.len() {
2277        anyhow::bail!("decoded range shorter than requested span");
2278    }
2279    Ok(bytes.slice(lead..))
2280}
2281
2282/// Feed a bao interleaved wire stream through the verifying decoder, appending
2283/// each verified leaf to an in-memory buffer, and return the buffer plus the
2284/// reader for the caller to finish. Every chunk group is checked against the
2285/// content-hash root `root` as it is decoded, so a corrupt group — at any offset,
2286/// including a resumed tail — is rejected at that group without needing earlier
2287/// bytes or the rest of the stream (ADR 038 §Receive side).
2288///
2289/// The decoded buffer spans the chunk-group-aligned **superset**
2290/// `[aligned.fetch_start(), aligned.fetch_end())` the server serves (a bao proof
2291/// anchors whole 16 KiB groups); the caller trims the lead. The buffer is grown
2292/// leaf by leaf rather than pre-sized: `aligned` derives from the peer's
2293/// unverified `total_bytes`, so a capacity hint would let an inflated claim drive
2294/// an over-allocation before a single byte verifies.
2295///
2296/// `on_progress`, when set, is called with `(content_position, total_bytes)` after
2297/// each verified leaf lands — the same content-byte unit [`driver::drive`] reports.
2298///
2299/// Same loop shape as [`ClientRangedStore::ingest_stream`], minus the durable
2300/// checkpointing; a typed fault the reader stashed ([`StashedFault`]) takes
2301/// precedence over the decoder's own complaint, and a decode failure is
2302/// classified by [`sink::classify_decode_error`] ([`HashMismatch`] for a
2303/// verification failure, a truncation error for a short stream).
2304///
2305/// # Errors
2306///
2307/// The reader's stashed typed fault, [`HashMismatch`], or a truncation / IO fault.
2308#[cfg(any(test, feature = "test-util"))]
2309async fn decode_to_vec<R>(
2310    root: [u8; 32],
2311    total_bytes: u64,
2312    aligned: &AlignedRange,
2313    reader: R,
2314    on_progress: Option<&ProgressCallback>,
2315) -> anyhow::Result<(Vec<u8>, R)>
2316where
2317    R: iroh_io::AsyncStreamReader + StashedFault + Send,
2318{
2319    let tree = BaoTree::new(total_bytes, IROH_BLOCK_SIZE);
2320    let root = blake3::Hash::from_bytes(root);
2321    let mut decoder = ResponseDecoder::new(root, aligned.chunk_ranges().clone(), tree, reader);
2322    let mut out = Vec::new();
2323    loop {
2324        match decoder.next().await {
2325            ResponseDecoderNext::More((rest, Ok(BaoContentItem::Leaf(leaf)))) => {
2326                out.extend_from_slice(&leaf.data);
2327                if let Some(cb) = on_progress {
2328                    let position = aligned
2329                        .fetch_start()
2330                        .saturating_add(u64::try_from(out.len()).unwrap_or(u64::MAX));
2331                    cb(position, total_bytes);
2332                }
2333                decoder = rest;
2334            }
2335            ResponseDecoderNext::More((rest, Ok(BaoContentItem::Parent(_)))) => decoder = rest,
2336            ResponseDecoderNext::More((rest, Err(decode_err))) => {
2337                let mut r = rest.finish();
2338                if let Some(fault) = r.take_fault() {
2339                    return Err(fault);
2340                }
2341                return Err(sink::classify_decode_error(decode_err));
2342            }
2343            ResponseDecoderNext::Done(mut r) => {
2344                if let Some(fault) = r.take_fault() {
2345                    return Err(fault);
2346                }
2347                return Ok((out, r));
2348            }
2349        }
2350    }
2351}
2352
2353/// The security-critical core shared by every "did WE sign this?" check: does `signature` recover
2354/// to `expected` over `voucher` under `domain`?
2355///
2356/// A node's claim about a lane's watermark arrives over an unauthenticated application-level
2357/// message — a mid-stream `StreamError::VoucherRejected` on the fetch path (#1042) — so the claimed
2358/// `amount`/`bytes_delivered` are attacker-controllable on the wire. Without this check a malicious
2359/// or buggy upstream could hand back an inflated watermark and have the client act on it: `reseed`
2360/// its ledger and then sign a voucher for the echoed `amount + delta` on the retried pull. That is
2361/// a voucher this client genuinely holds the key to sign and the node can redeem on-chain up to the
2362/// deposit, draining the pool while delivering ~nothing.
2363///
2364/// The client's OWN last-accepted voucher signature closes that hole: a node can only echo back a
2365/// signature the client itself produced, so a tuple that verifies is by construction one this client
2366/// already committed to. That the node's stored signature always matches its stored tuple is the
2367/// write-side invariant of the seller-side lane state, which writes `last_amount`/`last_bytes` and
2368/// the accepted signature together and only after verifying the signature against the lane's pinned
2369/// capability `signer`. A signature that does not recover to `expected` is treated as a hostile or
2370/// corrupt echo, never as evidence.
2371///
2372/// Callers MUST verify over the exact tuple they are about to adopt as their new committed baseline
2373/// — never a tuple derived from it — so there is no window in which the proof covers one state and
2374/// the action commits another. Taking the whole [`Voucher`] rather than its fields loose is what
2375/// lets a caller verify and then act on *the same value*.
2376#[must_use]
2377pub(crate) fn voucher_signed_by(
2378    voucher: &Voucher,
2379    signature: &[u8],
2380    expected: Address,
2381    domain: &Eip712Domain,
2382) -> bool {
2383    let Ok(signature) = Signature::try_from(signature) else {
2384        return false;
2385    };
2386    SignedVoucher {
2387        voucher: voucher.clone(),
2388        signature,
2389    }
2390    .recover_signer(domain)
2391    .is_ok_and(|recovered| recovered == expected)
2392}
2393
2394/// Extract a resumable, AUTHENTICATED [`WatermarkBundle`] from a pull error, or `None` if the
2395/// error is not an `UpstreamVoucherRejected`, its reason is not
2396/// [`VoucherRejectReason::is_watermark_gated`], it carries no bundle, the bundle fails
2397/// [`WatermarkBundle::validate`] (malformed `last_signature` length), or either of the two
2398/// security-critical checks fails: `last_signature` must recover to this client's OWN
2399/// voucher-signing address over the bundle's anchor and chain section, and the bundle's `tip`
2400/// must PROVE its `verified_index`, by hashing forward to the root that signature covers.
2401///
2402/// Both halves of the bundle are folded into money a resuming signer signs, so both have to be
2403/// evidence. The anchor half is the client's own signature ([`voucher_signed_by`]; see its doc
2404/// for why acting on an unverified watermark is a channel-draining hole rather than a robustness
2405/// nicety). The chain half carries no signature at all, which is what makes the tip check the
2406/// only thing standing between an inflated `verified_index` and a client that signs it.
2407pub(crate) fn resumable_watermark<'a>(
2408    err: &'a anyhow::Error,
2409    ctx: &PoolContext,
2410) -> Option<&'a WatermarkBundle> {
2411    let rejected = err.downcast_ref::<UpstreamVoucherRejected>()?;
2412    if !rejected.reason.is_watermark_gated() {
2413        return None;
2414    }
2415    let bundle = rejected.bundle.as_ref()?;
2416    if bundle.validate().is_err() {
2417        return None;
2418    }
2419    let claimed = Voucher {
2420        pool_id: ctx.pool_id,
2421        signer: ctx.client_signer.address(),
2422        provider: ctx.provider,
2423        amount: U256::from(bundle.amount),
2424        bytes_delivered: U256::from(bundle.bytes_delivered),
2425        chain_root: B256::from(bundle.chain_root),
2426        chunk_price: U256::from(bundle.chunk_price),
2427    };
2428    if !voucher_signed_by(
2429        &claimed,
2430        &bundle.last_signature,
2431        ctx.client_signer.address(),
2432        &ctx.voucher_domain,
2433    ) {
2434        return None;
2435    }
2436    frontier_is_proved(bundle).then_some(bundle)
2437}
2438
2439/// How [`heal_watermark_desync`] resolved a rejection's authenticated watermark.
2440#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2441pub(crate) enum Healed {
2442    /// The bundle was AHEAD of our committed watermark: the node holds a voucher
2443    /// we lost. [`PoolLedger::reseed`] moved us up to it.
2444    Reseeded,
2445    /// The bundle was BEHIND our committed watermark on `amount`, and the
2446    /// ledger moved to it: on `Underpaid`, we hold vouchers the node never
2447    /// accepted ([`PoolLedger::rebase`]); on `BytesRegression`, the bundle is
2448    /// ahead of us on bytes, because the lane resumed below a voucher the node
2449    /// holds ([`PoolLedger::rebase_ahead_on_bytes`]).
2450    Rebased,
2451    /// The ledger already covers this rejection's watermark: an `Underpaid` or
2452    /// `BytesRegression` for a voucher signed before the latest rebase, or an
2453    /// `UnderFold`, `AmountRegression` or `BytesRegression` whose bundle the
2454    /// ledger has reached. The watermark
2455    /// did not move, and the pull retries from the ledger. An `UnderFold` that
2456    /// no heal has taken yet also retires the live chain the node refused
2457    /// ([`PoolLedger::retire_unadopted_chain`]), so the retry opens a fresh one.
2458    Stale,
2459}
2460
2461/// The authenticated watermark a voucher rejection carries, as the cumulative a
2462/// ledger re-anchors to, or `None` when [`resumable_watermark`] refuses it.
2463#[must_use]
2464pub(crate) fn rejection_watermark(err: &anyhow::Error, ctx: &PoolContext) -> Option<Cumulative> {
2465    resumable_watermark(err, ctx).map(Cumulative::from)
2466}
2467
2468/// Heal the watermark desync a voucher rejection reports, from `watermark` — the
2469/// authenticated bundle [`rejection_watermark`] extracted — and say how, or
2470/// `None` when it proves no desync and the caller must surface the real error.
2471///
2472/// A bundle ahead of our committed watermark reseeds, whatever the reason. A
2473/// bundle that does not advance our amount rebases on
2474/// [`VoucherRejectReason::Underpaid`], and on a
2475/// [`VoucherRejectReason::BytesRegression`] whose bundle is ahead of us on
2476/// bytes (see [`heal_bytes_regression`]). A rebase applies only to a voucher
2477/// signed under the ledger's current generation; see [`PoolLedger::rebase`].
2478/// On [`VoucherRejectReason::UnderFold`] or
2479/// [`VoucherRejectReason::AmountRegression`] it retries without moving the
2480/// ledger (below). On every other reason it proves no desync: the node attaches
2481/// a bundle to every watermark-gated rejection once a voucher is accepted, so an
2482/// exhausted lane echoes our own watermark straight back.
2483///
2484/// An `UnderFold` or `AmountRegression` whose folded bundle does not advance our
2485/// amount splits two ways. Both reasons say a proof trailed the node's
2486/// watermark. When our ledger already covers the bundle on both axes, the
2487/// stream retries ([`Healed::Stale`]): either a sibling stream took the same
2488/// rejection and healed first, or the ledger metered reveals under a root the
2489/// node refused, which an `UnderFold` retires. When it covers the amount but
2490/// not the bytes, the ledger paid the node's whole claim but signed fewer bytes
2491/// than the node holds. No bundle can heal that; it is terminal and logged at
2492/// `warn!`.
2493pub(crate) async fn heal_watermark_desync(
2494    err: &anyhow::Error,
2495    watermark: Cumulative,
2496    ledger: &PoolLedger,
2497) -> Option<Healed> {
2498    if ledger.reseed(watermark) {
2499        tracing::info!(
2500            amount = %watermark.amount,
2501            bytes = %watermark.bytes,
2502            "voucher rejection carried a watermark ahead of ours; reseeded"
2503        );
2504        return Some(Healed::Reseeded);
2505    }
2506    let rejected = err.downcast_ref::<UpstreamVoucherRejected>()?;
2507    if matches!(
2508        rejected.reason,
2509        VoucherRejectReason::UnderFold | VoucherRejectReason::AmountRegression
2510    ) {
2511        return already_covered(rejected.reason, watermark, ledger).await;
2512    }
2513    match rejected.reason {
2514        VoucherRejectReason::Underpaid => {
2515            let outcome = ledger.rebase(watermark, rejected.proof_generation).await;
2516            rebase_outcome(
2517                rejected.reason,
2518                outcome,
2519                watermark,
2520                rejected.proof_generation,
2521            )
2522        }
2523        VoucherRejectReason::BytesRegression => {
2524            heal_bytes_regression(watermark, rejected.proof_generation, ledger).await
2525        }
2526        _ => None,
2527    }
2528}
2529
2530/// Resolve a `BytesRegression` whose bundle does not advance our amount.
2531///
2532/// A bundle ahead of us on bytes is our own voucher that the node holds and
2533/// our ledger does not. The lane resumed from a lower anchor, typically the
2534/// chain watermark after the payer's store lost the lane, while the node holds
2535/// an unredeemed voucher above it. The ledger priced its spans from that lower
2536/// anchor, so its amount passed the node's anchor while its bytes still
2537/// trailed it. The rebase moves the ledger to the node's anchor
2538/// ([`PoolLedger::rebase_ahead_on_bytes`]). The payer never pays more than it
2539/// signed for, because the anchor is its own voucher.
2540///
2541/// A bundle the ledger already covers on both axes retries from the ledger
2542/// ([`Healed::Stale`]). The rejected voucher trailed the ledger: a sibling
2543/// stream healed first, or a sibling voucher committed past the node's anchor,
2544/// or the voucher was signed before the latest rebase. Every voucher the
2545/// ledger signs next is at or above the node's anchor on both axes. A
2546/// `BytesRegression` with no bundle is the single-signer fault that no heal
2547/// takes; it never reaches this function.
2548async fn heal_bytes_regression(
2549    watermark: Cumulative,
2550    proof_generation: Option<u64>,
2551    ledger: &PoolLedger,
2552) -> Option<Healed> {
2553    let outcome = ledger
2554        .rebase_ahead_on_bytes(watermark, proof_generation)
2555        .await;
2556    if outcome != Rebase::Refused {
2557        return rebase_outcome(
2558            VoucherRejectReason::BytesRegression,
2559            outcome,
2560            watermark,
2561            proof_generation,
2562        );
2563    }
2564    let committed = ledger.committed();
2565    if committed.amount >= watermark.amount && committed.bytes >= watermark.bytes {
2566        tracing::debug!(
2567            bundle_amount = %watermark.amount,
2568            bundle_bytes = %watermark.bytes,
2569            committed_amount = %committed.amount,
2570            committed_bytes = %committed.bytes,
2571            ?proof_generation,
2572            "bytes-regression rejection carried a watermark our ledger already covers; \
2573             retrying from the ledger"
2574        );
2575    } else {
2576        // The bundle is ahead of us on amount, which `reseed` refused a moment
2577        // ago: a rewind landed in between. The retry's next rejection carries
2578        // the same bundle, and the reseed takes it then.
2579        tracing::warn!(
2580            bundle_amount = %watermark.amount,
2581            bundle_bytes = %watermark.bytes,
2582            committed_amount = %committed.amount,
2583            committed_bytes = %committed.bytes,
2584            ?proof_generation,
2585            "bytes-regression rejection carried a watermark ahead of our ledger on amount \
2586             that neither a reseed nor a rebase took; retrying"
2587        );
2588    }
2589    Some(Healed::Stale)
2590}
2591
2592/// Resolve a trailing-proof rejection (`UnderFold` or `AmountRegression`) whose
2593/// folded bundle does not advance our amount. When our ledger covers its bytes
2594/// too, the stream retries. Otherwise the ledger covers the amount but not the
2595/// bytes, which no bundle can heal.
2596///
2597/// A covering ledger is in one of two states. Either a sibling stream took the
2598/// same rejection and healed the lane first, or the ledger meters a root the
2599/// node refused to adopt. The second state follows an `UnderFold` on a fresh
2600/// root: the stream released reveals under it before the rejection landed, and
2601/// only the last one rewound. A retry would re-anchor under that root and draw
2602/// the same rejection, so an `UnderFold` retires it
2603/// ([`PoolLedger::retire_unadopted_chain`]), unless a heal has already taken
2604/// this bundle.
2605async fn already_covered(
2606    reason: VoucherRejectReason,
2607    watermark: Cumulative,
2608    ledger: &PoolLedger,
2609) -> Option<Healed> {
2610    let committed = ledger.committed();
2611    if committed.bytes < watermark.bytes {
2612        tracing::warn!(
2613            bundle_amount = %watermark.amount,
2614            bundle_bytes = %watermark.bytes,
2615            committed_amount = %committed.amount,
2616            committed_bytes = %committed.bytes,
2617            "trailing-proof rejection carried a watermark our amount covers but our bytes \
2618             do not; the ledger paid the amount but not the bytes"
2619        );
2620        return None;
2621    }
2622    let retired = if reason == VoucherRejectReason::UnderFold {
2623        ledger.retire_unadopted_chain(watermark).await
2624    } else {
2625        None
2626    };
2627    log_covered(watermark, committed, retired);
2628    Some(Healed::Stale)
2629}
2630
2631/// Log how [`already_covered`] resolved a rejection whose watermark our
2632/// `committed` ledger covers, and the root it `retired`, if any.
2633fn log_covered(watermark: Cumulative, committed: Cumulative, retired: Option<B256>) {
2634    if let Some(root) = retired {
2635        tracing::info!(
2636            retired_root = %root,
2637            bundle_amount = %watermark.amount,
2638            bundle_bytes = %watermark.bytes,
2639            committed_amount = %committed.amount,
2640            committed_bytes = %committed.bytes,
2641            "under-fold rejection: the node never adopted our live chain, and our ledger \
2642             already covers its watermark; folded the ledger and retired the chain, so the \
2643             retry opens a fresh one"
2644        );
2645    } else if committed == watermark {
2646        tracing::debug!(
2647            amount = %watermark.amount,
2648            bytes = %watermark.bytes,
2649            "trailing-proof rejection already healed by a sibling stream; retrying"
2650        );
2651    } else {
2652        tracing::debug!(
2653            bundle_amount = %watermark.amount,
2654            bundle_bytes = %watermark.bytes,
2655            committed_amount = %committed.amount,
2656            committed_bytes = %committed.bytes,
2657            "trailing-proof rejection carried a watermark our ledger is already past; \
2658             retrying from the ledger"
2659        );
2660    }
2661}
2662
2663/// Log what a [`PoolLedger::rebase`] did and say how the heal resolved.
2664fn rebase_outcome(
2665    reason: VoucherRejectReason,
2666    outcome: Rebase,
2667    watermark: Cumulative,
2668    proof_generation: Option<u64>,
2669) -> Option<Healed> {
2670    match outcome {
2671        Rebase::Rebased { from } => {
2672            tracing::warn!(
2673                ?reason,
2674                from_amount = %from.amount,
2675                from_bytes = %from.bytes,
2676                to_amount = %watermark.amount,
2677                to_bytes = %watermark.bytes,
2678                "upstream refused our vouchers: the watermark it accepted differs from our \
2679                 lane's; rebased to it"
2680            );
2681            Some(Healed::Rebased)
2682        }
2683        Rebase::Stale => {
2684            tracing::debug!(
2685                ?reason,
2686                ?proof_generation,
2687                "rejection of a voucher signed before the latest rebase; retrying from the \
2688                 healed anchor"
2689            );
2690            Some(Healed::Stale)
2691        }
2692        Rebase::Refused => None,
2693    }
2694}
2695
2696/// Whether the bundle's `tip` proves the `verified_index` it reports:
2697/// `keccak^verified_index(tip) == chain_root`.
2698///
2699/// A bundle reports two things and the resuming signer folds BOTH into the amount it re-signs.
2700/// The anchor half is covered by the client's own signature. The chain half is covered by
2701/// nothing — `verified_index` is a number the node writes, and the signature it echoes is over a
2702/// voucher that contains no index. A node that reported 255 where the client released 3 would
2703/// otherwise collect `252 × chunk_price` for chunks it never delivered, from a client that signed
2704/// for them itself.
2705///
2706/// The tip closes that, and it is the only thing that can: a preimage is unforgeable, so the
2707/// deepest one the node can present is the deepest one it was actually GIVEN, and hashing it
2708/// forward to a root the client's own signature commits to turns `verified_index` from a claim
2709/// into a proof. The walk is `verified_index` keccaks against local state — no signer, no round
2710/// trip.
2711///
2712/// `verified_index == 0` folds nothing, so it needs no proof and carries no tip (the node sends
2713/// all-zero). That is the never-metered and sealed case, and the only one where an all-zero tip
2714/// is legitimate.
2715#[must_use]
2716fn frontier_is_proved(bundle: &WatermarkBundle) -> bool {
2717    bundle.verified_index == 0
2718        || decdn_incentive::chain::verify_forward(
2719            B256::from(bundle.tip),
2720            bundle.verified_index,
2721            B256::from(bundle.chain_root),
2722        )
2723}
2724
2725/// Whether a failed open of a bounded range could mean only that the range's
2726/// START is past the blob's end: the size it was cut from was wrong. An end
2727/// past the blob clamps and serves rather than failing, so this fires only on
2728/// a start with no bytes to serve.
2729///
2730/// A holder refuses such a range before it signs (`RangeNotSatisfiable`, which
2731/// reaches the wire as [`StreamError::Declined`]). That refusal names only the
2732/// class, so it removes the holder for this fetch and does not qualify here. A
2733/// relay signs its own size instead, and the range then fails to align against
2734/// it here ([`decdn_bao_range::RangeVerifyError::RangeOutOfBounds`] or
2735/// [`ResumeOffsetPastEnd`]). A caller that cut the range from an unsigned size
2736/// asks for the whole blob on these, and treats every other failure as the
2737/// open's own outcome (#2063).
2738#[must_use]
2739#[doc(hidden)]
2740pub fn is_range_past_end(err: &anyhow::Error) -> bool {
2741    err.downcast_ref::<ResumeOffsetPastEnd>().is_some()
2742        || err.chain().any(|cause| {
2743            matches!(
2744                cause.downcast_ref::<decdn_bao_range::RangeVerifyError>(),
2745                Some(decdn_bao_range::RangeVerifyError::RangeOutOfBounds { .. })
2746            )
2747        })
2748}
2749
2750/// The wire-byte bound for a fetch of `[byte_offset, byte_offset + byte_len)`
2751/// (`byte_len == 0` meaning "to end") of a `total_bytes` blob: the bao-encoded
2752/// size of the chunk-group-aligned range (content plus interleaved proof, ADR
2753/// 038 §Payment metering), exactly the byte count the server emits. The server
2754/// widens the request to enclosing 16 KiB groups and clamps an end past the
2755/// blob to the blob's end (a claimed size is a hint); `total_bytes` here is the response
2756/// header's signed size, so [`align_range_clamped`] /
2757/// [`AlignedRange::wire_len`](decdn_bao_range::AlignedRange::wire_len)
2758/// reproduce the same clamp the server applied, keeping the two sides in
2759/// lock-step. The one site every pull derives its wire bound (and its
2760/// received-byte ceiling) through. A range with no byte below `total_bytes`
2761/// faults with [`RangeVerifyError::RangeOutOfBounds`] (see
2762/// [`is_range_past_end`]). Every input but the offset comes from the peer's
2763/// signed size or the caller's own cap, so the fault is never marked
2764/// [`LocalPullFault`].
2765///
2766/// [`RangeVerifyError::RangeOutOfBounds`]: decdn_bao_range::RangeVerifyError::RangeOutOfBounds
2767fn aligned_wire_len(byte_offset: u64, byte_len: u64, total_bytes: u64) -> anyhow::Result<u64> {
2768    let aligned = align_range_clamped(byte_offset, byte_len, total_bytes)
2769        .map_err(|e| anyhow::Error::new(e).context("range alignment"))?;
2770    Ok(aligned.wire_len())
2771}
2772
2773/// The wire bound of an open of `[byte_offset, byte_offset + byte_len)`
2774/// (`byte_len == 0` meaning "to end") against the size `total_bytes` the peer
2775/// signed ([`aligned_wire_len`]). A range that asks for bytes and starts at or
2776/// past that size is refused first, with [`ResumeOffsetPastEnd`]: an offset
2777/// past a resumed blob's end, or a size of zero signed for a bounded open. The
2778/// open of the empty blob, `(0, 0)` against a size of zero, passes.
2779fn served_wire_len(byte_offset: u64, byte_len: u64, total_bytes: u64) -> anyhow::Result<u64> {
2780    if total_bytes <= byte_offset && (byte_offset > 0 || byte_len > 0) {
2781        return Err(anyhow::Error::new(ResumeOffsetPastEnd {
2782            total_bytes,
2783            byte_offset,
2784        }));
2785    }
2786    aligned_wire_len(byte_offset, byte_len, total_bytes)
2787}
2788
2789/// How often the throughput floor samples the byte counter: a fraction of the window, so a
2790/// stall is detected within roughly one extra sample period beyond the window, floored at
2791/// 100 ms so a tiny window cannot spin the sampler.
2792pub(crate) fn stall_sample_period(window: Duration) -> Duration {
2793    (window / 8).max(Duration::from_millis(100))
2794}
2795
2796/// How long the receive loop waits for a terminal message after a voucher write
2797/// fails (below). The node writes `StreamEnd` (or a `StreamError`) *before* the
2798/// teardown that stops our send, so the terminal signal is already in flight and
2799/// arrives at once; the bound only stops a peer that stops our send and then goes
2800/// silent from pinning this recovery read. It is an error-path bound, not a
2801/// steady-state one.
2802const TERMINAL_AFTER_WRITE_TIMEOUT: Duration = Duration::from_secs(5);
2803
2804/// How long one phase of a leg may run before the leg logs it at debug
2805/// ([`log_proof_stall`]). A node stops a stream whose proof it has waited on
2806/// for 10 s, so this names the slow phase before that happens.
2807const PROOF_STALL_DEBUG_AFTER: Duration = Duration::from_secs(5);
2808
2809/// Log at debug that `phase` of one leg took `elapsed`, once it reaches
2810/// [`PROOF_STALL_DEBUG_AFTER`]. Each phase names one place a leg can stall:
2811/// `unpolled` (the caller did not read the stream, so a proof the node waits
2812/// for was not sent), `pay` (waiting for the lane's ledger, signing and
2813/// writing the proof), `first_frame` (a fresh leg waited from its open for
2814/// its first frame, any time before the caller's first read included), or
2815/// `read` (the leg waited for bytes). A `read` stall
2816/// with `unproved > 0` can be the node waiting for a proof this leg does not
2817/// yet owe; with `unproved == 0` the leg owes nothing and the node is slow to
2818/// send. `unpolled` and `pay` are logged when the phase ends; `first_frame`
2819/// and `read` are logged while the leg still waits.
2820fn log_proof_stall(
2821    pull: (&dyn std::fmt::Display, [u8; 32], u64),
2822    cumulative: u64,
2823    unproved: u64,
2824    phase: &'static str,
2825    elapsed: Duration,
2826) {
2827    if elapsed < PROOF_STALL_DEBUG_AFTER {
2828        return;
2829    }
2830    let (peer, hash, byte_offset) = pull;
2831    tracing::debug!(
2832        peer = %peer,
2833        hash = %blake3::Hash::from_bytes(hash).to_hex(),
2834        byte_offset,
2835        cumulative,
2836        unproved,
2837        phase,
2838        elapsed_ms = elapsed.as_millis(),
2839        "a leg phase ran past {PROOF_STALL_DEBUG_AFTER:?}"
2840    );
2841}
2842
2843/// Header fields from the upstream `StreamResponse`, surfaced by
2844/// [`open_progressive_pull`] before the first chunk so the fused serve path
2845/// (#856) knows `total_bytes` up front — it must sign its OWN downstream
2846/// `StreamResponse` (which commits to a `total_bytes`) before forwarding a byte.
2847#[derive(Debug, Clone, Copy)]
2848#[doc(hidden)]
2849pub struct UpstreamPullHeader {
2850    /// Whole-blob size the upstream promised (echoed into our downstream
2851    /// `StreamResponse`).
2852    pub total_bytes: u64,
2853    /// Upstream rate; informational for the caller (the buyer pays it inside
2854    /// [`UpstreamPull::next_chunk`]).
2855    pub rate_per_mb: u64,
2856    /// Upstream metering quantum in bytes: the buyer releases one hash-chain
2857    /// preimage per chunk as they arrive. Always `CHUNK_BYTES` on a paid
2858    /// upstream — it is a protocol constant, not a negotiated value — and `0`
2859    /// on an unpaid source, which is the only reason it is carried at all.
2860    pub interval_bytes: u64,
2861}
2862
2863/// A live, progressive `cdn/client/v1` pull (#856) — the ONE receive loop for
2864/// paid delivery. Opened by [`open_progressive_pull`] (which has already done the
2865/// handshake and verified the response), driven chunk-by-chunk via
2866/// [`Self::next_chunk`], and closed by [`Self::finish`] (a wire-completeness
2867/// check — integrity is verified per bao chunk group by the consumer's decoder
2868/// as the bytes land, not by a whole-blob re-hash) or [`Self::abort`].
2869///
2870/// It yields each chunk to the caller — a [`sink::PullReader`] feeding a
2871/// verifying decoder into the ranged store, the cache admit, or (`test-util`)
2872/// memory — instead of buffering the whole blob, and pays the upstream per chunk
2873/// of wire bytes that decoder has verified ([`Self::mark_verified`]). This is what
2874/// lets the serving node cap its speculative exposure to a bounded window rather
2875/// than fronting the entire upstream cost before any downstream voucher arrives,
2876/// and what keeps a lying peer's bill at the bytes before its first unverifiable
2877/// chunk group.
2878///
2879/// The acked voucher watermark is OWNED here (not threaded as a `&mut`
2880/// out-param) and read back via [`Self::progress`] /
2881/// returned by `finish`/`abort` — the caller (`node_origin`) persists it. On any
2882/// exit, the caller MUST call `progress`/`finish`/`abort` to recover the
2883/// watermark for `record_progress` (#852); a [`Drop`] guard closes the
2884/// connection if none ran, but cannot return the watermark, so the obligation
2885/// stands.
2886///
2887/// **Deadlines.** The *open* (handshake) phase is bounded inside
2888/// [`open_progressive_pull`] by `PullDeadlines::open`, via the shared `open_stream`
2889/// helper — NOT by the caller (#1134). `decdn-node` does additionally wrap the open
2890/// in its per-candidate budget, but that is belt-and-braces: a bound left to the
2891/// caller is a bound a caller can forget.
2892///
2893/// The streaming `next_chunk`/`finish` reads are bounded here, by a THROUGHPUT FLOOR
2894/// (#1797): bytes are counted off the QUIC stream sub-frame, and a read is abandoned when
2895/// the bytes across the trailing `window` fall below `floor_bps · window`. The detector
2896/// persists across reads, so its window spans the whole stream rather than one call, and a
2897/// self-inflicted window-pacing pause is excluded from the window.
2898///
2899/// A wall clock would be the wrong bound to reach for here: this type exists to
2900/// stream blobs of any size, so any fixed deadline would either kill a healthy
2901/// large transfer or be too loose to catch a dead one. A byte-throughput floor is
2902/// indifferent to size and link speed, and frame-size-independent.
2903#[doc(hidden)]
2904pub struct UpstreamPull {
2905    conn: iroh::endpoint::Connection,
2906    /// What this pull owns of its connection: a one-shot dial it closes on its
2907    /// terminal method, a caller-owned [`WarmConnection`] it borrows, or a
2908    /// [`Connections`] map's connection it holds and unpins on a transport fault.
2909    owner: connection::ConnOwner,
2910    send: SendStream,
2911    recv: RecvStream,
2912    ctx: PoolContext,
2913    /// The channel's voucher ledger, SHARED with every other concurrent pull on this
2914    /// channel (#1145 review). Not a per-pull one-shot: that made two concurrent pulls
2915    /// each compute the same next cumulative `amount` independently and collide
2916    /// (`AmountRegression`).
2917    ledger: Arc<PoolLedger>,
2918    hash: [u8; 32],
2919    /// The requested content range's start, kept for the stall diagnostics.
2920    byte_offset: u64,
2921    /// The requested content range's length (`0` = to the end), kept for the
2922    /// stall diagnostics.
2923    byte_len: u64,
2924    rate_per_mb: u64,
2925    /// This stream's chain anchor — which epoch it has told the upstream about.
2926    meter: StreamMeter,
2927    /// Throughput-floor window for every streaming read (#1797). Names the `after` on a
2928    /// [`PullStalled`] / [`PullTimeout`]; the floor itself owns the window and floor rate.
2929    window: Duration,
2930    /// Bytes read off the QUIC stream so far, tallied sub-frame by the `ProgressReader`
2931    /// each `next_chunk`/`finish` read wraps `recv` in. The floor reads this counter.
2932    progress_counter: Arc<std::sync::atomic::AtomicU64>,
2933    /// The byte-progress stall detector, persisted across reads so its window spans the
2934    /// whole stream, not one `next_chunk` call.
2935    floor: progress::ThroughputFloor,
2936    /// The floor's sampling tick.
2937    sampler: tokio::time::Interval,
2938    /// Promised **wire** bytes for this stream: the bao-encoded size of the
2939    /// chunk-group-aligned range (content plus interleaved proof, ADR 038), not
2940    /// the content-byte remainder. Bounds the receive loop and the closing voucher.
2941    expected_wire_bytes: u64,
2942    /// The buyer's received-byte ceiling (#1895) as a WIRE bound: the wire size of a
2943    /// ceiling-sized blob's content from this stream's offset, so `next_chunk` can
2944    /// enforce `max_blob_size_bytes` on the bytes that ACTUALLY arrive without
2945    /// decoding. `0` = unlimited.
2946    max_received_wire: u64,
2947    /// Wire bytes received so far on this stream.
2948    cumulative: u64,
2949    /// Wire bytes the caller's decoder has verified against the root, never
2950    /// above `cumulative`. Payment follows this count, not `cumulative`.
2951    verified: u64,
2952    /// Verified wire bytes not yet covered by a proof.
2953    unproved: u64,
2954    /// `StreamEnd` seen — `next_chunk` returns `None` and `finish` skips the
2955    /// drain.
2956    ended: bool,
2957    /// When `next_chunk` last returned a chunk, so the next call can tell how
2958    /// long the caller left the stream unread ([`log_proof_stall`]).
2959    returned_at: Option<tokio::time::Instant>,
2960    /// When the open returned, so a fresh leg's wait for its first frame is
2961    /// timed from there ([`log_proof_stall`]).
2962    opened_at: tokio::time::Instant,
2963}
2964
2965impl std::fmt::Debug for UpstreamPull {
2966    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2967        f.debug_struct("UpstreamPull")
2968            .field("hash", &blake3::Hash::from_bytes(self.hash))
2969            .field("expected_wire_bytes", &self.expected_wire_bytes)
2970            .field("cumulative", &self.cumulative)
2971            .field("ended", &self.ended)
2972            .finish_non_exhaustive()
2973    }
2974}
2975
2976/// Open a progressive `cdn/client/v1` pull (#856): connect, send the
2977/// [`StreamRequest`], read and verify the signed [`StreamResponse`] (so
2978/// `total_bytes` is known up front), and return its header plus a live
2979/// [`UpstreamPull`] to drive. Zero-rate rejection, `slash_sig` recovery, echoed
2980/// field checks, the buyer's rate ceiling, and the `total_bytes >= byte_offset`
2981/// floor are all enforced BEFORE the first chunk. The `max_blob_size_bytes`
2982/// ceiling is NOT one of them (#1895): the peer's `total_bytes` claim is
2983/// unverified, so the ceiling is enforced on the bytes that ACTUALLY arrive,
2984/// inside [`UpstreamPull::next_chunk`], as [`BlobTooLarge`]. `0` = unlimited.
2985///
2986/// # Errors
2987///
2988/// Connect / transport faults, a refused or zero-rate response, a bad `slash_sig`,
2989/// a mismatched echoed field, a [`RateAboveCeiling`] quote, a
2990/// [`ResumeOffsetPastEnd`] offset, or a resume offset already at/past the size
2991/// ceiling ([`BlobTooLarge`]). The size ceiling otherwise surfaces later, as a
2992/// [`BlobTooLarge`] abort once received bytes cross it.
2993///
2994/// The open stage is bounded by `deadlines.open`, inside the shared `open_stream`
2995/// helper — NOT left to the caller (#1134). `node_origin` additionally wraps this
2996/// call in its per-candidate budget, which is belt-and-braces rather than the sole
2997/// bound. `deadlines.window` and `deadlines.floor_bps` are the throughput floor the returned
2998/// [`UpstreamPull`] carries into every streaming read; `deadlines.hard_cap` is not consulted
2999/// here (the caller owns the streaming lifetime on this path).
3000///
3001/// `ledger` is the CHANNEL's voucher ledger, not this pull's: pass the same
3002/// `Arc<PoolLedger>` to every concurrent pull on one channel, or they will each
3003/// compute the same next cumulative `amount` independently and collide
3004/// (`AmountRegression`, #1145 review). The caller reads what to
3005/// persist from it — including after a drop — via [`PoolLedger::settlement`].
3006///
3007/// `dial_runtime` is the runtime the dial and the connection's QUIC driver run
3008/// on; `None` runs them on the caller's. A caller that drops the runtime it pulls
3009/// on passes a long-lived one — see
3010/// [`PeerSource::with_dial_runtime`](crate::source::PeerSource::with_dial_runtime).
3011///
3012/// Runs inside an `open_progressive_pull` span that covers the dial and the
3013/// signed-response handshake — the time to first byte. Its `hash`, `pool_id`,
3014/// `byte_offset`, `peer` and `local_node_id` fields match the serving node's
3015/// `serve_stream` span, with `peer` and `local_node_id` swapped.
3016#[allow(clippy::too_many_arguments)]
3017#[tracing::instrument(
3018    name = "open_progressive_pull",
3019    skip_all,
3020    fields(
3021        error = tracing::field::Empty,
3022        otel.status_code = tracing::field::Empty,
3023        otel.kind = "client",
3024        peer = %target.id,
3025        local_node_id = %endpoint.id(),
3026        hash = %decdn_protocol::ContentHash::from_bytes(hash),
3027        pool_id = %ctx.pool_id,
3028        byte_offset = byte_offset,
3029        byte_len = byte_len,
3030    )
3031)]
3032#[doc(hidden)]
3033pub async fn open_progressive_pull(
3034    endpoint: &Endpoint,
3035    target: EndpointAddr,
3036    ctx: &PoolContext,
3037    ledger: Arc<PoolLedger>,
3038    slash_domain: &Eip712Domain,
3039    expected_signer: Address,
3040    hash: [u8; 32],
3041    namespace_id: [u8; 32],
3042    byte_offset: u64,
3043    timestamp_us: u64,
3044    max_blob_size_bytes: u64,
3045    max_rate_per_mb: u64,
3046    deadlines: PullDeadlines,
3047    byte_len: u64,
3048    dial_runtime: Option<&tokio::runtime::Handle>,
3049) -> anyhow::Result<(UpstreamPullHeader, UpstreamPull)> {
3050    open_progressive_pull_impl(
3051        ConnSource::Dial {
3052            endpoint,
3053            target,
3054            runtime: dial_runtime,
3055        },
3056        // One-shot dial: the pull owns this connection and closes it on teardown.
3057        connection::ConnOwner::Owned,
3058        ctx,
3059        ledger,
3060        slash_domain,
3061        expected_signer,
3062        hash,
3063        namespace_id,
3064        byte_offset,
3065        timestamp_us,
3066        max_blob_size_bytes,
3067        max_rate_per_mb,
3068        deadlines,
3069        byte_len,
3070    )
3071    .await
3072}
3073
3074/// Like [`open_progressive_pull`], but opens the pull on the warm connection
3075/// `conn` instead of dialling a fresh one (#1848). `owner` names where `conn`
3076/// comes from: a caller-owned [`WarmConnection`] or a [`Connections`] map's
3077/// connection. The warm connection is reused across
3078/// many hashes (one dial, a fresh bi-stream per hash: one stream = one hash, no
3079/// wire change) and stays open when this pull ends, so the pull never closes
3080/// it. A map's connection is held by the pull while it runs, and a transport
3081/// fault on the open or on a later read unpins it from the map
3082/// ([`Connections::invalidate`]).
3083///
3084/// Every other argument behaves exactly as on [`open_progressive_pull`]; see its
3085/// docs. There is no `endpoint`/`target` pair — the warm connection already names
3086/// its peer.
3087#[allow(clippy::too_many_arguments)]
3088#[tracing::instrument(
3089    name = "open_progressive_pull",
3090    skip_all,
3091    fields(
3092        error = tracing::field::Empty,
3093        otel.status_code = tracing::field::Empty,
3094        otel.kind = "client",
3095        peer = %conn.remote_id(),
3096        hash = %decdn_protocol::ContentHash::from_bytes(hash),
3097        pool_id = %ctx.pool_id,
3098        byte_offset = byte_offset,
3099        byte_len = byte_len,
3100    )
3101)]
3102pub(crate) async fn open_progressive_pull_on(
3103    conn: &iroh::endpoint::Connection,
3104    owner: connection::ConnOwner,
3105    ctx: &PoolContext,
3106    ledger: Arc<PoolLedger>,
3107    slash_domain: &Eip712Domain,
3108    expected_signer: Address,
3109    hash: [u8; 32],
3110    namespace_id: [u8; 32],
3111    byte_offset: u64,
3112    timestamp_us: u64,
3113    max_blob_size_bytes: u64,
3114    max_rate_per_mb: u64,
3115    deadlines: PullDeadlines,
3116    byte_len: u64,
3117) -> anyhow::Result<(UpstreamPullHeader, UpstreamPull)> {
3118    let opened = open_progressive_pull_impl(
3119        ConnSource::Reuse(conn),
3120        // A warm connection: leave it open for the next hash.
3121        owner.clone(),
3122        ctx,
3123        ledger,
3124        slash_domain,
3125        expected_signer,
3126        hash,
3127        namespace_id,
3128        byte_offset,
3129        timestamp_us,
3130        max_blob_size_bytes,
3131        max_rate_per_mb,
3132        deadlines,
3133        byte_len,
3134    )
3135    .await;
3136    if let Err(err) = &opened {
3137        owner.unpin_on_fault(err).await;
3138    }
3139    opened
3140}
3141
3142/// The shared body behind [`open_progressive_pull`] (dial) and
3143/// [`open_progressive_pull_on`] (reuse). `owner` records what the pull owns of
3144/// its connection, so the returned [`UpstreamPull`] knows whether its terminal
3145/// method closes the connection (owned, one-shot) or leaves it open for the
3146/// next hash (warm), and whether a read fault unpins it from a map.
3147#[allow(clippy::too_many_arguments)]
3148async fn open_progressive_pull_impl(
3149    source: ConnSource<'_>,
3150    owner: connection::ConnOwner,
3151    ctx: &PoolContext,
3152    ledger: Arc<PoolLedger>,
3153    slash_domain: &Eip712Domain,
3154    expected_signer: Address,
3155    hash: [u8; 32],
3156    namespace_id: [u8; 32],
3157    byte_offset: u64,
3158    timestamp_us: u64,
3159    max_blob_size_bytes: u64,
3160    max_rate_per_mb: u64,
3161    deadlines: PullDeadlines,
3162    // Upper bound on the requested range: `[byte_offset, byte_offset + byte_len)`.
3163    // `0` means "to end" (the whole tail, which every non-gap caller requests).
3164    // A gap-driven caller (`source::PeerSource`, #1608) passes the exact gap
3165    // length so the server scopes both the serve and the payment to it, rather
3166    // than streaming the whole remainder.
3167    byte_len: u64,
3168) -> anyhow::Result<(UpstreamPullHeader, UpstreamPull)> {
3169    let result: anyhow::Result<(UpstreamPullHeader, UpstreamPull)> = async {
3170        let window = deadlines.window;
3171        let floor_bps = deadlines.floor_bps;
3172        let (conn, send, recv, resp, resp_ext) = open_stream(
3173            source,
3174            ctx,
3175            slash_domain,
3176            expected_signer,
3177            hash,
3178            // Node-to-node pull. `NO_NAMESPACE` (0) for a DHT-discovered *holder* — it
3179            // already holds the bytes, so the downstream node needs no namespace (ADR
3180            // 002 §Retrieval by namespace). But a directory-discovered *cold origin* is
3181            // reached with the served request's namespace: it must fill from its own
3182            // backend, and its pull-through authorized-origin gate resolves on that
3183            // namespace (ADR 005 §Namespace routing). The caller passes whichever
3184            // applies.
3185            namespace_id,
3186            byte_offset,
3187            byte_len,
3188            timestamp_us,
3189            deadlines.open,
3190        )
3191        .await?;
3192        if !resp.body.ok {
3193            return Err(UpstreamRefused::open(resp, &resp_ext));
3194        }
3195        // The peer's signed `total_bytes` never drives a refusal (#1895): it is
3196        // peer-controlled and unverified (`StreamResponse::validate()` does not bound
3197        // it), so the `max_blob_size_bytes` ceiling is enforced on the bytes that
3198        // ACTUALLY arrive, inside `UpstreamPull::next_chunk`, not on the claim. Refusing
3199        // on an inflated claim would let a holder centralise a small blob's traffic
3200        // across every finite-ceiling relay; a lie cannot produce bytes that verify
3201        // against the true root, and an honest giant is streamed and paid for only up
3202        // to one ceiling.
3203        // Buyer-side rate ceiling (#1375): refuse an over-ceiling quote before the
3204        // first paid interval, carrying the signed quote out as rate-manipulation
3205        // evidence. `0` = unbounded.
3206        if max_rate_per_mb > 0 && resp.body.rate_per_mb > max_rate_per_mb {
3207            return Err(RateAboveCeiling::over_ceiling(resp, max_rate_per_mb));
3208        }
3209        let rate_per_mb = resp.body.rate_per_mb;
3210        // Wire-byte bound (bao-encoded size of the aligned range), not content bytes:
3211        // the window path forwards this stream verbatim and pays the upstream in wire
3212        // bytes (ADR 038 §Payment metering).
3213        let total_bytes = resp.body.total_bytes;
3214        let expected_wire_bytes = served_wire_len(byte_offset, byte_len, total_bytes)?;
3215        // Received-byte ceiling (#1895), expressed as a WIRE bound so `next_chunk` can
3216        // enforce it without decoding: the wire size of a ceiling-sized blob's content
3217        // from this offset. Enforced on the bytes that ACTUALLY arrive, never on the
3218        // peer's unverified `total_bytes` claim. A resume offset already at/past the
3219        // ceiling means the blob is genuinely oversized — abort before the first chunk.
3220        // `0` = unlimited.
3221        let max_received_wire = if max_blob_size_bytes == 0 {
3222            0
3223        } else if byte_offset >= max_blob_size_bytes {
3224            return Err(anyhow::Error::new(BlobTooLarge {
3225                reached: byte_offset,
3226                ceiling: max_blob_size_bytes,
3227            }));
3228        } else {
3229            aligned_wire_len(byte_offset, 0, max_blob_size_bytes)?
3230        };
3231        let header = UpstreamPullHeader {
3232            total_bytes,
3233            rate_per_mb,
3234            interval_bytes: CHUNK_BYTES,
3235        };
3236        let progress_counter = Arc::new(std::sync::atomic::AtomicU64::new(0));
3237        let floor = progress::ThroughputFloor::new(
3238            progress::FloorConfig { window, floor_bps },
3239            Arc::clone(&progress_counter),
3240            tokio::time::Instant::now(),
3241        );
3242        let mut sampler = tokio::time::interval(stall_sample_period(window));
3243        sampler.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
3244        let pull = UpstreamPull {
3245            window,
3246            progress_counter,
3247            floor,
3248            sampler,
3249            conn,
3250            owner,
3251            send,
3252            recv,
3253            ctx: ctx.clone(),
3254            ledger,
3255            hash,
3256            byte_offset,
3257            byte_len,
3258            rate_per_mb,
3259            meter: StreamMeter::default(),
3260            expected_wire_bytes,
3261            max_received_wire,
3262            cumulative: 0,
3263            verified: 0,
3264            unproved: 0,
3265            ended: false,
3266            returned_at: None,
3267            opened_at: tokio::time::Instant::now(),
3268        };
3269        Ok((header, pull))
3270    }
3271    .await;
3272    record_open_result(&tracing::Span::current(), &result);
3273    result
3274}
3275
3276impl UpstreamPull {
3277    /// Wire bytes this stream will deliver: the bao-encoded size of the
3278    /// chunk-group-aligned range (content plus interleaved proof, ADR 038). The
3279    /// window serve loop uses it as the pull budget `total` — it must be wire
3280    /// bytes, since `pulled`/`served_paid` count forwarded wire bytes.
3281    #[must_use]
3282    pub const fn expected_wire_bytes(&self) -> u64 {
3283        self.expected_wire_bytes
3284    }
3285
3286    /// The watermark to PERSIST on any exit — including an error from `next_chunk`, and
3287    /// including a DROP (#852, #1122).
3288    ///
3289    /// Reads [`PoolLedger::settlement`], not a copied-back field. A field can only be
3290    /// updated on a path that RUNS, and this pull's does not always run: the serve loop
3291    /// drives it as a future on the `accept` task, which is dropped on shutdown or a
3292    /// downstream reset, and `settlement` additionally covers a voucher left in flight when
3293    /// that happened. See the ledger's docs.
3294    #[must_use]
3295    pub fn progress(&self) -> VoucherProgress {
3296        VoucherProgress::from_ledger(&self.ledger, self.ctx.prior_amount)
3297    }
3298
3299    /// Record that the caller's decoder has verified the first `wire` bytes of
3300    /// this stream against the root. The count only moves forward and never
3301    /// passes the bytes received. The newly verified bytes become payable: the
3302    /// next [`Self::next_chunk`], [`Self::finish`] or [`Self::stop`] pays for
3303    /// them, and a byte that never verifies is never paid for.
3304    pub(crate) fn mark_verified(&mut self, wire: u64) {
3305        let wire = wire.min(self.cumulative);
3306        if wire > self.verified {
3307            self.unproved = self.unproved.saturating_add(wire - self.verified);
3308            self.verified = wire;
3309        }
3310    }
3311
3312    /// Pay for the verified bytes no proof covers yet: one reveal per whole
3313    /// chunk, plus one closing signature for the residual once every promised
3314    /// byte has verified. The sends commit optimistically; only a rejection
3315    /// comes back, on a later read.
3316    ///
3317    /// A write failure may only mean the node stopped our send after it
3318    /// finished, or is rejecting us, so the terminal signal it left wins over
3319    /// the opaque write failure ([`Self::terminal_after_write_failure`]).
3320    async fn pay_verified(&mut self) -> anyhow::Result<()> {
3321        let complete = self.verified >= self.expected_wire_bytes;
3322        if self.unproved < CHUNK_BYTES && !(complete && self.unproved > 0) {
3323            return Ok(());
3324        }
3325        let unproved = self.unproved;
3326        // Gate the floor across our own payment: while we owe the covering proof
3327        // the upstream legitimately pauses (ADR 005 §Payment pacing), so that
3328        // pause is self-inflicted, not a sender stall (#1797).
3329        let pay_started = tokio::time::Instant::now();
3330        self.floor.pause(pay_started);
3331        let paid = self.pay_one(unproved, complete).await;
3332        let paid_at = tokio::time::Instant::now();
3333        self.floor.resume(paid_at);
3334        log_proof_stall(
3335            (&self.conn.remote_id(), self.hash, self.byte_offset),
3336            self.cumulative,
3337            unproved,
3338            "pay",
3339            paid_at.saturating_duration_since(pay_started),
3340        );
3341        match paid {
3342            Ok(remaining) => self.unproved = remaining,
3343            // Boxed so this cold error-path future does not enlarge the steady
3344            // receive loop's future (`clippy::large_futures`); the allocation
3345            // only happens on the failure path. A confirmed voucher covers every
3346            // byte this stream verified, so nothing stays unproved.
3347            Err(write_err) => {
3348                if Box::pin(self.terminal_after_write_failure(write_err)).await? {
3349                    self.unproved = 0;
3350                }
3351            }
3352        }
3353        Ok(())
3354    }
3355
3356    /// Send one voucher for `delta_bytes` newly verified since the last voucher,
3357    /// through the CHANNEL's ledger — shared with every other concurrent pull on it, so
3358    /// their vouchers are serialized into strict cumulative-amount order rather than
3359    /// colliding. Sends optimistically (#1484): the ack is
3360    /// read back by [`Self::next_chunk`] / [`Self::finish`], not awaited here.
3361    async fn pay_one(&mut self, unproved: u64, complete: bool) -> anyhow::Result<u64> {
3362        let ledger = Arc::clone(&self.ledger);
3363        self.meter
3364            .pay(
3365                &mut self.send,
3366                &self.ctx,
3367                &ledger,
3368                self.rate_per_mb,
3369                unproved,
3370                complete,
3371            )
3372            .await
3373    }
3374
3375    /// Read one message under the persistent throughput floor (#1797). Wraps `recv` in a
3376    /// `ProgressReader` feeding the shared counter, and races the read against
3377    /// the floor's sampling tick: a read slow enough to lose the race is judged, and a
3378    /// sub-floor window aborts with [`PullTimeout`] before the first byte (our own
3379    /// blob-size-dependent budget) or [`PullStalled`] after it — neither scores the peer.
3380    ///
3381    /// A transport fault on a [`Connections`] map's connection (either floor verdict,
3382    /// or a connection that has closed under the read) unpins that connection from
3383    /// the map, so the node's next leg dials again.
3384    async fn read_under_floor(&mut self) -> anyhow::Result<ClientMessage> {
3385        let read = self.read_under_floor_once().await;
3386        if let Err(err) = &read {
3387            self.owner.unpin_on_fault(err).await;
3388        }
3389        read
3390    }
3391
3392    /// [`Self::read_under_floor`] before a transport fault unpins the connection.
3393    async fn read_under_floor_once(&mut self) -> anyhow::Result<ClientMessage> {
3394        let cumulative = self.cumulative;
3395        let window = self.window;
3396        let recv = &mut self.recv;
3397        let sampler = &mut self.sampler;
3398        let floor = &mut self.floor;
3399        let (peer, hash, byte_offset) = (self.conn.remote_id(), self.hash, self.byte_offset);
3400        let unproved = self.unproved;
3401        // A fresh leg's wait runs from its open, any time before the caller's
3402        // first read included, so a first frame that never comes is named
3403        // once, as `first_frame` (#2230).
3404        let (phase, read_started) = if cumulative == 0 {
3405            ("first_frame", self.opened_at)
3406        } else {
3407            ("read", tokio::time::Instant::now())
3408        };
3409        // The stall line runs on its own timer, not on the sampler, whose
3410        // period follows the configurable window and can be later than the
3411        // node's proof timeout.
3412        let read_stall_at = read_started + PROOF_STALL_DEBUG_AFTER;
3413        let mut read_stall_logged = false;
3414        let mut reader = progress::ProgressReader::new(recv, Arc::clone(&self.progress_counter));
3415        // Pin ONE read future and poll it across ticks. `read_client_message` is not
3416        // cancellation-safe — `read_frame` fills a frame with `read_exact`, so dropping the
3417        // future mid-frame loses the bytes already consumed and desynchronises the stream.
3418        // Recreating it per tick would corrupt every frame that spans a tick; the pinned
3419        // future is dropped only when the floor actually aborts (#1797). `biased` cannot
3420        // starve the floor: a ready read arm means a whole frame arrived, which is byte
3421        // progress the counter already holds, so a skipped tick only ever coincides with
3422        // throughput the floor would clear.
3423        let read = read_client_message(&mut reader);
3424        tokio::pin!(read);
3425        loop {
3426            tokio::select! {
3427                biased;
3428                r = &mut read => return r,
3429                _ = sampler.tick() => {
3430                    let now = tokio::time::Instant::now();
3431                    let last_sample = floor.last_sampled_at();
3432                    if let progress::FloorVerdict::Stalled = floor.evaluate(now) {
3433                        // What tells a quiet peer from a reader that was not
3434                        // polled (#2211): the gap since the previous sample.
3435                        tracing::debug!(
3436                            peer = %self.conn.remote_id(),
3437                            hash = %blake3::Hash::from_bytes(self.hash).to_hex(),
3438                            byte_offset = self.byte_offset,
3439                            byte_len = self.byte_len,
3440                            cumulative,
3441                            expected_wire_bytes = self.expected_wire_bytes,
3442                            window_bytes = floor.window_bytes(),
3443                            unpolled_ms = last_sample
3444                                .map(|t| now.saturating_duration_since(t).as_millis()),
3445                            window_ms = window.as_millis(),
3446                            "upstream pull fell under its throughput floor"
3447                        );
3448                        return Err(if cumulative == 0 {
3449                            anyhow::Error::new(PullTimeout { after: window })
3450                        } else {
3451                            anyhow::Error::new(PullStalled { after: window })
3452                        });
3453                    }
3454                }
3455                // A long wait here is the leg waiting on the node, while the
3456                // node may wait on a proof or still be sending its first frame.
3457                () = tokio::time::sleep_until(read_stall_at), if !read_stall_logged => {
3458                    read_stall_logged = true;
3459                    let waited = read_started.elapsed();
3460                    let pull = (&peer as &dyn std::fmt::Display, hash, byte_offset);
3461                    log_proof_stall(pull, cumulative, unproved, phase, waited);
3462                }
3463            }
3464        }
3465    }
3466
3467    /// Pay for the bytes the caller's decoder has verified since the last proof
3468    /// ([`Self::mark_verified`]), then read the next `ChunkData` and return it
3469    /// for that decoder (and, on the node's serve leg, to forward downstream).
3470    /// The payment goes first because the node may be waiting on it: a read
3471    /// before it could wait on bytes the node holds back until it is paid.
3472    /// Returns `Ok(None)` on `StreamEnd`.
3473    ///
3474    /// # Errors
3475    ///
3476    /// More bytes than promised, received bytes crossing the buyer's size ceiling
3477    /// ([`BlobTooLarge`], raised before the crossing chunk is paid), a mid-stream `StreamError`
3478    /// (typed [`UpstreamRefused`]), an unexpected message, a [`UpstreamVoucherRejected`] /
3479    /// transport error while paying, or —
3480    /// on the throughput floor — [`PullStalled`] once bytes have flowed, or [`PullTimeout`] if
3481    /// the floor trips before the first byte (`cumulative == 0`). A malformed `ChunkData` is
3482    /// not raised here: an empty payload is rejected at decode by `ChunkData`'s
3483    /// `serde(try_from)` (#1088) and an oversized frame by the framing layer's
3484    /// `MAX_MESSAGE_SIZE` before it allocates, so both surface out of `read_client_message`.
3485    pub async fn next_chunk(&mut self) -> anyhow::Result<Option<Bytes>> {
3486        if self.ended {
3487            return Ok(None);
3488        }
3489        if let Some(returned_at) = self.returned_at.take() {
3490            log_proof_stall(
3491                (&self.conn.remote_id(), self.hash, self.byte_offset),
3492                self.cumulative,
3493                self.unproved,
3494                "unpolled",
3495                returned_at.elapsed(),
3496            );
3497        }
3498        self.pay_verified().await?;
3499        if self.ended {
3500            return Ok(None);
3501        }
3502        // Read exactly one message. In the pool model there is no positive ack to
3503        // consume (acceptance is implicit — continued delivery IS acceptance, ADR
3504        // 005), so every message either carries a chunk, ends the stream, or is a
3505        // mid-stream error/unexpected frame; none is skipped.
3506        {
3507            // Byte-progress bound (#1797): the read is judged by the persistent throughput
3508            // floor, which counts bytes off the QUIC stream sub-frame. A `ChunkData` carries
3509            // at least one byte (#1088 bans empty frames), so every frame is a unit of
3510            // progress. The floor's window spans the whole stream, not this call, and the
3511            // caller's downstream-forward time is not charged against it — the counter only
3512            // moves on bytes the reader pulls.
3513            let msg = self.read_under_floor().await?;
3514            match msg {
3515                ClientMessage::ChunkData(chunk) => {
3516                    // The payload is bounded on both sides by construction (#1088): the
3517                    // ceiling caps per-frame allocation, and the non-empty floor ties every
3518                    // frame to payload, so a peer cannot advance this loop with a run of
3519                    // empty frames that move neither `cumulative` nor the voucher
3520                    // accounting. This path has no belt-and-braces payload check behind
3521                    // that floor, and does not need one now the floor is structural.
3522                    let chunk_len = chunk.bytes().len() as u64;
3523                    self.cumulative = self.cumulative.saturating_add(chunk_len);
3524                    if self.cumulative > self.expected_wire_bytes {
3525                        anyhow::bail!(
3526                            "server sent {} bytes, more than the {} promised",
3527                            self.cumulative,
3528                            self.expected_wire_bytes
3529                        );
3530                    }
3531                    // Received-byte ceiling (#1895): enforce the size cap on the bytes
3532                    // that ACTUALLY arrive, never on the peer's unverified `total_bytes`
3533                    // claim. `max_received_wire` is the wire size of a ceiling-sized
3534                    // blob's content, so this is the wire form of "received content >
3535                    // ceiling" — distinct from the claim-derived `expected_wire_bytes`
3536                    // overrun guard above. Abort BEFORE paying for the chunk that
3537                    // crosses it, so the spend stays bounded to roughly one ceiling.
3538                    // `0` = unlimited.
3539                    if self.max_received_wire > 0 && self.cumulative > self.max_received_wire {
3540                        return Err(anyhow::Error::new(BlobTooLarge {
3541                            reached: self.cumulative,
3542                            ceiling: self.max_received_wire,
3543                        }));
3544                    }
3545                    // No per-chunk hashing here: this stream's bytes are bao wire
3546                    // bytes the caller feeds to a verifying decoder — the cache's
3547                    // (`admit_bao_stream`), the ranged store's, or the in-memory
3548                    // test wrapper's — which checks every chunk group against the
3549                    // root as it lands (ADR 038) and reports what it verified
3550                    // through `mark_verified`. Only verified bytes are paid for, so
3551                    // a peer that sends a bad group bills nothing for it.
3552                    self.returned_at = Some(tokio::time::Instant::now());
3553                    Ok(Some(Bytes::from(chunk.into_bytes())))
3554                }
3555                // A mid-stream `StreamError` is either a `VoucherRejected` (our
3556                // payment fault, with the self-heal bundle) or a refusal; both
3557                // surface as errors.
3558                ClientMessage::StreamError(e) => {
3559                    Err(voucher_rejection(&self.ledger, &self.meter, e))
3560                }
3561                ClientMessage::StreamEnd => {
3562                    self.ended = true;
3563                    Ok(None)
3564                }
3565                other => {
3566                    anyhow::bail!("unexpected message mid-delivery: {}", variant_name(&other))
3567                }
3568            }
3569        }
3570    }
3571
3572    /// Decide what a voucher write failure really means.
3573    ///
3574    /// A node stops our send half only once it needs nothing more from us: a
3575    /// completed delivery leaves a `StreamEnd` on the wire, a mid-stream rejection a
3576    /// `StreamError`. The write half and the read half run in lock-step in the
3577    /// receive loop, so a raw voucher-write failure — the end-of-stream
3578    /// `STOP_SENDING(0)` a node emits when it finishes and drops `recv` — would
3579    /// otherwise mask that terminal signal and abort a complete, paid fetch (or
3580    /// swallow a typed rejection the fault classification keys on). Read the terminal
3581    /// signal, briefly, and prefer it.
3582    ///
3583    /// A `StreamEnd` marks the stream ended, and the next read returns `None`. When the failed write was
3584    /// a voucher, the `StreamEnd` also confirms it. An honest node ends a stream
3585    /// only once every interval, the closing one included, is credited. So
3586    /// payment-based completion ([`driver::drive`]) sees this leg paid through its
3587    /// end and does not re-pull and re-bill the tail. Confirming costs nothing
3588    /// extra, because [`PoolLedger::settlement`] already reports the armed voucher.
3589    ///
3590    /// A `StreamError` surfaces as the typed rejection. The node delivers ahead of
3591    /// payment within its credit window, so `ChunkData` it sent before it rejected
3592    /// can sit in front of the `StreamError`: the read passes over those frames,
3593    /// unread, to reach the terminal message. The rejected leg pays for none of
3594    /// them, and a caller that resumes pulls them again. A `StreamEnd` behind
3595    /// skipped `ChunkData` confirms nothing, because this leg did not take in those
3596    /// bytes. A write we caused ourselves (a [`LocalPullFault`]) is never masked,
3597    /// nor is a stream that yields no terminal signal before the bound.
3598    ///
3599    /// Returns whether a voucher was confirmed. A `StreamEnd` after a failed reveal
3600    /// or re-anchor write, or after a sibling's voucher replaced this stream's
3601    /// armed one, ends the stream with nothing confirmed.
3602    async fn terminal_after_write_failure(
3603        &mut self,
3604        write_err: anyhow::Error,
3605    ) -> anyhow::Result<bool> {
3606        if write_err.is::<LocalPullFault>() {
3607            return Err(write_err);
3608        }
3609        // Any outcome other than a terminal message means nothing terminal was
3610        // waiting, so the write failure stands as the honest outcome (its typed
3611        // cause still downcasts through the added context); what the recovery saw
3612        // rides along so the log can tell a silent peer from a chatty one.
3613        // The frames it passes over stay under the same ceilings as the ones it
3614        // takes in (`next_chunk`): an honest node sends no byte past the
3615        // request's promised wire length, and our own received-byte ceiling
3616        // stops the pull as it would on any read.
3617        let mut skipped: u64 = 0;
3618        let terminal = tokio::time::timeout(TERMINAL_AFTER_WRITE_TIMEOUT, async {
3619            loop {
3620                match self.read_under_floor().await {
3621                    Ok(ClientMessage::ChunkData(chunk)) => {
3622                        skipped = skipped.saturating_add(chunk.bytes().len() as u64);
3623                        let seen = self.cumulative.saturating_add(skipped);
3624                        if seen > self.expected_wire_bytes {
3625                            anyhow::bail!(
3626                                "server sent {seen} bytes, more than the {} promised",
3627                                self.expected_wire_bytes
3628                            );
3629                        }
3630                        if self.max_received_wire > 0 && seen > self.max_received_wire {
3631                            return Err(anyhow::Error::new(BlobTooLarge {
3632                                reached: seen,
3633                                ceiling: self.max_received_wire,
3634                            }));
3635                        }
3636                    }
3637                    other => return other,
3638                }
3639            }
3640        })
3641        .await;
3642        match terminal {
3643            Ok(Ok(ClientMessage::StreamEnd)) if skipped > 0 => Err(write_err.context(format!(
3644                "the peer ended the stream after {skipped} bytes of ChunkData this leg did not \
3645                 take in once the voucher write failed"
3646            ))),
3647            Ok(Ok(ClientMessage::StreamEnd)) => {
3648                self.ended = true;
3649                let attempted = write_err
3650                    .downcast_ref::<UnconfirmedVoucher>()
3651                    .map(|voucher| voucher.amount);
3652                let confirmed = match attempted {
3653                    Some(amount) => self.ledger.confirm_armed_stamped(amount).await,
3654                    None => None,
3655                };
3656                if let Some((confirmed, generation)) = confirmed {
3657                    tracing::debug!(
3658                        amount = %confirmed.amount,
3659                        error = %format_args!("{write_err:#}"),
3660                        "upstream ended the stream after a voucher write failed; confirmed \
3661                         the voucher"
3662                    );
3663                    self.meter.last_proof = Some(StreamProof::Voucher {
3664                        amount: confirmed.amount,
3665                        generation,
3666                    });
3667                    Ok(true)
3668                } else {
3669                    tracing::warn!(
3670                        attempted = ?attempted,
3671                        error = %format_args!("{write_err:#}"),
3672                        "upstream ended the stream after a proof write failed; nothing \
3673                         confirmed, so the leg's tail stays unpaid"
3674                    );
3675                    Ok(false)
3676                }
3677            }
3678            Ok(Ok(ClientMessage::StreamError(e))) => {
3679                Err(voucher_rejection(&self.ledger, &self.meter, e))
3680            }
3681            Ok(Ok(other)) => Err(write_err.context(format!(
3682                "no terminal signal after the voucher write failed; peer sent {}",
3683                variant_name(&other)
3684            ))),
3685            // The ceiling stays typed: it is terminal, not a peer to fail over from.
3686            Ok(Err(read_err)) if read_err.is::<BlobTooLarge>() => Err(read_err),
3687            Ok(Err(read_err)) => Err(write_err.context(format!(
3688                "no terminal signal after the voucher write failed; recovery read failed: \
3689                 {read_err:#}"
3690            ))),
3691            Err(_) => Err(write_err.context(format!(
3692                "no terminal signal within {TERMINAL_AFTER_WRITE_TIMEOUT:?} of the voucher \
3693                 write failing"
3694            ))),
3695        }
3696    }
3697
3698    /// Finalize a completed pull: pay for the verified bytes no proof covers yet
3699    /// (the closing voucher, which the node waits for before its `StreamEnd`),
3700    /// drain to `StreamEnd` if needed, enforce wire-byte completeness (the full
3701    /// promised bao wire size was received), close the connection cleanly, and
3702    /// return the final acked watermark to persist. Per ADR 038 this does not
3703    /// re-hash the whole blob — bao verification is the caller's decoder's job,
3704    /// group by group as the bytes land.
3705    ///
3706    /// # Errors
3707    ///
3708    /// A short delivery (fewer wire bytes than promised before `StreamEnd`), a stream/protocol
3709    /// error while paying or draining, or — on the same inactivity bound as `next_chunk` —
3710    /// [`PullStalled`] if the upstream goes silent before `StreamEnd`.
3711    pub async fn finish(mut self) -> anyhow::Result<VoucherProgress> {
3712        if !self.ended {
3713            self.pay_verified().await?;
3714        }
3715        while !self.ended {
3716            // Same byte-progress bound as `next_chunk` (#1797): an upstream that never sends
3717            // its `StreamEnd` must not hold the drain open forever.
3718            let msg = self.read_under_floor().await?;
3719            match msg {
3720                ClientMessage::StreamEnd => self.ended = true,
3721                ClientMessage::ChunkData(_) => {
3722                    anyhow::bail!("server sent ChunkData after the promised total")
3723                }
3724                // The closing voucher was committed optimistically when it was sent
3725                // (implicit acceptance, ADR 005), so the drain only needs to watch
3726                // for a late `VoucherRejected` / other `StreamError`, which still
3727                // surfaces as an error.
3728                ClientMessage::StreamError(e) => {
3729                    return Err(voucher_rejection(&self.ledger, &self.meter, e));
3730                }
3731                other => {
3732                    anyhow::bail!("unexpected message at stream end: {}", variant_name(&other))
3733                }
3734            }
3735        }
3736        // Completeness for every fetch (full and resumed): bao verification is
3737        // the caller's decoder's job, group by group as the bytes land, so
3738        // `finish` does not re-hash the whole blob. A truncated stream (fewer
3739        // wire bytes than promised) can't be decoded, so require the full
3740        // promised wire size as the completeness signal.
3741        if self.cumulative < self.expected_wire_bytes {
3742            self.close_transport(0, b"short-delivery");
3743            anyhow::bail!(
3744                "server sent {} of {} promised wire bytes before StreamEnd",
3745                self.cumulative,
3746                self.expected_wire_bytes
3747            );
3748        }
3749        self.close_transport(0, b"done");
3750        Ok(self.progress())
3751    }
3752
3753    /// Abandon the pull (e.g. the downstream client dropped, so we stop pulling
3754    /// and paying). Closes the connection and returns the acked watermark so the
3755    /// caller can still persist what it paid (#852).
3756    #[must_use]
3757    pub fn abort(mut self) -> VoucherProgress {
3758        self.close_transport(0, b"client-abandoned");
3759        self.progress()
3760    }
3761
3762    /// Stop the pull before its promised end, after paying for every wire byte
3763    /// the caller's decoder verified. A steal that takes the tail of this pull's
3764    /// range stops the pull at the split on its open stream, so the stolen tail
3765    /// goes to one source and this one opens no new stream for the part it
3766    /// keeps. Frames past the split that the stream already carried are not
3767    /// verified, so they are not paid for, and the stolen tail is paid once.
3768    ///
3769    /// A rejection the node already sent waits behind the `ChunkData` it sent
3770    /// first, so the stop first reads what has arrived, without waiting: a
3771    /// `VoucherRejected` there rewinds the rejected proof and surfaces as the
3772    /// typed rejection, as it does at [`Self::finish`]. A rejection still in
3773    /// flight reaches the lane's next leg instead. The closing voucher then
3774    /// settles the verified residual, the send half is finished, and the
3775    /// transport is torn down (this stream, or the whole connection when the
3776    /// pull owns it), so the node sees its sending stopped. A node that stops
3777    /// sending before it reads that voucher does not redeem it, and the lane's
3778    /// next cumulative voucher covers the same bytes.
3779    ///
3780    /// A closing voucher that fails to send stays armed in the ledger:
3781    /// [`PoolLedger::settlement`] counts it, and the lane's next voucher builds
3782    /// on it. The caller's paid frontier, which reads
3783    /// [`PoolLedger::committed`], stays behind the verified bytes, so its next
3784    /// leg re-delivers and pays for them once more.
3785    ///
3786    /// # Errors
3787    ///
3788    /// The typed rejection of a `StreamError` that had already arrived, or a
3789    /// closing voucher that fails on our side ([`LocalPullFault`]). Any other
3790    /// send failure is logged and leaves the residual as above.
3791    pub async fn stop(mut self) -> anyhow::Result<VoucherProgress> {
3792        if let Some(rejection) = self.arrived_rejection() {
3793            self.close_transport(0, b"stopped-at-split");
3794            return Err(rejection);
3795        }
3796        let paid = self.pay_one(self.unproved, true).await;
3797        let _ = self.send.finish();
3798        self.close_transport(0, b"stopped-at-split");
3799        match paid {
3800            Ok(remaining) => self.unproved = remaining,
3801            Err(err) if err.is::<LocalPullFault>() => return Err(err),
3802            Err(err) => tracing::warn!(
3803                peer = %self.conn.remote_id(),
3804                cumulative = self.cumulative,
3805                unproved = self.unproved,
3806                error = %format_args!("{err:#}"),
3807                "the closing voucher of a pull stopped at a split did not send"
3808            ),
3809        }
3810        Ok(self.progress())
3811    }
3812
3813    /// The typed rejection of a `StreamError` that has already arrived, read
3814    /// without waiting past the `ChunkData` in front of it. Every frame it
3815    /// takes in counts as received, as in [`Self::next_chunk`]; no decoder
3816    /// verifies it, so no voucher pays for it. A frame past the request's
3817    /// promised wire length or past our received-byte ceiling ends the read:
3818    /// an honest node sends neither. `None` once nothing more has arrived, on
3819    /// any other message, or on a read fault: the caller closes the stream
3820    /// either way.
3821    fn arrived_rejection(&mut self) -> Option<anyhow::Error> {
3822        loop {
3823            match futures_util::FutureExt::now_or_never(self.read_under_floor_once())? {
3824                Ok(ClientMessage::ChunkData(chunk)) => {
3825                    let seen = self.cumulative.saturating_add(chunk.bytes().len() as u64);
3826                    if seen > self.expected_wire_bytes
3827                        || (self.max_received_wire > 0 && seen > self.max_received_wire)
3828                    {
3829                        return None;
3830                    }
3831                    self.cumulative = seen;
3832                }
3833                Ok(ClientMessage::StreamError(e)) => {
3834                    return Some(voucher_rejection(&self.ledger, &self.meter, e));
3835                }
3836                _ => return None,
3837            }
3838        }
3839    }
3840
3841    /// Tear down this pull's transport on any exit.
3842    ///
3843    /// An OWNED connection (a one-shot dial) is closed, which ends the QUIC
3844    /// connection so the upstream's serve task stops and the paid stream does not
3845    /// linger half-open. A WARM connection (a [`WarmConnection`] reused across
3846    /// hashes, caller-owned or from a [`Connections`] map) is left open for the
3847    /// next hash, and only THIS stream is torn down: the send half is finished (a
3848    /// clean FIN) and the recv half is stopped. The warm connection's own `Drop`
3849    /// closes the connection once, later.
3850    ///
3851    /// `Connection::close`, `SendStream::finish`, and `RecvStream::stop` are all
3852    /// first-wins / idempotent, so an explicit terminal method (`finish`/`abort`)
3853    /// keeps its richer reason and the `Drop` safety net becomes a no-op.
3854    fn close_transport(&mut self, code: u32, reason: &[u8]) {
3855        if self.owner.owns() {
3856            self.conn.close(code.into(), reason);
3857        } else {
3858            let _ = self.send.finish();
3859            let _ = self.recv.stop(code.into());
3860        }
3861    }
3862}
3863
3864impl Drop for UpstreamPull {
3865    /// Safety net for the "call a terminal method on every exit" contract: if a
3866    /// caller returns or panics without `finish`/`abort`, still tear this pull's
3867    /// transport down through `close_transport` so the QUIC stream and the
3868    /// upstream's server-side serve task don't linger and keep that paid stream
3869    /// half-open. `Connection::close` / `SendStream::finish` / `RecvStream::stop`
3870    /// are all first-wins and idempotent, so an explicit teardown in
3871    /// `finish`/`abort` keeps its richer reason and this is a no-op when one of
3872    /// them ran; it only takes effect on a dropped-without-finalize path.
3873    ///
3874    /// It tears down the transport and nothing else, and that is sufficient: the
3875    /// acked watermark lives in the channel's [`PoolLedger`], which OUTLIVES the
3876    /// pull (it is shared with the other pulls on the channel), not in a field of
3877    /// this struct. A dropped pull's caller reads it with [`PoolLedger::settlement`]
3878    /// and persists it — `node_origin` does exactly that from its own `Drop` guard
3879    /// (#1145 review).
3880    fn drop(&mut self) {
3881        self.close_transport(0, b"upstream-pull-dropped");
3882    }
3883}
3884
3885/// Issue one cumulative voucher for `delta_bytes` newly delivered since the last
3886/// voucher and SEND it. Acceptance is implicit (ADR 005): continued delivery IS
3887/// acceptance, so there is no ack to wait for — the send itself commits.
3888///
3889/// Voucher issuance runs through the lane's [`PoolLedger`], which serializes the
3890/// compute → sign → send critical section across every concurrent stream drawing
3891/// on the lane — so vouchers reach the node in strict cumulative order — and
3892/// releases its issuance lock the instant the send returns. Parallel range
3893/// streams to one provider do not serialize their payments behind each other's
3894/// round trips, because there is no round trip: only a rejection comes back, and
3895/// it arrives as its own mid-stream `StreamError`.
3896///
3897/// Each voucher's own *delta* (`ceil(delta_bytes * rate / 1 MiB)`) covers its own
3898/// bytes at the advertised rate (the node checks deltas, not the rounded
3899/// cumulative). A successful send advances the committed watermark optimistically;
3900/// an ambiguous send failure leaves the voucher armed so [`PoolLedger::settlement`]
3901/// still reports it (settle high — the upstream persists a voucher before it would
3902/// reject it, ADR 003). The client never pays ahead of what it received (vouchers
3903/// are cumulative over delivered bytes).
3904///
3905/// Returns the sent voucher as the [`StreamProof`] the stream records, stamped
3906/// with the ledger generation it was signed under, and the chain root the
3907/// voucher anchors this stream to (`None` for a sealed voucher, which clears the
3908/// node's anchor). The root is the one the voucher signed, read under the
3909/// issuance lock: a read of the ledger after the send can already name a
3910/// sibling's newer chain.
3911async fn send_voucher(
3912    send: &mut SendStream,
3913    ctx: &PoolContext,
3914    ledger: &PoolLedger,
3915    rate_per_mb: u64,
3916    delta_bytes: u64,
3917    epoch: EpochAction,
3918) -> anyhow::Result<(StreamProof, Option<B256>)> {
3919    if ctx.provider.is_zero() {
3920        return Err(anyhow::anyhow!(
3921            "voucher provider is not pinned (Address::ZERO) — call PoolContext::with_provider \
3922             before signing"
3923        )
3924        .context(LocalPullFault));
3925    }
3926    let mut attempted = None;
3927    let mut signed_root = B256::ZERO;
3928    let issued = ledger
3929        .issue_stamped(delta_bytes, rate_per_mb, epoch, |next, chain| {
3930            attempted = Some(next.amount);
3931            signed_root = chain.chain_root;
3932            sign_and_write_voucher(send, ctx, next, chain)
3933        })
3934        .await;
3935    issued
3936        .map(|(next, generation)| {
3937            (
3938                StreamProof::Voucher {
3939                    amount: next.amount,
3940                    generation,
3941                },
3942                (!signed_root.is_zero()).then_some(signed_root),
3943            )
3944        })
3945        .map_err(|err| match attempted {
3946            Some(amount) => err.context(UnconfirmedVoucher { amount }),
3947            None => err,
3948        })
3949}
3950
3951/// Names the voucher a failed [`send_voucher`] left armed, so a terminal
3952/// `StreamEnd` read afterwards confirms that voucher and no other
3953/// ([`PoolLedger::confirm_armed_stamped`]). Carried as error context: it composes with,
3954/// and still downcasts through, the send error it wraps.
3955#[derive(Debug)]
3956struct UnconfirmedVoucher {
3957    /// The cumulative amount the armed voucher signs.
3958    amount: U256,
3959}
3960
3961impl std::fmt::Display for UnconfirmedVoucher {
3962    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3963        write!(
3964            f,
3965            "voucher for cumulative amount {} did not confirm",
3966            self.amount
3967        )
3968    }
3969}
3970
3971impl std::error::Error for UnconfirmedVoucher {}
3972
3973/// Sign one voucher over `next`/`chain` with the pool context's key and write it
3974/// to `send` — the body both [`send_voucher`] and [`PoolLedger::reanchor`] hand to
3975/// the ledger, so the two differ only in which cumulative the ledger hands back.
3976async fn sign_and_write_voucher(
3977    send: &mut SendStream,
3978    ctx: &PoolContext,
3979    next: Cumulative,
3980    chain: ChainCommit,
3981) -> anyhow::Result<()> {
3982    let signed = Voucher {
3983        pool_id: ctx.pool_id,
3984        signer: ctx.client_signer.address(),
3985        provider: ctx.provider,
3986        amount: next.amount,
3987        bytes_delivered: next.bytes,
3988        chain_root: chain.chain_root,
3989        chunk_price: chain.chunk_price,
3990    }
3991    .sign(ctx.client_signer.as_ref(), &ctx.voucher_domain)
3992    .map_err(|e| anyhow::anyhow!("voucher signing failed: {e}").context(LocalPullFault))?;
3993    let wire_voucher = signed_to_wire_voucher(&signed)
3994        .map_err(|e| anyhow::anyhow!("voucher exceeds wire width: {e}").context(LocalPullFault))?;
3995    write_message(send, &ClientMessage::Voucher(wire_voucher)).await
3996}
3997
3998/// One stream's view of the lane's hash chain (ADR 003 §Concurrent Streams).
3999///
4000/// The chain's scope is the **lane**, not the stream: every stream from one
4001/// signer to one node shares a single root and a single index, because a
4002/// released preimage advances the lane and cannot be attributed to whichever
4003/// stream happened to carry it. What IS per-stream is the *anchor* — which
4004/// epoch this stream has told the node about.
4005///
4006/// That distinction is the whole reason this type exists. A bare preimage names
4007/// no chain, so the node places it against the anchor of the stream it arrived
4008/// on. If a stream released a reveal before carrying that epoch's root voucher,
4009/// the node could not name the chain the reveal belongs to and would reject it
4010/// `UnanchoredPreimage`. So each stream re-anchors itself whenever the lane
4011/// rolls, rather than trusting a sibling to have done it — QUIC orders within a
4012/// stream, so a fast stream that has already adopted a new root can never
4013/// invalidate a slower sibling still finishing the old one.
4014#[derive(Debug, Default)]
4015struct StreamMeter {
4016    /// The chain this stream has already carried the root voucher for, named by
4017    /// its root. `None` until it has anchored to anything.
4018    anchored_root: Option<B256>,
4019    /// The last proof THIS stream claimed money with, so a `VoucherRejected`
4020    /// read here rewinds that proof and not whatever a sibling stream has done
4021    /// on the lane since (see [`PoolLedger::resolve_reject`]). `None` before the
4022    /// stream has claimed anything, and left alone by a re-anchor — which claims
4023    /// nothing and so has nothing to rewind.
4024    last_proof: Option<StreamProof>,
4025}
4026
4027impl StreamMeter {
4028    /// Ensure this stream has told the node which chain its reveals belong to,
4029    /// opening a chain if the lane meters none, and sending that chain's root
4030    /// voucher first if this stream has not carried it.
4031    ///
4032    /// Called from one place — a stream about to release a reveal, which is the
4033    /// only thing that needs a chain to exist. Residual settlement commits to
4034    /// whatever the lane already meters against and goes straight to
4035    /// [`send_voucher`].
4036    async fn anchor(
4037        &mut self,
4038        send: &mut SendStream,
4039        ctx: &PoolContext,
4040        ledger: &PoolLedger,
4041        rate_per_mb: u64,
4042    ) -> anyhow::Result<()> {
4043        if ledger.chain_root().is_none() {
4044            let (proof, root) =
4045                send_voucher(send, ctx, ledger, rate_per_mb, 0, EpochAction::Open).await?;
4046            self.last_proof = Some(proof);
4047            // The root the voucher signed: the send is what opened the chain,
4048            // and a sibling may have rolled it again since.
4049            self.anchored_root = root;
4050            return Ok(());
4051        }
4052        if ledger.chain_root().is_some() && self.anchored_root != ledger.chain_root() {
4053            // A sibling rolled the lane, or this is our first reveal on a chain a
4054            // sibling opened. Either way this stream must name the chain before it
4055            // can reveal against it (ADR 003 §Concurrent Streams, Rule 1).
4056            //
4057            // This must NOT go through `send_voucher`: every voucher the ledger
4058            // issues folds the chain's accrual, and a voucher that folds must also
4059            // roll — so re-anchoring that way would retire the chain it meant to
4060            // join, strand every sibling's in-flight reveals under the retired
4061            // root, and buy a signature plus a fresh keccak ladder each time.
4062            // `reanchor` re-states the signed anchor under the live root instead,
4063            // folding nothing.
4064            //
4065            // Take the root `reanchor` reports, not one read before the send: the
4066            // lane's live chain is only knowable while the issuance lock is held,
4067            // and storing a stale root would leave this stream re-anchoring on
4068            // every chunk.
4069            self.anchored_root = ledger
4070                .reanchor(|anchor, chain| sign_and_write_voucher(send, ctx, anchor, chain))
4071                .await?;
4072        }
4073        Ok(())
4074    }
4075
4076    /// Meter one delivered chunk: release the next preimage on this stream,
4077    /// rolling the chain first if the epoch is spent.
4078    ///
4079    /// The reveal costs one hash and one 33-byte message — no signature, no
4080    /// acknowledgement, and nothing durable to write — which is the whole point
4081    /// of the chain. A rollover costs one signature, and happens once per 255
4082    /// chunks rather than once per chunk.
4083    async fn release_chunk(
4084        &mut self,
4085        send: &mut SendStream,
4086        ctx: &PoolContext,
4087        ledger: &PoolLedger,
4088        rate_per_mb: u64,
4089    ) -> anyhow::Result<()> {
4090        // One rollover, never more: the first meter can find the epoch spent,
4091        // and the roll that follows opens a chain with index 1 available by
4092        // construction. A second exhaustion would mean the ledger contradicted
4093        // itself. A `Moved` tick re-anchors and meters again; each one means a
4094        // sibling rolled the lane, so the lane progresses every time it repeats.
4095        let mut rolled = false;
4096        loop {
4097            self.anchor(&mut *send, ctx, ledger, rate_per_mb).await?;
4098            // Reborrow per iteration: the closure takes the stream by unique
4099            // reference for the duration of the send, and the loop needs it back.
4100            let wire = &mut *send;
4101            match ledger
4102                .meter(self.anchored_root, |released: Released| async move {
4103                    write_message(
4104                        wire,
4105                        &ClientMessage::ChunkPreimage(ChunkPreimage {
4106                            preimage: released.preimage.into(),
4107                            index: released.index,
4108                        }),
4109                    )
4110                    .await
4111                })
4112                .await?
4113            {
4114                Metered::Released(released) => {
4115                    self.last_proof = Some(StreamProof::Reveal {
4116                        chain_root: released.chain_root,
4117                        index: released.index,
4118                    });
4119                    return Ok(());
4120                }
4121                Metered::Moved => {}
4122                Metered::Exhausted if !rolled => {
4123                    // Roll. The new voucher's `amount` already folds this
4124                    // chain's frontier — every reveal advanced the committed
4125                    // cumulative as it went — so the fold is the frontier
4126                    // actually reached, never a flat 255 (ADR 003 §Rollover).
4127                    let (proof, root) =
4128                        send_voucher(&mut *send, ctx, ledger, rate_per_mb, 0, EpochAction::Roll)
4129                            .await?;
4130                    self.last_proof = Some(proof);
4131                    self.anchored_root = root;
4132                    rolled = true;
4133                }
4134                Metered::Exhausted => {
4135                    // A ledger self-contradiction, not a peer fault: marked as OURS so
4136                    // it is scored as a local fault and never rescued by the
4137                    // voucher-write recovery into a clean completion.
4138                    return Err(
4139                        anyhow::anyhow!("hash chain still exhausted after a rollover")
4140                            .context(LocalPullFault),
4141                    );
4142                }
4143            }
4144        }
4145    }
4146
4147    /// Settle a residual smaller than one chunk with a signed voucher.
4148    ///
4149    /// A preimage always advances the claim by a *whole* chunk, so a partial
4150    /// trailing chunk cannot be priced by one — it settles through a signature,
4151    /// exact to one token base unit.
4152    ///
4153    /// It asks for [`EpochAction::Keep`], and usually does not get it. Any
4154    /// transfer that released at least one reveal has accrual outstanding, and a
4155    /// voucher that folds must also roll ([`PoolLedger::issue`]) — so the typical
4156    /// close retires the chain and opens a fresh one, and a sibling stream's
4157    /// in-flight reveals under the old root land as superseded and re-anchor.
4158    /// That is the correct trade and not an oversight: the fold is what makes the
4159    /// residual exact, and a folded frontier must never coexist with the root
4160    /// that proved it. `Keep` is what a transfer smaller than one whole chunk
4161    /// gets — nothing accrued, nothing to fold — and there it commits the sealed
4162    /// section, since such a lane never opened a chain at all.
4163    async fn settle_residual(
4164        &mut self,
4165        send: &mut SendStream,
4166        ctx: &PoolContext,
4167        ledger: &PoolLedger,
4168        rate_per_mb: u64,
4169        residual_bytes: u64,
4170    ) -> anyhow::Result<()> {
4171        let (proof, root) = send_voucher(
4172            send,
4173            ctx,
4174            ledger,
4175            rate_per_mb,
4176            residual_bytes,
4177            EpochAction::Keep,
4178        )
4179        .await?;
4180        self.last_proof = Some(proof);
4181        self.anchored_root = root;
4182        Ok(())
4183    }
4184
4185    /// Emit the proofs `delivered` new bytes have earned: one reveal per whole
4186    /// chunk, plus a closing signature for any residual once the transfer is
4187    /// complete.
4188    ///
4189    /// Returns the bytes still unproved — always below one chunk.
4190    async fn pay(
4191        &mut self,
4192        send: &mut SendStream,
4193        ctx: &PoolContext,
4194        ledger: &PoolLedger,
4195        rate_per_mb: u64,
4196        mut unproved: u64,
4197        complete: bool,
4198    ) -> anyhow::Result<u64> {
4199        while unproved >= CHUNK_BYTES {
4200            self.release_chunk(send, ctx, ledger, rate_per_mb).await?;
4201            unproved -= CHUNK_BYTES;
4202        }
4203        if complete && unproved > 0 {
4204            self.settle_residual(send, ctx, ledger, rate_per_mb, unproved)
4205                .await?;
4206            unproved = 0;
4207        }
4208        Ok(unproved)
4209    }
4210}
4211
4212/// Turn a mid-stream `StreamError` read off the receive loop into the typed error
4213/// the loop surfaces. There is no positive ack in the pool model — continued
4214/// delivery is acceptance (ADR 005) — so this handles only the rejection slot.
4215///
4216/// A `VoucherRejected` is OUR payment-side fault: rewind the proof `meter` last sent
4217/// (known-not-taken, so it must not be settled optimistically) and carry its typed
4218/// reason — plus the wallet-less-resume `bundle` (#1481) — so the orchestrator can
4219/// exonerate the provider (#857) and the reseed loops (`driver::fill_gap`, the
4220/// `test-util` `fetch_inner`) can self-heal from an authenticated watermark
4221/// instead of treating the rejection as terminal. Any
4222/// OTHER `StreamError` is the upstream refusing mid-stream; carry the typed wire
4223/// class as [`UpstreamRefused`] so a refusing peer is scored on its real class
4224/// rather than the `Unreachable` catch-all (#1145 review).
4225fn voucher_rejection(
4226    ledger: &PoolLedger,
4227    meter: &StreamMeter,
4228    error: StreamError,
4229) -> anyhow::Error {
4230    match error {
4231        StreamError::VoucherRejected { reason, bundle } => {
4232            // Rewind the proof THIS stream sent. The lane is shared, so the proof
4233            // it last issued may belong to a sibling; naming ours is what keeps
4234            // the rejection from un-committing a voucher the node accepted.
4235            if let Some(proof) = meter.last_proof {
4236                ledger.resolve_reject(proof);
4237            }
4238            let proof_generation = match meter.last_proof {
4239                Some(StreamProof::Voucher { generation, .. }) => Some(generation),
4240                Some(StreamProof::Reveal { .. }) | None => None,
4241            };
4242            anyhow::Error::new(UpstreamVoucherRejected {
4243                reason,
4244                bundle,
4245                proof_generation,
4246            })
4247        }
4248        other => anyhow::Error::new(UpstreamRefused::mid_stream(other)),
4249    }
4250}
4251
4252/// Validate + verify a `StreamResponse` on receive (ADR 005, ADR 014 §1, #252).
4253fn verify_response(
4254    resp: &StreamResponse,
4255    ext: &StreamResponseExt,
4256    slash_domain: &Eip712Domain,
4257    expected_signer: Address,
4258    hash: [u8; 32],
4259    pool_id: B256,
4260    timestamp_us: u64,
4261) -> anyhow::Result<()> {
4262    // #252 + slash_sig length/upper-bound checks on the frozen base...
4263    resp.validate()
4264        .map_err(|e| anyhow::anyhow!("invalid stream response: {e}"))?;
4265    // ...and the ok/error agreement, which spans the signed base and the unsigned
4266    // extension, so neither validator can see it alone (ADR 013 §Tier 1).
4267    ext.validate(resp.body.ok)
4268        .map_err(|e| anyhow::anyhow!("invalid stream response: {e}"))?;
4269    // Echoed-field correlation (ADR 005).
4270    if resp.body.hash != hash {
4271        anyhow::bail!("response hash does not match request");
4272    }
4273    if resp.body.pool_id != pool_id.as_slice() {
4274        anyhow::bail!("response pool_id does not match request");
4275    }
4276    if resp.body.timestamp_us != timestamp_us {
4277        anyhow::bail!("response timestamp_us not echoed");
4278    }
4279    // slash_sig must recover to the delivering node's Ethereum address.
4280    let sig = Signature::try_from(resp.slash_sig.as_slice())
4281        .map_err(|e| anyhow::anyhow!("slash_sig parse: {e}"))?;
4282    StreamSlashData::from_response_body(&resp.body)
4283        .verify_signer(&sig, expected_signer, slash_domain)
4284        .map_err(|e| anyhow::anyhow!("slash_sig verification failed: {e}"))?;
4285    Ok(())
4286}
4287
4288async fn write_message(send: &mut SendStream, msg: &ClientMessage) -> anyhow::Result<()> {
4289    // The encode is ours; the write below is the peer's connection.
4290    let payload = encode_message(msg)
4291        .map_err(|e| anyhow::anyhow!("encode failed: {e}").context(LocalPullFault))?;
4292    // A mid-stream voucher write, past the open handshake: `APP_ERR_RATE_LIMITED`
4293    // (`0x10`) is only ever sent before any application stream exists (ADR 013
4294    // §Application Error Codes), so it cannot reach this site. Keep the plain text
4295    // rather than route through `transport_error` — typing a shed here would trust
4296    // that invariant instead of scoping the recovery to the open-stage sites.
4297    write_frame(send, &payload)
4298        .await
4299        .map_err(|e| anyhow::anyhow!("write failed: {e}"))
4300}
4301
4302/// Read the open-stage `StreamResponse` together with its trailing
4303/// [`StreamResponseExt`] (ADR 013 §Tier 1, two-phase).
4304///
4305/// Separate from [`read_client_message`] because only this message carries an
4306/// extension: every mid-stream variant is a single postcard value, and widening
4307/// the shared reader would push an always-`default()` ext onto all of them.
4308async fn read_stream_response(
4309    recv: &mut RecvStream,
4310) -> anyhow::Result<(StreamResponse, StreamResponseExt)> {
4311    let frame = read_frame(recv)
4312        .await
4313        .map_err(|e| rate_limited::transport_error("frame read failed", e))?;
4314    let (msg, tail) = decode_message::<ClientMessage>(&frame)
4315        .map_err(|e| anyhow::anyhow!("decode failed: {e}"))?;
4316    let ClientMessage::StreamResponse(response) = msg else {
4317        anyhow::bail!("expected StreamResponse, got {}", variant_name(&msg));
4318    };
4319    let ext = decdn_protocol::parse_stream_response_ext(tail)
4320        .map_err(|e| anyhow::anyhow!("decode stream response ext: {e}"))?;
4321    Ok((response, ext))
4322}
4323
4324async fn read_client_message<R: tokio::io::AsyncRead + Unpin>(
4325    recv: &mut R,
4326) -> anyhow::Result<ClientMessage> {
4327    // A mid-stream read, past the open handshake. `0x10` reaches only the
4328    // open-stage sites (`connect` / `open_bi` / the initial request write and
4329    // [`read_stream_response`]), never here — the limiters shed before any stream
4330    // exists (ADR 013 §Application Error Codes). Plain text, not `transport_error`.
4331    let frame = read_frame(recv)
4332        .await
4333        .map_err(|e| anyhow::anyhow!("frame read failed: {e}"))?;
4334    let (msg, _rest) = decode_message::<ClientMessage>(&frame)
4335        .map_err(|e| anyhow::anyhow!("decode failed: {e}"))?;
4336    Ok(msg)
4337}
4338
4339const fn variant_name(msg: &ClientMessage) -> &'static str {
4340    match msg {
4341        ClientMessage::StreamRequest(_) => "StreamRequest",
4342        ClientMessage::StreamResponse(_) => "StreamResponse",
4343        ClientMessage::ChunkData(_) => "ChunkData",
4344        ClientMessage::Voucher(_) => "Voucher",
4345        ClientMessage::ChunkPreimage(_) => "ChunkPreimage",
4346        ClientMessage::StreamEnd => "StreamEnd",
4347        ClientMessage::StreamError(_) => "StreamError",
4348    }
4349}
4350
4351/// Record a failed [`open_progressive_pull`] on its span once: the error text
4352/// and an error status, so a failed dial or handshake is not exported as a
4353/// slow success.
4354fn record_open_result<T>(span: &tracing::Span, result: &anyhow::Result<T>) {
4355    if let Err(e) = result {
4356        span.record("error", tracing::field::display(format_args!("{e:#}")));
4357        span.record("otel.status_code", "ERROR");
4358    }
4359}
4360
4361#[cfg(test)]
4362mod tests;