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