Skip to main content

decdn_node/handlers/client/
mod.rs

1//! `cdn/client/v1` handler — paid blob delivery (ADR 005 §`cdn/client/v1`).
2//!
3//! Serves the revenue path: a payer opens one bidirectional QUIC stream per
4//! blob, the node answers with a signed [`StreamResponse`], then streams
5//! [`ChunkData`](decdn_protocol::client::ChunkData) frames, collecting one
6//! hash-chain preimage per delivered
7//! `CHUNK_BYTES` chunk — with a signed `Voucher` to open a chain, to roll one,
8//! and to settle a sub-chunk residual — and pausing only when the unpaid balance
9//! (`delivered − paid`) reaches the credit window — so delivery pipelines
10//! several intervals ahead of payment rather than stopping at each one — and
11//! finishing with [`ClientMessage::StreamEnd`]. A delivery fault rides in the
12//! initial response (`ok: false` + [`StreamError`]); a mid-stream voucher
13//! rejection is sent as a [`ClientMessage::StreamError`] and the stream is
14//! closed **cleanly** (no QUIC reset) so the client can read the reason.
15//!
16//! # Scope (#317 / #327)
17//!
18//! This handler validates vouchers per **lane** — a `(pool_id, signer,
19//! provider)` triple keyed by [`LaneKey`] — against the persisted
20//! [`PoolStateStore`]. A lane's watermark is hydrated from the store at
21//! construction (see [`ClientHandler::new`]) and, for a lane first seen live,
22//! created on the first voucher from its off-chain capability handle. A voucher
23//! whose `pool_id` names an unknown pool is rejected with
24//! [`VoucherRejectReason::WrongPool`]. After accepting a voucher the handler
25//! emits a redeem hint (via the `redeem_hint` sender wired on
26//! [`ClientHandlerDeps`]) so the settlement service can redeem the accrued claim
27//! once it crosses its threshold.
28
29use std::collections::{HashMap, VecDeque};
30use std::sync::Arc;
31use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
32use std::time::Duration;
33
34use alloy::dyn_abi::Eip712Domain;
35use alloy::primitives::{Address, B256, U256};
36use alloy::signers::local::PrivateKeySigner;
37use dashmap::DashMap;
38use decdn_cache::{CHUNK_GROUP_BYTES, CacheEngine, CacheError, Hash};
39use decdn_incentive::rate::{DEFAULT_TOLERANCE_BPS, RateError, min_payment, verify_rate};
40use decdn_incentive::store::{PoolStateStore, StoreError};
41use decdn_incentive::{
42    Capability, LaneKey, LaneState, RetrySignal, SignedCapability, SignedVoucher, StreamSlashData,
43    verify_binding, voucher_reject_reason, wire_voucher_to_signed,
44};
45use decdn_protocol::client::{
46    ClientMessage, StreamError, StreamRequest, StreamRequestExt, StreamResponse,
47    StreamResponseBody, StreamResponseExt, VoucherRejectReason, WatermarkBundle,
48};
49use decdn_protocol::{
50    ALPN_CLIENT, APP_ERR_RATE_LIMITED, CHUNK_BYTES, FrameError, decode_message, encode_message,
51    is_unknown_variant, read_frame, write_frame,
52};
53use iroh::PublicKey;
54use iroh::endpoint::{Connection, RecvStream, SendStream, VarInt};
55use iroh::protocol::{AcceptError, ProtocolHandler};
56use tokio::io::AsyncReadExt;
57use tokio::sync::{Mutex, OwnedSemaphorePermit, Semaphore, mpsc};
58
59use crate::dispatch::{ConnectionLimiter, RejectReason};
60use crate::metrics::Metrics;
61use crate::node_origin::NodeOrigin;
62use crate::receipt_log::{RawReceipt, ReceiptSink};
63use crate::warn_throttle::WarnThrottle;
64
65// The paid-delivery methods are split across concern-focused submodules, each
66// a bare `impl ClientHandler` block over the fields defined here. Support
67// types, consts, and free functions stay in this module so every submodule
68// (and the test module) can reach them via `use super::*` — Rust makes a
69// module's private items visible to its descendants (#1254).
70mod delivery;
71mod dispatch;
72mod fill;
73mod outcome;
74mod proof_wait;
75mod ramp;
76mod serve_encoder;
77mod serve_leg;
78mod serve_waits;
79mod voucher;
80mod window;
81mod wire;
82
83pub(crate) use proof_wait::PROOF_WAIT_CEILING;
84
85/// Default per-connection concurrent-stream cap for `cdn/client/v1` (ADR 005
86/// §Concurrent stream limits). The QUIC transport config also caps bidi
87/// streams at this value; the application semaphore makes the per-ALPN bound
88/// explicit and testable.
89pub const MAX_CLIENT_STREAMS: usize = 100;
90
91// Per-stage timeouts so a stalled peer cannot pin a stream task indefinitely.
92const REQUEST_READ_TIMEOUT: Duration = Duration::from_secs(5);
93/// How long the serve loop waits for a proof while the connection sends no new
94/// STREAM frame. The proofs of one owed chunk never wait past
95/// [`proof_wait::PROOF_WAIT_CEILING`] in total, from the chunk's first proof
96/// wait (see [`proof_wait`]). Traffic on sibling streams of the same
97/// connection counts as progress, so it can hold a wait up to that ceiling.
98const VOUCHER_READ_TIMEOUT: Duration = Duration::from_secs(10);
99const REJECTION_CLOSE_TIMEOUT: Duration = Duration::from_millis(250);
100/// After a clean `StreamEnd`, how long the serve waits for the client's FIN while
101/// draining its send half (see [`drain_recv_to_fin`]). A conforming client
102/// finishes its send right after its last voucher, so the FIN lands within a round
103/// trip; this only bounds a client that completes delivery but never FINs from
104/// pinning the serve task.
105const POST_END_DRAIN_TIMEOUT: Duration = Duration::from_secs(2);
106/// Fallback overall deadline for opening a window-paced pull (#856) when no
107/// pull-through deadline is configured. In practice the runtime always sets
108/// one alongside the window provider, so this only guards a misconfiguration.
109const WINDOW_PULL_FALLBACK_DEADLINE: Duration = Duration::from_mins(1);
110/// The largest first frame a serve writes ([`ClientHandler::first_frame_target`]):
111/// four bao chunk groups. It sits above the one-group floor the pull side
112/// reserves for a prefetch past a shut window (`decdn_client::PULL_WINDOW_FLOOR`),
113/// so the miss leg's pairing holds.
114const FIRST_FRAME_BYTES: u64 = 4 * decdn_bao_range::CHUNK_GROUP_BYTES;
115
116/// Application-layer idle-close ceiling (ADR 005 §Connection lifetime): a served
117/// connection is closed this long after its last stream closes — or after it is
118/// accepted, if no stream ever opens. Distinct from
119/// the QUIC transport idle timeout (`runtime::QUIC_MAX_IDLE_TIMEOUT`, also 30s
120/// today — the two are independent constants that happen to match): keep-alive
121/// PINGs refresh the transport timer, so a peer can hold a connection open
122/// indefinitely while sending zero streams — only this app-layer clock reclaims
123/// it. ADR 005's "no unacknowledged vouchers in flight" clause never delays the
124/// reaper here: vouchers only ever flow *inside* a stream, so a connection with
125/// no stream in flight has no voucher in flight either (sent or received), and
126/// the rule reduces to purely stream-idle.
127const APP_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
128
129// QUIC application error codes (ADR 013 §Application Error Codes). A clean
130// voucher rejection does NOT use these — it writes a `StreamError` frame and
131// finishes the stream so the reason survives.
132const APP_ERR_NO_ERROR: u32 = 0x00;
133const APP_ERR_UNSUPPORTED_MESSAGE: u32 = 0x01;
134const APP_ERR_MALFORMED_MESSAGE: u32 = 0x03;
135
136/// Per-lane delivery state: the validated voucher watermark plus the lane-wide
137/// cumulative byte counter that feeds voucher reconstruction (ADR 003 §Voucher
138/// wire format — `bytes_delivered` is not on the wire).
139#[derive(Debug)]
140struct LaneDeliveryState {
141    state: LaneState,
142    /// Lane-wide cumulative bytes the lane's claim covers: the signed bytes
143    /// extended by the chunks the chain has proved.
144    bytes_delivered_cumulative: U256,
145    /// Cumulative wire bytes CREDITED to streams' paid headroom on this lane
146    /// (design rule #1). Monotone and never exceeds the chain-extended watermark
147    /// the lane has settled, so a benign already-satisfied proof — which does
148    /// not raise the watermark — can only advance a stream's window for bytes
149    /// the lane has actually settled, never for unpaid delivered bytes.
150    paid_credited: U256,
151    /// Wall-clock (Unix milliseconds) of the last accepted voucher on this lane,
152    /// or `0` when this process has accepted none since it hydrated the lane
153    /// (issue #1733). Stamped by [`ClientHandler::commit_one_proof`] under the
154    /// per-lane lock it already holds — a field write, not a separate global
155    /// mutex — and read back by [`LaneActivityClock::ages`] for the admin
156    /// "seconds since last voucher" readout. Best-effort liveness bookkeeping:
157    /// it gates nothing, and the stamp lifecycle follows the lane row (a
158    /// forgotten lane drops it automatically).
159    last_voucher_at: AtomicU64,
160    /// Paid wire bytes that finished streams on this lane returned for the next
161    /// stream's credit ramp (ADR 003 §Credit window). In memory only: a lane this
162    /// process loads or registers starts with an empty pool.
163    ramp_pool: Arc<ramp::RampPool>,
164}
165
166impl LaneDeliveryState {
167    /// The delivery state for a lane this process loads or registers, with its
168    /// whole claim — signed bytes and proved chain frontier — already credited.
169    ///
170    /// Every byte the lane's claim covers was delivered before this entry
171    /// existed — by an earlier process, or before the lane was last forgotten —
172    /// to a stream that either took the credit or ended with it unclaimed. No
173    /// stream outlives the entry it drew on. A new stream delivers
174    /// only new bytes, so none of that claim is headroom it may draw on.
175    /// Crediting only the signed half would hand each restart the proved
176    /// frontier — up to a whole chain — as free credit for proofs that pay
177    /// nothing.
178    fn hydrated(state: LaneState) -> Self {
179        let owed_bytes = state.owed_bytes();
180        Self {
181            state,
182            bytes_delivered_cumulative: owed_bytes,
183            paid_credited: owed_bytes,
184            last_voucher_at: AtomicU64::new(0),
185            ramp_pool: Arc::default(),
186        }
187    }
188}
189
190/// Read handle over the client handler's live lane registry, exposing each
191/// lane's whole-seconds age since its last accepted voucher for the admin
192/// `lanes` surface (issue #1733). Cloning shares the same registry `Arc`, so
193/// the admin surface and the handler observe one set of lanes. The timestamp
194/// lives on the lane's own delivery state, stamped under the per-lane lock the
195/// voucher-accept path already holds, so reading liveness needs no separate
196/// global lock.
197#[derive(Clone)]
198pub struct LaneActivityClock {
199    lanes: Arc<DashMap<LaneKey, Arc<Mutex<LaneDeliveryState>>>>,
200}
201
202impl std::fmt::Debug for LaneActivityClock {
203    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204        f.debug_struct("LaneActivityClock").finish_non_exhaustive()
205    }
206}
207
208impl LaneActivityClock {
209    /// An activity clock over an empty registry — reports "never" for every
210    /// lane. Used where no client handler is wired (a node with no payment
211    /// surface, and admin unit tests).
212    #[must_use]
213    pub fn empty() -> Self {
214        Self {
215            lanes: Arc::new(DashMap::new()),
216        }
217    }
218
219    /// Whole seconds since each live lane last accepted a voucher, keyed by
220    /// [`LaneKey`]. A lane whose stamp is `0` (none accepted since this process
221    /// hydrated it) is omitted, so a caller reads it back as "never" rather than
222    /// a bogus zero age. Best-effort: `Instant`-free wall-clock arithmetic, so a
223    /// clock that stepped backwards can only under-report the age — the safe
224    /// direction for a diagnostic.
225    pub async fn ages(&self) -> HashMap<LaneKey, u64> {
226        let now_ms = unix_millis();
227        // Snapshot the live lane handles first (cloning the per-lane `Arc`s
228        // releases the registry's shard guards), then read each stamp with only
229        // its own (brief) lane lock held — never a shard guard across an await.
230        let handles: Vec<(LaneKey, Arc<Mutex<LaneDeliveryState>>)> = self
231            .lanes
232            .iter()
233            .map(|e| (*e.key(), Arc::clone(e.value())))
234            .collect();
235        let mut out = HashMap::with_capacity(handles.len());
236        for (key, lane) in handles {
237            let stamped = lane.lock().await.last_voucher_at.load(Ordering::Relaxed);
238            if stamped != 0 {
239                out.insert(key, now_ms.saturating_sub(stamped) / 1000);
240            }
241        }
242        out
243    }
244
245    /// Test-only: a clock whose registry holds each `lane` already stamped
246    /// "now", so [`ages`](Self::ages) reports a near-zero age for it. Mirrors
247    /// what the voucher-accept path does to a live lane, without a running
248    /// handler. Used to drive the admin `lanes` ordering test.
249    #[cfg(test)]
250    pub(crate) fn with_stamped_lanes(lanes: &[LaneKey]) -> Self {
251        let now = unix_millis();
252        let map = DashMap::new();
253        for &key in lanes {
254            let state = LaneState::hydrate(
255                key.pool_id,
256                key.signer,
257                key.provider,
258                U256::MAX,
259                0,
260                U256::ZERO,
261                U256::ZERO,
262                None,
263                decdn_incentive::LaneChain::NONE,
264            );
265            map.insert(
266                key,
267                Arc::new(Mutex::new(LaneDeliveryState {
268                    state,
269                    bytes_delivered_cumulative: U256::ZERO,
270                    paid_credited: U256::ZERO,
271                    last_voucher_at: AtomicU64::new(now),
272                    ramp_pool: Arc::default(),
273                })),
274            );
275        }
276        Self {
277            lanes: Arc::new(map),
278        }
279    }
280}
281
282/// Wall-clock now in milliseconds since the Unix epoch, or `0` if the system
283/// clock is before the epoch. Stamps [`LaneDeliveryState::last_voucher_at`] on
284/// each accepted voucher and is read back by [`LaneActivityClock::ages`] (issue
285/// #1733). The `0` fallback doubles as the "never stamped" sentinel and only
286/// makes a lane look staler than it is — the safe direction for a best-effort
287/// diagnostic.
288pub(super) fn unix_millis() -> u64 {
289    std::time::SystemTime::now()
290        .duration_since(std::time::UNIX_EPOCH)
291        .map_or(0, |d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
292}
293
294impl ClientHandler {
295    /// A read handle over this handler's live lane registry for the admin
296    /// `lanes` surface (issue #1733): the last-voucher clock the operator's
297    /// "seconds since last voucher" readout reads. Shares the registry `Arc`, so
298    /// it observes every stamp the voucher-accept path writes without a separate
299    /// shared structure.
300    #[must_use]
301    pub fn lane_activity_clock(&self) -> LaneActivityClock {
302        LaneActivityClock {
303            lanes: Arc::clone(&self.lanes),
304        }
305    }
306}
307
308/// One capability signer's slice of a pool's floor accounting (ADR 003 §Pool
309/// solvency, per-signer floor isolation). Two independent concerns live here.
310///
311/// `live_reservation` is the `µUSDC` this signer's in-flight streams currently
312/// reserve — un-vouchered floor delivered ahead of payment, released as each stream
313/// pays. It is the concurrency/solvency dimension: it carries no memory and never
314/// penalizes a signer for quitting. Bounding it per signer (`k · one_window`) is
315/// what stops one capability key from taking a shared pool's whole live floor
316/// budget from its co-tenants (ADR 003 §Pool solvency, per-signer floor isolation).
317#[derive(Debug, Default, Clone, Copy)]
318pub(super) struct SignerFloorState {
319    live_reservation: U256,
320}
321
322impl SignerFloorState {
323    /// A row that carries no information: no live reservation. An unseen signer reads
324    /// the same, so such a row can be pruned.
325    fn is_empty(self) -> bool {
326        self.live_reservation.is_zero()
327    }
328}
329
330/// One pool's floor accounting (ADR 003 §Pool solvency). `live_reservation` is the
331/// `µUSDC` currently reserved by in-flight streams — the hard money envelope, summed
332/// across every signer and bounded by `remaining − M`. It is ephemeral: no stream is
333/// live at restart, so it clears.
334///
335/// `signers` holds each capability signer's [`SignerFloorState`]: its slice of
336/// `live_reservation`, so admission can bound one signer's concurrent un-vouchered
337/// exposure. The pool's `live_reservation` stays the sum of the signer slices —
338/// every charge and release touches both levels under one lock hold.
339#[derive(Debug, Clone)]
340pub(super) struct PoolFloorState {
341    live_reservation: U256,
342    signers: HashMap<Address, SignerFloorState>,
343    /// Generation stamp, unique across every entry this process creates. A
344    /// [`FloorReservation`] copies it at charge time and both reconcile paths
345    /// compare it, so a guard whose pool was reclaimed
346    /// ([`ClientHandler::forget_pool_floor`] removed the entry) and whose `pool_id`
347    /// a later admission then re-entered reconciles against nothing, rather than
348    /// decrementing counters it never contributed to against a signer row belonging
349    /// to a different pool generation.
350    epoch: u64,
351}
352
353/// Source of [`PoolFloorState::epoch`]. Process-wide and monotonic, so no two
354/// entries share a stamp — including an entry re-created under a `pool_id` a
355/// removed entry previously held.
356static POOL_FLOOR_EPOCH: AtomicU64 = AtomicU64::new(0);
357
358impl Default for PoolFloorState {
359    fn default() -> Self {
360        Self {
361            live_reservation: U256::ZERO,
362            signers: HashMap::new(),
363            epoch: POOL_FLOOR_EPOCH.fetch_add(1, Ordering::Relaxed),
364        }
365    }
366}
367
368impl PoolFloorState {
369    /// Live floor credit committed across every signer on the pool — the pool
370    /// solvency quantity, bounded by `remaining − M`.
371    const fn live_committed(&self) -> U256 {
372        self.live_reservation
373    }
374
375    /// Drop a signer's entry once it carries no information — no live reservation. An
376    /// unseen signer reads the same way (zero live reservation), so keeping such a row
377    /// changes no decision.
378    ///
379    /// Without this the map only ever grows. ADR 003 §Revocation makes short-expiry
380    /// session keys the intended usage, so a busy publisher mints signer identities
381    /// steadily, and every one that reserves and pays cleanly would leave an empty row
382    /// alive until the pool is reclaimed on-chain — inside a map locked on every
383    /// admission.
384    ///
385    /// Safe against a live guard: a repaid guard's `Drop` returns before touching
386    /// the map at all, and an unrepaid guard holds `live_reservation > 0` — every
387    /// admission reserves at least one chunk at the on-chain-floored rate, so a
388    /// reservation is never zero — and it cannot have its row pruned out from
389    /// under it.
390    fn prune_spent(&mut self, signer: Address) {
391        if self
392            .signers
393            .get(&signer)
394            .is_some_and(|lane| lane.is_empty())
395        {
396            self.signers.remove(&signer);
397        }
398    }
399}
400
401/// RAII hold for one stream's span-capped reservation against a pool's budget.
402///
403/// Construction charges the reserved amount to the pool's `live_reservation` (and
404/// the signer's slice of it). [`Self::release_if_repaid`] frees the reservation
405/// once THIS stream's cumulative payment reaches the reserved floor. On drop (every
406/// exit path — success, `?`, disconnect, panic) the guard releases the live
407/// reservation if it was not already repaid, so an abandoned stream never holds a
408/// pool's floor headroom past its own lifetime. The reservation is owned by the
409/// guard and never adjusted by hand, and every counter update saturates.
410#[must_use = "dropping the guard at once releases its live reservation"]
411pub(super) struct FloorReservation {
412    map: Arc<std::sync::Mutex<HashMap<B256, PoolFloorState>>>,
413    pool_id: B256,
414    /// Generation of the [`PoolFloorState`] this reservation is charged against
415    /// (see [`PoolFloorState::epoch`]). Both reconcile paths skip an entry whose
416    /// stamp differs: such an entry belongs to a later pool generation, and this
417    /// guard's reservation went with the one that was removed.
418    epoch: u64,
419    /// The capability signer this reservation belongs to, so its live slice is
420    /// released on the same signer row it was charged to.
421    signer: Address,
422    reserved: U256,
423    /// Set by [`Self::release_live_repaid`]; makes drop a no-op (live already freed).
424    /// Idempotent.
425    repaid: AtomicBool,
426}
427
428impl FloorReservation {
429    /// Reserve `reserved` `µUSDC` of the pool's budget. Charges
430    /// `live_reservation += reserved` (saturating) under the sync lock — an O(1) map
431    /// touch with no `.await` held, so a blocking lock is correct even on the async
432    /// serve path (mirrors `lane_gauge_publish`). A poisoned lock recovers the
433    /// guard rather than panicking; the reservation is best-effort accounting, never
434    /// a safety gate.
435    ///
436    /// The standalone increment-then-build form, used only by the accumulator's
437    /// own unit tests. The serve path admits through
438    /// [`ClientHandler::try_reserve_floor`] instead, which needs the increment to
439    /// happen inside its own check-and-reserve lock hold and so builds the guard
440    /// via [`Self::new_charged`].
441    #[cfg(test)]
442    fn reserve(
443        map: Arc<std::sync::Mutex<HashMap<B256, PoolFloorState>>>,
444        pool_id: B256,
445        signer: Address,
446        reserved: U256,
447    ) -> Self {
448        let epoch = {
449            let mut guard = map
450                .lock()
451                .unwrap_or_else(std::sync::PoisonError::into_inner);
452            let entry = guard.entry(pool_id).or_default();
453            entry.live_reservation = entry.live_reservation.saturating_add(reserved);
454            let lane = entry.signers.entry(signer).or_default();
455            lane.live_reservation = lane.live_reservation.saturating_add(reserved);
456            entry.epoch
457        };
458        Self::new_charged(map, pool_id, signer, reserved, epoch)
459    }
460
461    /// Build a guard for a floor that is ALREADY charged to `live_reservation`
462    /// under the caller's own lock hold. This does NOT touch the map — the
463    /// increment happens exactly once, at the caller's atomic check-and-reserve,
464    /// so re-incrementing here would double-charge the pool. Used by
465    /// [`ClientHandler::try_reserve_floor`], whose single lock hold covers both the
466    /// budget check and the increment; `FloorReservation::reserve` is the test-only
467    /// standalone form that increments first, then delegates here.
468    const fn new_charged(
469        map: Arc<std::sync::Mutex<HashMap<B256, PoolFloorState>>>,
470        pool_id: B256,
471        signer: Address,
472        reserved: U256,
473        epoch: u64,
474    ) -> Self {
475        Self {
476            map,
477            pool_id,
478            epoch,
479            signer,
480            reserved,
481            repaid: AtomicBool::new(false),
482        }
483    }
484
485    /// Mark the reservation repaid: the lane's cumulative payment reached a floor,
486    /// so free the live reservation now (`live_reservation -= reserved`, saturating)
487    /// and make the eventual drop a no-op. Idempotent — only the first call moves
488    /// the live counter.
489    fn release_live_repaid(&self) {
490        if self.repaid.swap(true, Ordering::Relaxed) {
491            return; // already repaid; live already released
492        }
493        let mut guard = self
494            .map
495            .lock()
496            .unwrap_or_else(std::sync::PoisonError::into_inner);
497        // `get_mut`, not `entry().or_default()`, same as the drop guard: if the
498        // pool was reclaimed (`forget_pool_floor` removed its entry) the live
499        // reservation went with the entry, and re-inserting a default state here
500        // resurrects an entry for a closed pool that nothing removes again — the
501        // in-memory face of #1781. The epoch test covers the other half of that
502        // window: an entry present under this `pool_id` but stamped differently is
503        // a LATER generation, re-entered by an admission that ran after the remove,
504        // and subtracting from it would report a reservation this guard never
505        // charged to it.
506        if let Some(entry) = guard
507            .get_mut(&self.pool_id)
508            .filter(|entry| entry.epoch == self.epoch)
509        {
510            entry.live_reservation = entry.live_reservation.saturating_sub(self.reserved);
511            // The signer entry moves with the pool total — the two levels are one
512            // accumulator, so a release that touched only one would leave the
513            // signer's share permanently consumed by a stream that paid for it.
514            if let Some(lane) = entry.signers.get_mut(&self.signer) {
515                lane.live_reservation = lane.live_reservation.saturating_sub(self.reserved);
516            }
517            entry.prune_spent(self.signer);
518        }
519    }
520
521    /// Release the live reservation once cumulative payment (`paid_micro`) reaches
522    /// the amount that was reserved. Matches release to the reserved size at any
523    /// `credit_ramp_divisor`: with the ramp disabled the reservation is the full
524    /// `credit_max`, so release must wait for that much to be paid rather than a
525    /// single chunk. Idempotent (delegates to [`Self::release_live_repaid`]).
526    fn release_if_repaid(&self, paid_micro: U256) {
527        if paid_micro >= self.reserved {
528            self.release_live_repaid();
529        }
530    }
531
532    /// Release the reservation for a serve that was REFUSED before the serve loop
533    /// ran — no upstream USDC fronted, no downstream byte delivered. Frees the live
534    /// reservation promptly (rather than at drop) and marks the guard repaid so the
535    /// drop is a clean no-op. Mechanically a refused-unspent serve and a fully-repaid
536    /// one both owe nothing, so this delegates to [`Self::release_live_repaid`]; the
537    /// distinct name states the intent at the refusal call sites.
538    fn release_unspent(&self) {
539        self.release_live_repaid();
540    }
541}
542
543impl Drop for FloorReservation {
544    fn drop(&mut self) {
545        // Release the live reservation on every exit path (success, `?`, disconnect,
546        // panic) unless a paying stream already released it via
547        // [`Self::release_live_repaid`]. An abandoned stream must not hold a pool's
548        // floor headroom past its own lifetime, so an unrepaid drop frees the
549        // reservation at both the pool and signer levels. No abandonment charge
550        // survives the drop: bounding un-vouchered floor is the admission-time job of
551        // the pool ceiling and the per-signer live cap (ADR 003 §Pool solvency).
552        self.release_live_repaid();
553    }
554}
555
556/// Which floor gate refused an admission
557/// ([`ClientHandler::try_reserve_floor`]). They stay distinct here so the
558/// per-reason metric separates "this pool cannot pay" from the node-local
559/// per-signer live cap, and they part on the wire too: the pool arm speaks
560/// [`StreamError::Unfunded`] to the proven requester (both floor gates run past
561/// the lane-ownership proof), while the signer arm stays a plain `NotFound`.
562#[derive(Debug, Clone, Copy, PartialEq, Eq)]
563pub(super) enum FloorRefusal {
564    /// The pool-wide ceiling: `remaining − M` cannot cover the live floor
565    /// reservation already committed across every signer on the pool plus this new
566    /// floor. This is the hard money envelope; it clears with a top-up.
567    PoolExhausted,
568    /// The per-signer LIVE concurrency cap: the pool can still pay, but THIS signer
569    /// already holds `k` windows of live un-vouchered reservation, so admitting
570    /// another would let one signer front more concurrent credit than its share of
571    /// the pool covers. Carries the cap that refused, read under the same lock hold
572    /// as the decision, so the operator line reports the number that actually
573    /// applied. Clears as the signer's in-flight streams pay.
574    SignerAtCap { signer_cap: U256 },
575}
576
577/// What a floor-admission refusal needs to describe itself
578/// ([`ClientHandler::log_floor_refusal`]). Grouped rather than passed positionally
579/// because the two messages read different fields: the pool arm wants the pool's
580/// headroom against the reserved `ceiling`, the signer arm wants the signer. The
581/// signer's cap is NOT here — it rides on [`FloorRefusal::SignerAtCap`], read under
582/// the lock hold that made the decision. `remaining` is the raw `getPool` value, so
583/// the callee derives the headroom rather than every call site deriving it.
584#[derive(Debug, Clone, Copy)]
585pub(super) struct FloorRefusalSite {
586    pub pool_id: B256,
587    pub signer: Address,
588    pub hash: Hash,
589    pub remaining: U256,
590    /// The `µUSDC` this admission asked to reserve.
591    pub ceiling: U256,
592}
593
594impl From<FloorRefusal> for ServeRejectReason {
595    /// The refusal reason a serve-path admission gate reports for this cap. Keeping
596    /// the mapping on the conversion makes an inconsistent cap/reason pairing
597    /// unrepresentable at the call sites, exactly as
598    /// [`ServeRejectReason::wire_error`] does for the wire codes.
599    fn from(refusal: FloorRefusal) -> Self {
600        match refusal {
601            FloorRefusal::PoolExhausted => Self::InsufficientDeposit,
602            FloorRefusal::SignerAtCap { .. } => Self::SignerFloorAtCap,
603        }
604    }
605}
606
607/// Server-side classification of a `serve_stream` refusal, used to pick the
608/// per-reason reject counter (#876). Finer-grained than the wire `StreamError`,
609/// which carries only the three open-time refusal classes (ADR 005 §Open-time
610/// refusal classes): `CacheMiss`, `UnknownChannel`, and `OwnerMismatch` all
611/// ship as `NotFound` (to avoid leaking channel existence), but are distinct
612/// here so an operator can, e.g., isolate an unknown-channel abuse campaign.
613#[derive(Debug, Clone, Copy, PartialEq, Eq)]
614enum ServeRejectReason {
615    /// The node withdrew the blob after a probe: an operator evict or a
616    /// corruption quarantine with no blacklist entry behind it.
617    EvictedSinceProbe,
618    CacheMiss,
619    /// A fault in this node: a local store fault, or a fill that faulted on
620    /// this node's own side. Signed `Declined`, never a false absence.
621    InternalError,
622    UnknownChannel,
623    OwnerMismatch,
624    /// The pool's on-chain remaining deposit, minus the node's refundable floor
625    /// `M`, can no longer cover a credit window (ADR 003 §Pool solvency). It is
626    /// reachable only past the lane-ownership proof (a known lane keyed to a
627    /// verified signer holding an owner-signed capability), so its audience is the
628    /// proven requester — never an unauthenticated prober — and it ships the wire
629    /// [`StreamError::Unfunded`], which counts toward that requester's funding
630    /// recovery (see [`Self::wire_error`]).
631    InsufficientDeposit,
632    /// A wired `PoolView` could not confirm the request's pool on-chain: the pool
633    /// has no on-chain record, is closed/reclaimed, or the admit-path `getPool`
634    /// faulted; or the admit-path `getAuthorization` faulted for a signer the node
635    /// holds no cached registered read of. The node refuses rather than serve a
636    /// pool it cannot confirm is live and solvent, or a signer it cannot confirm
637    /// can pay (ADR 003 §Pool solvency). Collapses to `NotFound` on the wire
638    /// (see [`Self::wire_error`]) — this refusal can precede any lane-ownership
639    /// proof, and an RPC fault is "not now", so it stays a plain miss: a client
640    /// cannot tell an unconfirmed pool from an absent blob, while an operator can
641    /// tell a chain/RPC problem from real deposit exhaustion.
642    PoolUnconfirmed,
643    /// The request's pool is `Closing` on-chain: it takes no top-up, and its
644    /// redemption ends at the dispute deadline (ADR 003 §Pool solvency).
645    /// `proven` says the requester holds a lane on the pool, so it passed the
646    /// lane-ownership proof: it hears `Unfunded` and its owner opens a new pool.
647    /// Anyone else hears `NotFound`, so pool state stays unmappable.
648    PoolClosing {
649        /// Whether the requester passed the lane-ownership proof.
650        proven: bool,
651    },
652    /// The request's voucher signer cannot pay: it is registered on-chain with
653    /// `cap − spent` below a serve floor or with an expired registration, or its
654    /// lane's capability is inside this node's expiry margin. A signer's `cap` is
655    /// shared across every provider (ADR 003 §Pool solvency), so a "spent"
656    /// capability — one whose signer has already drawn its full `cap` at other
657    /// nodes — is uncashable here: the node would serve for vouchers it could
658    /// never redeem. Ships [`StreamError::Unfunded`] (see [`Self::wire_error`]):
659    /// both gates key on a verified binding for a registered signer or a lane,
660    /// so the requester is proven.
661    SignerCapExhausted,
662    /// One capability signer's live un-vouchered reservation already fills its
663    /// `k`-window concurrency cap (ADR 003 §Pool solvency, per-signer floor
664    /// isolation). The pool itself can still pay and co-tenants are unaffected; the
665    /// cap is signer-scoped and clears as that signer's in-flight streams pay.
666    /// Collapses to `NotFound` on the wire (see [`Self::wire_error`]) — a node-local
667    /// concurrency stop, cleared by waiting rather than by a top-up, so unlike
668    /// [`Self::InsufficientDeposit`] it names no funding state.
669    SignerFloorAtCap,
670    /// A whole-blob request from an active staker, refused while this node's own
671    /// upstream open for the blob is in progress and no live fill exists to
672    /// coalesce onto (#2224). Answering it would let partial holders pull the
673    /// blob from each other in a loop. Collapses to `NotFound` on the wire (see
674    /// [`Self::wire_error`]) so the requester tries its next candidate and scores
675    /// no fault: the refusal is a loop guard, never a sign of a degraded node,
676    /// whatever an earlier tier of the request saw.
677    PullLoopGuard,
678    /// A cache-HIT serve shed under node overload — egress saturation, or this
679    /// client's fair-share cap while the node is pressured. Distinct from
680    /// [`Self::LoadShedMiss`] for the per-reason metric ONLY — both collapse to
681    /// `NotFound` on the wire (see [`Self::wire_error`]) so a client cannot
682    /// read node load, and both are reputation-benign (a client scores
683    /// `NotFound` as no fault).
684    LoadShedHit,
685    /// A cache-MISS serve shed under node overload — concurrency pressure,
686    /// per-client fairness, or egress saturation. See [`Self::LoadShedHit`].
687    LoadShedMiss,
688    /// The requested range starts at or past this node's size claim for the
689    /// blob. Ships [`StreamError::Declined`]: the size is this node's claim,
690    /// and another node can disagree (ADR 005 §Open-time refusal classes).
691    RangeNotSatisfiable,
692    /// A pull-through request whose start sits at or past this node's
693    /// buyer-side `max_blob_size` ceiling (ADR 005 §`max_blob_size` enforcement).
694    /// The gate reads only the requester's `byte_offset`, never the upstream's
695    /// unverified `total_bytes` claim (#1895), and fires before the node commits
696    /// to the stream. Ships the wire [`StreamError::Declined`] (see
697    /// [`Self::wire_error`]), so the requester drops this node for the blob.
698    BlobTooLarge,
699    /// The blob is on this operator's local denylist (ADR 011 §Local Denylist).
700    HashDenied,
701    /// The blob is on the governance blacklist (ADR 011 §On Blacklist Event).
702    /// Separate from [`Self::HashDenied`] for the operator's metrics ONLY — the
703    /// two are deliberately one and the same on the wire, see
704    /// [`Self::wire_error`].
705    ChainHashDenied,
706    /// The channel's funding address is on the origin blacklist — the operator's
707    /// local `denied_origins` or the on-chain one (ADR 011 §On Blacklist Event).
708    OriginDenied,
709    /// The origin-only policy (#1759, `cache.relay_foreign_namespaces = false`)
710    /// declined a hash this node's own backend genuinely does not hold. Ships
711    /// [`StreamError::Declined`] (see [`Self::wire_error`]): this node will not
712    /// relay the hash during this fetch, and another node may (ADR 002
713    /// §Retrieval by namespace).
714    ForeignNamespaceDeclined,
715    /// The node has been unable to reach the chain for longer than
716    /// `blockchain.chain_staleness_grace_sec` (ADR 011 § Serving while
717    /// chain-stale), so its deny-set, pool-solvency, and signer-cap guards are
718    /// all reading stale state. It refuses rather than sign a serve it can no
719    /// longer vouch for. Collapses to `NotFound` on the wire (see
720    /// [`Self::wire_error`]): the refusal is reputation-benign and the client
721    /// should re-route to a peer whose chain reads are live, exactly as for a
722    /// miss.
723    ChainStale,
724}
725
726impl ServeRejectReason {
727    /// The wire class a refusal for this reason signs to the client (ADR 005
728    /// §Open-time refusal classes). The reason is the single source of truth;
729    /// keeping the mapping on the type makes an inconsistent class/reason
730    /// pairing unrepresentable at the call sites, and the finer split survives
731    /// only in the per-reason metric (#876).
732    ///
733    /// The requester side of this mapping is `decdn_client::UpstreamRefused`,
734    /// which recovers the class — and ONLY the class — from a refusal (#1144).
735    /// A requester never ends a fetch on one node's refusal, and no class scores
736    /// the node as degraded.
737    const fn wire_error(self) -> StreamError {
738        match self {
739            // "No at the current funding". Spoken only to a proven requester: the
740            // floor and signer gates run past the lane-ownership proof or key on a
741            // verified binding for a registered signer, and a closing pool names
742            // its audience in `proven`. That requester already reads the pool's
743            // `remaining` and its signer's `{cap, expiry, spent}`, so the class
744            // leaks it nothing (ADR 003 §Pool solvency).
745            Self::InsufficientDeposit
746            | Self::SignerCapExhausted
747            | Self::PoolClosing { proven: true } => StreamError::Unfunded,
748            // "Not now": try other nodes, and this one may answer differently
749            // later. One code for a miss, a load shed, a stale chain, an
750            // unconfirmable pool, and every lane-existence refusal, so none of
751            // them can be told apart off the wire.
752            Self::CacheMiss
753            | Self::UnknownChannel
754            | Self::OwnerMismatch
755            | Self::PoolUnconfirmed
756            | Self::PoolClosing { proven: false }
757            | Self::SignerFloorAtCap
758            | Self::PullLoopGuard
759            | Self::LoadShedHit
760            | Self::LoadShedMiss
761            | Self::ChainStale => StreamError::NotFound,
762            // "This node will not serve this hash during this fetch". The
763            // requester drops this node for the hash and asks no further. One
764            // code for all of them means a client cannot tell a local denylist
765            // from a governance entry or a withdrawn blob, so it cannot map an
766            // operator's private legal exposure (ADR 011 §StreamRequest Response).
767            Self::EvictedSinceProbe
768            | Self::InternalError
769            | Self::RangeNotSatisfiable
770            | Self::BlobTooLarge
771            | Self::HashDenied
772            | Self::ChainHashDenied
773            | Self::OriginDenied
774            | Self::ForeignNamespaceDeclined => StreamError::Declined,
775        }
776    }
777}
778
779/// The result of a reactive cache-miss fill attempt (#1129).
780///
781/// Separates a genuine absence from a transient backend fault, which a bare
782/// `bool` cannot. The cache engine already draws this distinction (it
783/// deliberately prefers `OriginError` over `NotFound` when an origin faulted);
784/// preserving it here keeps the handler from collapsing both to "not filled" and
785/// refusing with `CacheMiss` — wire [`StreamError::NotFound`] — when the
786/// real cause is the operator's own S3/fs origin being down.
787///
788/// Why the reason code matters, stated precisely (the wire codes' own docs in
789/// `decdn_protocol::client` are the authority here):
790///
791/// - `CacheMiss` answers `NotFound` = "not now: try other nodes". It is the code
792///   a healthy-but-empty node returns, and this node can serve later.
793/// - `InternalError` answers `Declined` = "this node will not serve this hash
794///   during this fetch": the client drops this node for the hash, because this
795///   node is broken.
796///
797/// Both steer a client to another node, so this is not the difference between
798/// "retry" and "give up". What it buys is (a) an honest signal that the node is
799/// degraded rather than merely empty, and (b) the per-reason reject metric — the
800/// ONLY server-side place the true cause is observable, since the wire carries
801/// only the reason's class ([`ServeRejectReason::wire_error`]). An operator whose origin is 5xx-ing must
802/// not see that reported as a cache miss.
803///
804/// Note the reason code is NOT covered by the response's EIP-712 `slash_sig`,
805/// which signs only [`StreamResponseBody`] (`ok: false`); `StreamResponse::error`
806/// is explicitly "unsigned and informational". So a misclassification is a
807/// correctness and observability bug, not a false attestation.
808///
809/// A deadline expiry is deliberately NOT a `HardFault` — see
810/// [`ClientHandler::on_pull_through_timeout`].
811#[derive(Debug, Clone, Copy, PartialEq, Eq)]
812enum FillOutcome {
813    /// The blob is now present locally; fall through to the size gate + delivery.
814    Filled,
815    /// The tier produced no blob and no evidence of a fault. Covers BOTH a genuine
816    /// clean miss (a source was asked and did not have it) and a tier that was
817    /// never attempted at all (pull-through unconfigured, or the request not
818    /// authorized to make this node spend). The two are deliberately one variant:
819    /// neither is evidence of a fault, so both leave the terminal classification to
820    /// whatever the other tiers found. Terminal (when no tier fills and none
821    /// faulted): `NotFound`.
822    CleanMiss,
823    /// A backend/store fault, or a fault in this node's own buyer leg — the node is
824    /// degraded, not empty. Terminal: `InternalError`, which answers `Declined`, so a
825    /// client routes around it and the operator's reject metric names the real cause.
826    ///
827    /// Two different lifetimes arrive here, and the variant deliberately does not
828    /// distinguish them, because the client's answer is the same either way:
829    ///
830    /// - TRANSIENT: the operator's origin is 5xx-ing or its store is briefly unhappy.
831    ///   Passes on its own.
832    /// - PERMANENT: this node's buyer side cannot pay at all — a broken signer, an
833    ///   unusable deadline config, a channel store it cannot read (#1560). The node-origin
834    ///   surfaces these as `OriginPullError::Permanent`, which the engine collapses into
835    ///   `CacheError::OriginError` like any other origin failure. It recurs on every
836    ///   request for EVERY hash until an operator intervenes.
837    ///
838    /// Deliberately narrow: ONLY `CacheError::OriginError` and `CacheError::Store`
839    /// qualify. A `BlobTooLarge` / `HashMismatch` / `VerifyFailed` is deterministic
840    /// and will recur on every request for that hash — reporting those as "this
841    /// node is broken" would steer clients off a perfectly healthy node forever
842    /// over one oversized blob.
843    HardFault,
844}
845
846impl FillOutcome {
847    /// The `outcome` value a `pull_through` span records.
848    const fn as_str(self) -> &'static str {
849        match self {
850            Self::Filled => "filled",
851            Self::CleanMiss => "clean_miss",
852            Self::HardFault => "hard_fault",
853        }
854    }
855}
856
857impl FillOutcome {
858    /// The reject reason a *terminal* miss carries, given whether any tier
859    /// attempted for this request hit a hard fault. Falling THROUGH to a further
860    /// tier after a fault is legitimate (a different source may still serve) — so
861    /// a fault seen on an earlier tier must be remembered here rather than
862    /// overwritten by a later clean miss, which would report a degraded node as a
863    /// merely-empty one.
864    const fn miss_reason(fault_seen: bool) -> ServeRejectReason {
865        if fault_seen {
866            ServeRejectReason::InternalError
867        } else {
868            ServeRejectReason::CacheMiss
869        }
870    }
871
872    /// Whether this outcome is a hard backend fault.
873    const fn is_fault(self) -> bool {
874        matches!(self, Self::HardFault)
875    }
876
877    /// Whether the blob is now present locally.
878    const fn is_filled(self) -> bool {
879        matches!(self, Self::Filled)
880    }
881}
882
883/// The serve-miss exit that refused a request. It names the cause in the
884/// refusal's log line only: the wire answer depends on the refusal's reason,
885/// never on its path ([`ServeRejectReason::wire_error`]).
886#[derive(Debug, Clone, Copy, PartialEq, Eq)]
887enum MissPath {
888    /// The pull leg opened, but its ranked candidates do not cover part of the
889    /// range this node does not hold (#2195).
890    CoverageGate(CoverageShortfall),
891    /// The pull leg open ended without a target, for the reason the
892    /// [`PullMiss`](crate::node_origin::PullMiss) names: no reachable provider
893    /// or every candidate declined (`clean_miss`), every candidate quoted above
894    /// the buy ceiling (`below_margin`), a fault in this node (`local_fault`),
895    /// this node's own wallet unable to fund a pool (`funding_needed`), or every
896    /// candidate refusing this node's funding with no recovery step left
897    /// (`unfunded`).
898    PullLegMiss(crate::node_origin::PullMiss),
899    /// The pull leg open ran past its deadline: the configured pull-through
900    /// timeout, or [`WINDOW_PULL_FALLBACK_DEADLINE`] when none is set.
901    PullLegTimeout,
902    /// The buffered pull-through ran and did not fill the blob. A deadline
903    /// expiry lands here too: this tier has no separate timeout path.
904    BufferedMiss,
905    /// The request may make this node spend, but no node-to-node pull-through
906    /// is configured. The local-origin tiers may have run and missed first.
907    NoPullThrough,
908    /// The request may not make this node spend: it carries no verified
909    /// binding, or this node holds no lane for its signer. No fill tier ran.
910    /// A remote peer reaches this path at will, so its line is throttled.
911    NoLane,
912}
913
914impl MissPath {
915    /// The `path` value the refusal's log line records.
916    const fn as_str(self) -> &'static str {
917        match self {
918            Self::CoverageGate(_) => "coverage_gate",
919            Self::PullLegMiss(_) => "pull_leg_miss",
920            Self::PullLegTimeout => "pull_leg_timeout",
921            Self::BufferedMiss => "buffered_miss",
922            Self::NoPullThrough => "no_pull_through",
923            Self::NoLane => "no_lane",
924        }
925    }
926}
927
928/// Why the coverage gate refused: the part of the pull range no ranked
929/// candidate covers (#2195).
930#[derive(Debug, Clone, Copy, PartialEq, Eq)]
931struct CoverageShortfall {
932    /// Start of the range the pull would fetch. It differs from the request's
933    /// start when this serve attaches to a live fill for the front of it.
934    pull_offset: u64,
935    /// Length of the range the pull would fetch.
936    pull_len: u64,
937    /// How many ranked candidates the pull leg holds.
938    candidates: usize,
939    /// The first 1 KiB chunk no candidate covers.
940    first_uncovered_chunk: Option<u64>,
941}
942
943/// A terminal serve-miss refusal: the reject reason
944/// ([`FillOutcome::miss_reason`]) and the exit that refused.
945#[derive(Debug, Clone, Copy, PartialEq, Eq)]
946struct MissRefusal {
947    /// The reason the refusal carries: `CacheMiss`, or `InternalError` when a
948    /// tier this request attempted hit a hard fault.
949    reason: ServeRejectReason,
950    /// The exit that refused.
951    path: MissPath,
952}
953
954impl MissRefusal {
955    /// The refusal `path` sends, given whether any tier attempted for this
956    /// request hit a hard fault.
957    const fn new(fault_seen: bool, path: MissPath) -> Self {
958        Self {
959            reason: FillOutcome::miss_reason(fault_seen),
960            path,
961        }
962    }
963}
964
965/// Construction bundle for [`ClientHandler`] — the 16 required runtime deps plus
966/// every optional wiring hook, so a handler's full configuration is one literal
967/// at its call site instead of a `new()` call followed by a setter chain.
968///
969/// Build it with [`ClientHandlerDeps::new`] (required fields only; every optional
970/// defaults to `None`), set the `Some` optionals the deployment enables, then
971/// pass it to [`ClientHandler::new`]. Each optional field's runtime semantics are
972/// documented on the matching [`ClientHandler`] field.
973pub struct ClientHandlerDeps {
974    /// This node's iroh identity. Carried for diagnostics — the handler does
975    /// not put it on the wire; `StreamResponseBody` has no node-id field.
976    pub node_id: PublicKey,
977    /// Shared metrics registry the serve path records into.
978    pub metrics: Arc<Metrics>,
979    /// Per-source connection-rate gate, applied before any work is done.
980    pub limiter: Arc<ConnectionLimiter>,
981    /// Overload-protection gate: sheds new serves under resource pressure.
982    pub shed: Arc<crate::load_shed::LoadShedController>,
983    /// Blob store the serve path reads from, and the pull-through target on a
984    /// miss.
985    pub cache: CacheEngine,
986    /// The operator's Ethereum key. Signs the `slash_sig` on each
987    /// `StreamResponse`; every other use reads it only for its address.
988    /// Client bindings are verified here, never signed.
989    pub eth_signer: Arc<PrivateKeySigner>,
990    /// EIP-712 domain for slash attestations.
991    pub slash_domain: Eip712Domain,
992    /// EIP-712 domain for payment vouchers.
993    pub voucher_domain: Eip712Domain,
994    /// EIP-712 domain for lane bindings.
995    pub bind_domain: Eip712Domain,
996    /// Durable per-lane cumulative state. Fences voucher replay across a
997    /// restart (ADR 003 §Off-chain voucher state persistence).
998    pub channel_state_store: Arc<dyn PoolStateStore>,
999    /// Non-blocking sink for the served-and-paid audit log. One receipt per
1000    /// accepted voucher, so a single delivery emits several.
1001    pub receipt_sink: Arc<dyn ReceiptSink>,
1002    /// Cached `getPool` view (owner + remaining), read by the floor-`M` solvency
1003    /// gate and the ADR 011 funder gate. `None` (tests) disables both gates —
1004    /// they fail open, exactly as before E4 wired the view.
1005    pub pool_view: Option<Arc<dyn crate::pool_view::PoolView>>,
1006    /// Refundable minimum-remaining-deposit floor `M` (token base units). The
1007    /// seller refuses to serve a lane's pool once its on-chain remaining
1008    /// (`getPool.deposit − getPool.totalRedeemed`) minus this floor can no
1009    /// longer cover the next credit window. Threaded from config by E4/F; here
1010    /// it is a plain field the floor-M guard reads.
1011    pub pool_min_remaining_deposit: U256,
1012    /// The served per-MB price, fixed at startup. Reprice by restarting the
1013    /// daemon (see `runtime::reload::warn_restart_required_sections`).
1014    pub rate_per_mb: u64,
1015    /// Cap on concurrently served streams within one connection — the
1016    /// semaphore is built per accepted connection, not per node.
1017    pub max_concurrent_streams: usize,
1018    /// Live content deny-set (ADR 011): the operator's local denylist unioned
1019    /// with the on-chain origin blacklist. NOT an `Option`, unlike the wiring
1020    /// hooks below — an empty deny-set is a correct steady state (most operators
1021    /// deny nothing), so there is no "unwired" case to represent, and an
1022    /// `Option` would only add a way to fail open on a takedown gate. It is a
1023    /// required [`ClientHandlerDeps::new`] parameter: seeding it empty and
1024    /// relying on the runtime to overwrite it was itself a silent fail-open — a
1025    /// construction site that forgot the wiring was indistinguishable from an
1026    /// operator who denies nothing. Callers with no deny-set (tests) pass
1027    /// `ContentDenylist::empty()` explicitly.
1028    pub content_deny: Arc<crate::content_deny::ContentDenylist>,
1029    /// Liveness of the node's chain reads (ADR 011 § Serving while chain-stale).
1030    /// `Some` only when the blacklist watcher is wired; the admit path refuses a
1031    /// serve once this reads stale. `None` (dev/test, no chain) disables the
1032    /// gate — the same fail-open shape as an unwired [`Self::pool_view`].
1033    pub chain_freshness: Option<crate::chain_freshness::ChainFreshness>,
1034    // Optional wiring — `None` unless the deployment enables the feature.
1035    /// Best-effort nudge to the settlement service that a lane's accrued
1036    /// claim advanced; sent on every accepted voucher, against no threshold.
1037    /// `None` when no settlement service is wired.
1038    pub redeem_hint: Option<mpsc::Sender<LaneKey>>,
1039    /// Deadline for the node-to-node cache-miss pull, so a slow upstream
1040    /// cannot pin the delivery path. `None` disables the node-to-node leg
1041    /// only; a miss can still fill from the local origin via
1042    /// [`Self::local_populate`].
1043    pub pull_through: Option<Duration>,
1044    /// Deadline for filling a miss from this node's OWN configured fs/http/s3
1045    /// origin, tried ahead of any node-to-node path and independent of
1046    /// [`Self::pull_through`]. `None` skips the local-origin tier.
1047    pub local_populate: Option<Duration>,
1048    /// Selects the window-paced node-to-node pull leg, which bounds
1049    /// speculative spend to the ramped credit window. `None` falls back to the
1050    /// buffered `populate` path when [`Self::pull_through`] is set.
1051    pub pull_through_origin: Option<Arc<NodeOrigin>>,
1052    /// Downstream credit-window ceiling in bytes (ADR 003 §Credit window): the
1053    /// per-stream window ramps toward this cap as the stream pays. Defaults to
1054    /// `DEFAULT_CREDIT_MAX` (64 MiB); the runtime sets it from
1055    /// `payment.credit_max`. The SAME ceiling paces the pull leg's upstream
1056    /// speculative spend on a cache-miss pull (`RampPacer`, #1669), so the
1057    /// upstream and downstream ramps never diverge. Floored at one chunk so
1058    /// the serve loop can always make progress.
1059    pub credit_max: u64,
1060    /// Ramp divisor for the credit window (ADR 003 §Credit window): the window is
1061    /// `paid / credit_ramp_divisor`, floored at one chunk and capped at
1062    /// `credit_max`. Defaults to `DEFAULT_CREDIT_RAMP_DIVISOR` (2); the runtime
1063    /// sets it from `payment.credit_ramp_divisor`. `0` opens the full ceiling
1064    /// immediately.
1065    pub credit_ramp_divisor: u64,
1066    /// Target wire-frame size in bytes for the serve path (ADR 005
1067    /// §`cdn/client/v1`). Node-local policy, never negotiated: the payer accepts
1068    /// any non-empty frame, and neither payment nor bao verification is defined
1069    /// over frame boundaries. Defaults to `DEFAULT_FRAME_TARGET_BYTES` (1 MiB);
1070    /// the runtime sets it from `payment.frame_target_bytes`. The serve loop
1071    /// clamps each request to the credit window's remaining room, so this is a
1072    /// ceiling on frame size rather than an exact size.
1073    pub frame_target_bytes: u64,
1074    /// Application-layer idle-close ceiling for a whole connection (ADR 005
1075    /// §Connection lifetime). `None` — the production path — reads as
1076    /// `APP_IDLE_TIMEOUT` (30s); tests set a shorter one.
1077    pub idle_timeout: Option<Duration>,
1078    /// Wall-clock cadence for the mid-stream pool-solvency re-check (ADR 003
1079    /// §Pool solvency). `None` (the default and production path) reads as
1080    /// [`crate::pool_view::POOL_RECHECK_INTERVAL`]; a shorter value is set at
1081    /// construction only by tests, so a drain case need not wait a real interval.
1082    pub pool_recheck_interval: Option<Duration>,
1083    /// Per-signer LIVE concurrency cap `k`, in credit windows (ADR 003 §Pool solvency,
1084    /// per-signer floor isolation): the most live un-vouchered reservation one signer
1085    /// may hold, `k · one_window`. The runtime sets it from
1086    /// `blockchain.pool_floor_signer_live_windows`; the default of `u64::MAX` leaves it
1087    /// effectively unbounded, which is what unit tests that only exercise the pool-wide
1088    /// bound want. Lower-clamped to one window at use.
1089    pub pool_floor_signer_live_windows: u64,
1090    /// ADR 041 serve-credit boundary in front of the per-source warming
1091    /// allowance — the ledger the buy loop
1092    /// ([`crate::node_origin::NodeOriginConfig`]) debits and eviction forgets. On
1093    /// each clean serve the handler enqueues the realized operator margin for the
1094    /// source that speculatively warmed the blob (a no-op for an untagged /
1095    /// non-speculative hash), so the stream task never waits on the ledger lock.
1096    /// Defaults to the inert [`crate::warming_allowance::NoopWarmingCreditSink`];
1097    /// the runtime overwrites it with the channel sink from
1098    /// [`crate::warming_allowance::spawn_warming_creditor`], and a handler left
1099    /// with the default simply applies no warming credit.
1100    pub warming_credit: Arc<dyn crate::warming_allowance::WarmingCreditSink>,
1101    /// Live operator fee-share (basis points) cell (`FeeRouter.getShares()[0]`), the
1102    /// `(1 − f)` numerator the ADR 041 serve credit realizes. Defaults to zero
1103    /// (tests): a zero share credits nothing.
1104    pub operator_shares: crate::fee_shares::OperatorShares,
1105    /// Origin-only policy (#1759). When `false`, `serve_stream` declines any
1106    /// hash its own backend does not hold — including a cache HIT for a
1107    /// foreign hash — before any discovery, lane accounting, or spend. `true`
1108    /// (the default) serves and relays foreign hashes as well.
1109    pub relay_foreign_namespaces: bool,
1110    /// Coarse wall clock for the two reads the voucher-accept path takes under
1111    /// the per-lane lock — the capability-expiry gate and the `last_voucher_at`
1112    /// stamp (issue #1792 item 4). `None` (the default and every test) means the
1113    /// handler builds its own unrefreshed clock, which reads the live wall clock
1114    /// on every call. The runtime sets `Some` with a refresher running, so each
1115    /// of those two reads becomes a relaxed atomic load instead of a
1116    /// `SystemTime::now()` syscall in the critical section.
1117    pub coarse_clock: Option<Arc<crate::coarse_clock::CoarseClock>>,
1118    /// The redeemer's self-tick interval in seconds
1119    /// (`blockchain.redeem_interval_secs`). The voucher-accept path stops
1120    /// accepting vouchers this interval plus the redeemer's landing slack before
1121    /// a capability expires ([`decdn_common::config::capability_expiry_margin_secs`],
1122    /// ADR 003 §Revocation). Defaults to the config default.
1123    pub redeem_interval_secs: u64,
1124}
1125
1126impl std::fmt::Debug for ClientHandlerDeps {
1127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1128        f.debug_struct("ClientHandlerDeps")
1129            .field("node_id", &self.node_id)
1130            .field("max_concurrent_streams", &self.max_concurrent_streams)
1131            .finish_non_exhaustive()
1132    }
1133}
1134
1135impl ClientHandlerDeps {
1136    /// The required runtime deps; every optional wiring hook defaults to `None`.
1137    #[allow(clippy::too_many_arguments)] // required runtime state; optionals set on the returned value.
1138    pub fn new(
1139        node_id: PublicKey,
1140        metrics: Arc<Metrics>,
1141        limiter: Arc<ConnectionLimiter>,
1142        cache: CacheEngine,
1143        eth_signer: Arc<PrivateKeySigner>,
1144        slash_domain: Eip712Domain,
1145        voucher_domain: Eip712Domain,
1146        bind_domain: Eip712Domain,
1147        channel_state_store: Arc<dyn PoolStateStore>,
1148        receipt_sink: Arc<dyn ReceiptSink>,
1149        rate_per_mb: u64,
1150        max_concurrent_streams: usize,
1151        content_deny: Arc<crate::content_deny::ContentDenylist>,
1152        pool_min_remaining_deposit: U256,
1153        shed: Arc<crate::load_shed::LoadShedController>,
1154    ) -> Self {
1155        Self {
1156            node_id,
1157            metrics,
1158            limiter,
1159            shed,
1160            cache,
1161            eth_signer,
1162            slash_domain,
1163            voucher_domain,
1164            bind_domain,
1165            channel_state_store,
1166            receipt_sink,
1167            pool_view: None,
1168            pool_min_remaining_deposit,
1169            rate_per_mb,
1170            max_concurrent_streams,
1171            content_deny,
1172            chain_freshness: None,
1173            redeem_hint: None,
1174            pull_through: None,
1175            local_populate: None,
1176            pull_through_origin: None,
1177            credit_max: decdn_common::config::DEFAULT_CREDIT_MAX,
1178            credit_ramp_divisor: decdn_common::config::DEFAULT_CREDIT_RAMP_DIVISOR,
1179            frame_target_bytes: decdn_common::config::DEFAULT_FRAME_TARGET_BYTES,
1180            idle_timeout: None,
1181            pool_recheck_interval: None,
1182            // No-op per-signer live cap by default: the runtime overrides it from
1183            // config, and a caller that never sets it keeps the pool-wide ceiling as
1184            // the only floor bound (an unbounded per-signer live cap).
1185            pool_floor_signer_live_windows: u64::MAX,
1186            warming_credit: Arc::new(crate::warming_allowance::NoopWarmingCreditSink),
1187            operator_shares: crate::fee_shares::OperatorShares::new(0),
1188            relay_foreign_namespaces: decdn_common::config::DEFAULT_RELAY_FOREIGN_NAMESPACES,
1189            coarse_clock: None,
1190            redeem_interval_secs: decdn_common::config::DEFAULT_REDEEM_INTERVAL_SECS,
1191        }
1192    }
1193}
1194
1195/// Capacity of the capability-verification cache (#1789 item 2). Bounded so a
1196/// flood of distinct capability sends cannot grow it without limit; `ecrecover`
1197/// is expensive enough that a 1024-entry cache still pays for itself across a
1198/// client that re-sends the same capability on every request.
1199///
1200/// The bound is on memory, not on attacker CPU: a flood of distinct garbage
1201/// signatures thrashes the entries and still pays one `ecrecover` per miss, so
1202/// the cache is a hit-path saving rather than an admission control.
1203const CAPABILITY_VERIFY_CACHE_CAPACITY: usize = 1024;
1204
1205/// Cached outcome of a capability owner recovery: the recovered owner, or
1206/// `Invalid` for a signature that does not recover (high-`s`, bad recovery
1207/// id). Both are deterministic per `(digest, signature)`.
1208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1209enum CapabilityVerifyOutcome {
1210    /// `ecrecover` succeeded and recovered this owner.
1211    Owner(Address),
1212    /// The signature is malformed and recovers nothing (deterministic).
1213    Invalid,
1214}
1215
1216/// Bounded LRU cache of capability owner-recovery outcomes, keyed by the full
1217/// signed material `(EIP-712 signing hash, signature bytes)`.
1218///
1219/// Ownership verification in [`ClientHandler::intake_capability`] runs
1220/// `ecrecover` on every capability-carrying request; `ecrecover` is a pure
1221/// function of `(digest, signature)`, so the same signed material always
1222/// recovers the same owner and the result can be cached deterministically.
1223/// Entry is only ever made for the canonical 65-byte EOA signature shape the
1224/// intake path already accepts; an `Invalid` value means "this signature is
1225/// malformed" — also deterministic, also cached, so a repeated malformed
1226/// capability is not re-recovered either.
1227///
1228/// Holds no I/O, and a hit is one hash lookup under one short lock, so the
1229/// cache never lengthens the open path it exists to shorten. It carries no TTL — unlike the TTL-anchored `dht::negative_cache`, an entry
1230/// lives until the capacity evicts it. That is safe because the value is the
1231/// recovered ADDRESS, not a verdict: [`ClientHandler::intake_capability`]
1232/// re-compares it against the live pool owner on every call, so an on-chain
1233/// owner transfer takes effect immediately.
1234#[derive(Debug)]
1235struct CapabilityVerifyCache {
1236    /// `(signing_hash, signature bytes)` → recovered outcome and the tick at
1237    /// which it was last used. Recency lives in the value rather than in the
1238    /// map's order, so a hit is one hash lookup and a field write — no
1239    /// reordering, no memmove.
1240    entries: HashMap<(B256, [u8; 65]), (CapabilityVerifyOutcome, u64)>,
1241    /// Monotonic use counter. Only the ORDER of these values matters, and
1242    /// `u64` at one tick per capability verification does not wrap.
1243    tick: u64,
1244    cap: usize,
1245}
1246
1247impl Default for CapabilityVerifyCache {
1248    fn default() -> Self {
1249        Self::with_capacity(CAPABILITY_VERIFY_CACHE_CAPACITY)
1250    }
1251}
1252
1253impl CapabilityVerifyCache {
1254    fn with_capacity(cap: usize) -> Self {
1255        Self {
1256            entries: HashMap::with_capacity(cap),
1257            tick: 0,
1258            cap: cap.max(1),
1259        }
1260    }
1261
1262    /// The cached recovery outcome for `(signing_hash, signature)`, or `None`
1263    /// on a miss. A hit re-stamps the entry as most-recently-used.
1264    fn get(&mut self, signing_hash: B256, signature: [u8; 65]) -> Option<CapabilityVerifyOutcome> {
1265        self.tick = self.tick.wrapping_add(1);
1266        let tick = self.tick;
1267        let (outcome, last_used) = self.entries.get_mut(&(signing_hash, signature))?;
1268        *last_used = tick;
1269        Some(*outcome)
1270    }
1271
1272    /// Record `outcome` for `(signing_hash, signature)`, evicting the
1273    /// least-recently-used entry when full.
1274    ///
1275    /// The eviction scan is linear in `cap`, but it runs only on an insert that
1276    /// would overflow — i.e. on a path that has just paid an `ecrecover`, which
1277    /// costs orders of magnitude more than a 1024-entry scan. The hit path,
1278    /// which is the one this cache exists to shorten, stays O(1).
1279    fn insert(
1280        &mut self,
1281        signing_hash: B256,
1282        signature: [u8; 65],
1283        outcome: CapabilityVerifyOutcome,
1284    ) {
1285        self.tick = self.tick.wrapping_add(1);
1286        let tick = self.tick;
1287        let key = (signing_hash, signature);
1288        if self.entries.insert(key, (outcome, tick)).is_some() {
1289            return;
1290        }
1291        while self.entries.len() > self.cap {
1292            let Some(least_recent) = self
1293                .entries
1294                .iter()
1295                .min_by_key(|(_, (_, last_used))| *last_used)
1296                .map(|(k, _)| *k)
1297            else {
1298                break;
1299            };
1300            self.entries.remove(&least_recent);
1301        }
1302    }
1303
1304    #[cfg(test)]
1305    fn len(&self) -> usize {
1306        self.entries.len()
1307    }
1308}
1309
1310/// `cdn/client/v1` paid-delivery handler.
1311pub struct ClientHandler {
1312    node_id: PublicKey,
1313    metrics: Arc<Metrics>,
1314    limiter: Arc<ConnectionLimiter>,
1315    /// Overload-protection gate: sheds new serves under resource pressure.
1316    shed: Arc<crate::load_shed::LoadShedController>,
1317    cache: CacheEngine,
1318    eth_signer: Arc<PrivateKeySigner>,
1319    /// `SlashJudge` EIP-712 domain for `StreamResponse.slash_sig`.
1320    slash_domain: Eip712Domain,
1321    /// `PaymentPool` EIP-712 domain for voucher verification.
1322    voucher_domain: Eip712Domain,
1323    /// `CapacityBond` EIP-712 domain for ephemeral `BindNodeId` verification.
1324    bind_domain: Eip712Domain,
1325    channel_state_store: Arc<dyn PoolStateStore>,
1326    /// Refundable minimum-remaining-deposit floor `M` for the floor-M serving
1327    /// guard (see [`ClientHandlerDeps::pool_min_remaining_deposit`]).
1328    pool_min_remaining_deposit: U256,
1329    /// Non-blocking sink for the served-and-paid audit log (issues #248, #803).
1330    /// The voucher-accept path enqueues one receipt here as each voucher is
1331    /// accepted; the actual disk write happens off the hot path in the background
1332    /// receipt writer, so receipt-log I/O can never back-pressure paid delivery.
1333    /// A dropped receipt (queue full) is non-fatal — the payment already advanced
1334    /// the lane watermark.
1335    receipt_sink: Arc<dyn ReceiptSink>,
1336    /// Cached `getPool` view for the floor-`M` and ADR 011 funder gates. `None`
1337    /// (tests) makes both gates fail open.
1338    pool_view: Option<Arc<dyn crate::pool_view::PoolView>>,
1339    /// Liveness of the node's chain reads (ADR 011 § Serving while chain-stale).
1340    /// `Some` only when the blacklist watcher is wired; the admit path refuses
1341    /// once it reads stale. `None` (tests) disables the gate.
1342    chain_freshness: Option<crate::chain_freshness::ChainFreshness>,
1343    /// Per-lane state, hydrated from the store at construction. The sharded map
1344    /// resolves independent lanes concurrently — a lookup keyed by [`LaneKey`]
1345    /// is a point read that only locks that key's shard; each inner mutex
1346    /// serializes voucher application for one lane across its concurrent streams
1347    /// (ADR 003 §concurrent streams). No call site holds a map entry across an
1348    /// `.await`, so lane lookup never blocks an unrelated lane's admission.
1349    lanes: Arc<DashMap<LaneKey, Arc<Mutex<LaneDeliveryState>>>>,
1350    /// Live-lane count backing `decdn_lanes_open` (#1789 item 3): `fetch_add`
1351    /// on a real insert, `fetch_sub` on a real remove, seeded once at
1352    /// construction from the hydrated map. Holding the count in an atomic keeps
1353    /// the gauge off an ordered walk of the (possibly large) lane map on the
1354    /// registration path.
1355    lane_count: AtomicUsize,
1356    /// Test-only: a lane [`ClientHandler::register_lane_after_next_resolution`]
1357    /// armed, registered right after the next request resolves its lane.
1358    #[cfg(any(test, feature = "test-support"))]
1359    lane_after_resolution: std::sync::Mutex<Option<LaneState>>,
1360    /// Serializes the read-and-publish half of [`Self::tune_lane_gauge`], so
1361    /// two concurrent lane-lifecycle calls cannot publish `decdn_lanes_open`
1362    /// out of order. The atomic alone fixes the count, not the publication:
1363    /// without this the slower task's `set_lanes_open` overwrites a
1364    /// fresher lane count with its own staler one, and the gauge stays wrong
1365    /// until the next lifecycle event. Held across one atomic RMW and one gauge
1366    /// store, never across an `.await` or a map walk.
1367    lane_gauge_publish: std::sync::Mutex<()>,
1368    /// Bounded cache of capability owner-recovery outcomes keyed by the full
1369    /// signed material (#1789 item 2), so a client that re-sends the same
1370    /// capability on every request skips the per-request `ecrecover` in
1371    /// [`ClientHandler::intake_capability`].
1372    capability_verify_cache: std::sync::Mutex<CapabilityVerifyCache>,
1373    /// Floor accounting (ADR 003 §Pool solvency): per pool `live_reservation` and, per
1374    /// signer within each pool, that signer's live slice. Guards an O(1) map only and
1375    /// is never held across `.await` — a plain `std::sync::Mutex`, so a
1376    /// [`FloorReservation`]'s `Drop` can reconcile under it (a tokio mutex cannot be
1377    /// locked in `Drop`). Purely in-memory: no stream is live at restart, so it clears.
1378    pool_floor: Arc<std::sync::Mutex<HashMap<B256, PoolFloorState>>>,
1379    /// Per-signer LIVE concurrency cap `k`, in credit windows
1380    /// (see [`ClientHandlerDeps::pool_floor_signer_live_windows`]). Read by
1381    /// [`Self::signer_floor_cap`] on every admission
1382    /// ([`Self::try_reserve_floor`]). The mid-stream and direct-serve re-checks
1383    /// leave it out — see [`Self::pool_budget_covers_reserve`].
1384    pool_floor_signer_live_windows: u64,
1385    /// Redeem-hint sender to the on-chain settlement service (#327), set at
1386    /// construction via [`ClientHandlerDeps`]. `None` when no settlement service
1387    /// is wired (e.g. tests) — a hint is best-effort, so an absent sender or a
1388    /// full channel just skips it. Keyed by [`LaneKey`]: redemption is per-lane.
1389    redeem_hint: Option<mpsc::Sender<LaneKey>>,
1390    /// Node-to-node cache-miss pull-through deadline (#831), set at construction
1391    /// via [`ClientHandlerDeps`]. `None` (the default — feature off, and in
1392    /// tests) disables pull-through: a cache miss returns `NotFound`. When
1393    /// `Some`, a miss *from a request that proves ownership of the named channel*
1394    /// (see [`Self::pull_authorized`]) triggers `cache.populate` (the engine's
1395    /// `NodeOrigin` discovers, pays, pulls, and fills the store), bounded by this
1396    /// deadline so a slow upstream can't pin the delivery path. Proven channel
1397    /// ownership — not mere channel existence, which is public — is the
1398    /// anti-proxy-abuse gate: a client without an owned channel cannot make this
1399    /// node front upstream egress.
1400    pull_through: Option<Duration>,
1401    /// Reactive LOCAL-origin pull-through deadline (#1116), set at construction
1402    /// via [`ClientHandlerDeps`] whenever `[cache.origin]` is configured —
1403    /// INDEPENDENT of `node_to_node_pull_through_enabled`. When `Some`, a cache
1404    /// miss on a proven-owned channel first tries to fill from the node's OWN
1405    /// fs/http/s3 origin (`CacheEngine::populate_local`, which never touches the
1406    /// paid `Peer` origin), so a cache-only operator can reactively serve its own
1407    /// content and a local origin is preferred over the paid peer window path.
1408    /// `None` disables local populate (miss ⇒ node→node path or a plain
1409    /// `NotFound`).
1410    local_populate: Option<Duration>,
1411    /// Window-paced node→node pull-through provider (#856), set at construction
1412    /// via [`ClientHandlerDeps`]. When `Some` (alongside `pull_through`), a cache
1413    /// miss for an offset-0 request that proves channel ownership is served by
1414    /// fusing a progressive upstream pull with downstream delivery — forwarding
1415    /// each chunk to the paying client and teeing it into the cache — so
1416    /// per-request speculative exposure is bounded to the ramped credit window
1417    /// (#1669) instead of the whole blob. `None` keeps the buffered `populate`
1418    /// path (`pull_through`) or a plain `NotFound`.
1419    pull_through_origin: Option<Arc<NodeOrigin>>,
1420    /// Downstream credit-window ceiling in bytes (ADR 003 §Credit window), set at
1421    /// construction via [`ClientHandlerDeps`]. The serve loop keeps streaming
1422    /// while `delivered − paid ≤ credit_window`, collecting cumulative vouchers as
1423    /// they arrive instead of stalling a full round trip at every interval. Read
1424    /// through [`Self::credit_window`], which ramps from one interval toward this
1425    /// ceiling as `paid` grows.
1426    credit_max: u64,
1427    /// Ramp divisor for the credit window (ADR 003 §Credit window), set at
1428    /// construction via [`ClientHandlerDeps`]. Read through
1429    /// [`Self::credit_window`].
1430    credit_ramp_divisor: u64,
1431    /// Target wire-frame size in bytes, set at construction via
1432    /// [`ClientHandlerDeps`]. Read through [`Self::frame_target`], which clamps it
1433    /// to the credit window's remaining room.
1434    frame_target_bytes: u64,
1435    /// Live content deny-set (ADR 011), keyed on the pool FUNDER. Consulted at
1436    /// two points, both of which must gate or the check is bypassable: the
1437    /// open-time funder gate in `serve_stream`, right after the pool status
1438    /// resolves, and [`Self::takedown_landed`], the per-interval in-flight
1439    /// re-check every serve leg runs. The open-time gate precedes every fill
1440    /// tier, so omitting it would have this node front upstream USDC or origin
1441    /// egress on a blacklisted funder's behalf. The hash half of ADR 011 is the
1442    /// cache's own deny state (`CacheEngine::is_denied` / `refuses`).
1443    pub(crate) content_deny: Arc<crate::content_deny::ContentDenylist>,
1444    /// Served per-MB price, fixed at startup (see
1445    /// [`ClientHandlerDeps::rate_per_mb`]).
1446    rate_per_mb: u64,
1447    max_concurrent_streams: usize,
1448    /// Throttle for the insufficient-deposit refusal `warn!` (#1520). Unkeyed:
1449    /// the aggregate answers the triage question — "one client ran dry" versus
1450    /// "I am refusing everyone" — and the per-channel detail lives in the
1451    /// `debug!` beside it and in the counter.
1452    deposit_refusal_warn: WarnThrottle,
1453    /// The same window for the per-signer LIVE-cap arm. Kept separate from the
1454    /// pool arm because the two carry different remedies — a pool-wide shortfall
1455    /// clears with a top-up, a signer at its share does not — so one must not
1456    /// starve the other's line or pollute its `suppressed` count.
1457    signer_cap_refusal_warn: WarnThrottle,
1458    /// Throttle for the `warn!` on a client binding whose signature is invalid
1459    /// or recovers a different address. A remote peer triggers it at will. The
1460    /// two causes share one window on purpose: both are the same client fault
1461    /// with the same remedy (the client signs its binding wrongly), and each
1462    /// event also counts into `decdn_serve_stream_rejected_bad_binding_total`.
1463    binding_warn: WarnThrottle,
1464    /// Throttle for the `warn!` on a stream request that carries no verified
1465    /// binding. A remote peer triggers it at will.
1466    unbound_request_warn: WarnThrottle,
1467    /// Throttle for the `warn!` on a stream request that names no known lane. A
1468    /// remote peer triggers it at will.
1469    unknown_lane_warn: WarnThrottle,
1470    /// Throttle for the serve-miss refusal line on a request with no lane here
1471    /// ([`MissPath::NoLane`]). A remote peer triggers it at will.
1472    no_lane_miss_log: WarnThrottle,
1473    /// Application-layer idle-close ceiling (ADR 005 §Connection lifetime).
1474    /// `None` (the default and production path) reads as [`APP_IDLE_TIMEOUT`]
1475    /// (30s); a shorter value is set at construction via [`ClientHandlerDeps`]
1476    /// only by tests, so an idle-close case need not wait a real 30s.
1477    idle_timeout: Option<Duration>,
1478    /// Wall-clock cadence for the mid-stream pool-solvency re-check (ADR 003
1479    /// §Pool solvency), read through [`Self::pool_recheck_interval`]. `None` (the
1480    /// default and production path) reads as
1481    /// [`crate::pool_view::POOL_RECHECK_INTERVAL`]; a shorter value is set at
1482    /// construction via [`ClientHandlerDeps`] only by tests.
1483    pool_recheck_interval: Option<Duration>,
1484    /// ADR 041 serve-credit sink in front of the per-source warming allowance the
1485    /// buy loop and eviction share. Enqueues the realized operator margin on each
1486    /// clean serve (a no-op for an untagged hash) without taking the ledger lock.
1487    warming_credit: Arc<dyn crate::warming_allowance::WarmingCreditSink>,
1488    /// Live operator fee-share (basis points), the ADR 041 serve-credit numerator.
1489    operator_shares: crate::fee_shares::OperatorShares,
1490    /// Origin-only policy (#1759), set at construction via
1491    /// [`ClientHandlerDeps::relay_foreign_namespaces`]. Read by the gate at the
1492    /// top of `serve_stream`.
1493    relay_foreign_namespaces: bool,
1494    /// Coarse wall clock for the voucher-accept path's two under-lock reads —
1495    /// the capability-expiry gate and the `last_voucher_at` stamp (issue #1792
1496    /// item 4). The runtime supplies one with a background refresher; a handler
1497    /// built without one (tests) gets a fresh [`CoarseClock`](crate::coarse_clock::CoarseClock)
1498    /// that reads the live wall clock on every call, so behavior is unchanged
1499    /// there.
1500    coarse_clock: Arc<crate::coarse_clock::CoarseClock>,
1501    /// How long before a capability's expiry the voucher-accept path stops
1502    /// accepting vouchers, in seconds: one redeem interval plus the redeemer's
1503    /// landing slack ([`decdn_common::config::capability_expiry_margin_secs`]).
1504    capability_expiry_margin_secs: u64,
1505}
1506
1507impl std::fmt::Debug for ClientHandler {
1508    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1509        f.debug_struct("ClientHandler")
1510            .field("node_id", &self.node_id)
1511            .field("max_concurrent_streams", &self.max_concurrent_streams)
1512            .finish_non_exhaustive()
1513    }
1514}
1515
1516impl ClientHandler {
1517    /// The ALPN this handler answers on: `cdn/client/v1`, the single paid
1518    /// delivery protocol for both client-to-node and node-to-node transfers.
1519    pub const ALPN: &'static [u8] = ALPN_CLIENT;
1520
1521    /// Construct the handler from [`ClientHandlerDeps`], hydrating per-channel
1522    /// state from the deps' `channel_state_store`.
1523    ///
1524    /// All optional runtime wiring (settlement redeem hints, pull-through
1525    /// deadlines, the window/leech providers, …) is supplied on `deps` as
1526    /// `Some`/`None` at construction — there is no post-construction attach step,
1527    /// so a handler's full wiring is one reviewable literal at its call site.
1528    ///
1529    /// # Errors
1530    ///
1531    /// Propagates a [`decdn_incentive::StoreError`] if the persisted channel
1532    /// state cannot be loaded — the node must not serve paid delivery without
1533    /// knowing prior voucher state (the #527 replay guard).
1534    pub fn new(deps: ClientHandlerDeps) -> anyhow::Result<Self> {
1535        let map: DashMap<LaneKey, Arc<Mutex<LaneDeliveryState>>> = DashMap::new();
1536        for state in deps.channel_state_store.load_all()? {
1537            map.insert(
1538                state.key(),
1539                Arc::new(Mutex::new(LaneDeliveryState::hydrated(state))),
1540            );
1541        }
1542        // Seed the atomic lane counter once at hydrate; `register_lane` /
1543        // `forget_lane` tune it from then on (#1789 item 3). The pool deposit
1544        // behind these lanes is published separately, by the redeemer tick that
1545        // can actually see it (#2072).
1546        let open_lanes = map.len();
1547        let lane_count = AtomicUsize::new(open_lanes);
1548        deps.metrics.set_lanes_open(open_lanes);
1549        // The floor accumulator is purely in-memory: no stream is live at boot, so
1550        // every `live_reservation` starts at zero and nothing is loaded from disk.
1551        let pool_floor: HashMap<B256, PoolFloorState> = HashMap::new();
1552        Ok(Self {
1553            node_id: deps.node_id,
1554            metrics: deps.metrics,
1555            limiter: deps.limiter,
1556            shed: deps.shed,
1557            cache: deps.cache,
1558            eth_signer: deps.eth_signer,
1559            slash_domain: deps.slash_domain,
1560            voucher_domain: deps.voucher_domain,
1561            bind_domain: deps.bind_domain,
1562            channel_state_store: deps.channel_state_store,
1563            pool_min_remaining_deposit: deps.pool_min_remaining_deposit,
1564            receipt_sink: deps.receipt_sink,
1565            pool_view: deps.pool_view,
1566            chain_freshness: deps.chain_freshness,
1567            lanes: Arc::new(map),
1568            lane_count,
1569            #[cfg(any(test, feature = "test-support"))]
1570            lane_after_resolution: std::sync::Mutex::new(None),
1571            lane_gauge_publish: std::sync::Mutex::new(()),
1572            capability_verify_cache: std::sync::Mutex::new(CapabilityVerifyCache::default()),
1573            pool_floor: Arc::new(std::sync::Mutex::new(pool_floor)),
1574            pool_floor_signer_live_windows: deps.pool_floor_signer_live_windows,
1575            redeem_hint: deps.redeem_hint,
1576            pull_through: deps.pull_through,
1577            local_populate: deps.local_populate,
1578            pull_through_origin: deps.pull_through_origin,
1579            credit_max: deps.credit_max,
1580            credit_ramp_divisor: deps.credit_ramp_divisor,
1581            frame_target_bytes: deps.frame_target_bytes,
1582            content_deny: deps.content_deny,
1583            rate_per_mb: deps.rate_per_mb,
1584            max_concurrent_streams: deps.max_concurrent_streams,
1585            deposit_refusal_warn: WarnThrottle::new(Self::DEPOSIT_REFUSAL_WARN_INTERVAL),
1586            signer_cap_refusal_warn: WarnThrottle::new(Self::DEPOSIT_REFUSAL_WARN_INTERVAL),
1587            binding_warn: WarnThrottle::new(Self::PEER_FAULT_WARN_INTERVAL),
1588            unbound_request_warn: WarnThrottle::new(Self::PEER_FAULT_WARN_INTERVAL),
1589            unknown_lane_warn: WarnThrottle::new(Self::PEER_FAULT_WARN_INTERVAL),
1590            no_lane_miss_log: WarnThrottle::new(Self::PEER_FAULT_WARN_INTERVAL),
1591            idle_timeout: deps.idle_timeout,
1592            pool_recheck_interval: deps.pool_recheck_interval,
1593            warming_credit: deps.warming_credit,
1594            operator_shares: deps.operator_shares,
1595            relay_foreign_namespaces: deps.relay_foreign_namespaces,
1596            // A handler with no clock wired (tests) reads the live wall clock on
1597            // every call, exactly as before #1792 item 4.
1598            coarse_clock: deps
1599                .coarse_clock
1600                .unwrap_or_else(|| Arc::new(crate::coarse_clock::CoarseClock::new())),
1601            capability_expiry_margin_secs: decdn_common::config::capability_expiry_margin_secs(
1602                deps.redeem_interval_secs,
1603            ),
1604        })
1605    }
1606
1607    /// ADR 041 serve credit: return the realized operator margin to the source that
1608    /// speculatively warmed `hash`, for a clean serve of `served_bytes`. The credit
1609    /// is `(operator_bps / 10_000) · P_sell · MB(served_bytes)`, matching the
1610    /// buy-loop debit's units. A no-op for an untagged hash (own-namespace or
1611    /// non-speculative), so calling it unconditionally on every serve is correct.
1612    ///
1613    /// The arithmetic runs here and the ledger update is handed to the wired
1614    /// [`crate::warming_allowance::WarmingCreditSink`], so this — the stream
1615    /// task's last act on a clean completion — costs whatever that sink costs.
1616    /// The runtime's channel sink is one tag-map shard read and one bounded
1617    /// `try_send`, and never waits on the bucket lock the buy loop takes, which
1618    /// is what the sink contract requires of anything on this path.
1619    fn credit_warming_serve(&self, hash: Hash, served_bytes: u64) {
1620        let sell_rate = self.rate_per_mb;
1621        let margin_per_mb = sell_rate
1622            .saturating_mul(u64::from(self.operator_shares.bps()))
1623            .saturating_div(10_000);
1624        let mb = served_bytes.div_ceil(decdn_protocol::MB_BYTES);
1625        self.warming_credit
1626            .credit(hash, mb.saturating_mul(margin_per_mb));
1627    }
1628
1629    /// The wall-clock cadence for the mid-stream pool-solvency re-check (ADR 003
1630    /// §Pool solvency): the configured test override, else
1631    /// [`crate::pool_view::POOL_RECHECK_INTERVAL`].
1632    pub(super) fn pool_recheck_interval(&self) -> Duration {
1633        self.pool_recheck_interval
1634            .unwrap_or(crate::pool_view::POOL_RECHECK_INTERVAL)
1635    }
1636
1637    /// The pool's cached `getPool` status (owner + `remaining`), or `None` when no
1638    /// pool-view is wired or the read faulted. Both the takedown funder resolution
1639    /// ([`Self::pool_funder`]) reads through this at the START of a serve leg; a
1640    /// `None` result makes the caller fail open — a transient RPC blip must not stop
1641    /// a paying stream, and the on-chain `redeem` is the backstop. MAY block on a
1642    /// `getPool` fetch on a cache miss, so it is NOT for the per-voucher-boundary
1643    /// path — the mid-stream re-check uses [`Self::pool_view_status_cached`].
1644    pub(super) async fn pool_view_status(
1645        &self,
1646        pool_id: B256,
1647    ) -> Option<crate::pool_view::PoolStatus> {
1648        self.pool_view.as_ref()?.status(pool_id).await
1649    }
1650
1651    /// CACHE-ONLY pool status for the per-voucher-boundary mid-stream solvency
1652    /// re-check: never triggers a `getPool` `eth_call`, so it cannot stall the serve
1653    /// loop when the RPC is slow. Returns `None` when nothing fresh is cached (the
1654    /// re-check then fails open, exactly as on a fetch fault) — a long stream whose
1655    /// admission read has aged past the cache TTL simply stops re-checking rather
1656    /// than blocking delivery on a fresh read. The on-chain `redeem` remains the
1657    /// backstop, and a wider serve-path RPC reduction is tracked separately.
1658    pub(super) async fn pool_view_status_cached(
1659        &self,
1660        pool_id: B256,
1661    ) -> Option<crate::pool_view::PoolStatus> {
1662        self.pool_view.as_ref()?.cached_status(pool_id).await
1663    }
1664
1665    /// The signer's total on-chain `spent` across every provider in `pool_id`, read
1666    /// from the event-fed projection ONLY (never a `getAuthorization`), for the
1667    /// mid-stream signer cap-headroom re-check. `None` when no pool-view is wired
1668    /// (dev/test, no chain) or the view holds no projection — the re-check then skips
1669    /// and delivery continues, matching how [`Self::pool_view_status_cached`] fails
1670    /// open. See [`crate::pool_view::PoolView::signer_spent_cached`].
1671    pub(super) async fn signer_spent_cached(&self, pool_id: B256, signer: Address) -> Option<u64> {
1672        self.pool_view
1673            .as_ref()?
1674            .signer_spent_cached(pool_id, signer)
1675            .await
1676    }
1677
1678    /// Whether a live stream must stop because its voucher `signer` has drained its
1679    /// shared on-chain `cap` at other nodes since admission (ADR 003 §Pool solvency,
1680    /// mid-stream re-check). A signer's `cap` is shared across every provider, so a
1681    /// signer that spends it elsewhere leaves `held_cap − spent` unable to cover a
1682    /// serve floor here, and further vouchers redeem `min(desired, cap − spent) ≈ 0`
1683    /// — the node would eat the delivered bytes, unbounded for a large blob.
1684    ///
1685    /// `held_cap` is the `cap` the node already holds on the lane (the admit-time
1686    /// capability), and `spent` is read from the event-fed projection. The floor is
1687    /// one ramp-start credit window, the SAME quantity the admit-time gate confirms
1688    /// (`ClientHandler::serve_stream`), so the mid-stream threshold matches admission.
1689    ///
1690    /// Fails toward SERVING: `signer_spent_cached` returning `None` (no projection)
1691    /// skips the stop, and a projection that has not yet folded the signer's
1692    /// pre-admission spend only UNDER-counts `spent`, over-stating headroom. That is
1693    /// acceptable — the admit-time `getAuthorization` (#1958) already caught an
1694    /// already-exhausted signer authoritatively, so this only catches drain SINCE
1695    /// admit, and the on-chain `redeemMany` `min(desired, cap − spent)` is the
1696    /// backstop; this re-check only BOUNDS over-delivery, it is not a correctness
1697    /// gate. The caller ends the stream as `Stopped { SignerCapExhausted }`, which
1698    /// the dispatch sink counts on the mid-stream metric.
1699    pub(super) async fn signer_cap_drained_midstream(
1700        &self,
1701        pool_id: B256,
1702        signer: Address,
1703        held_cap: U256,
1704        rate_per_mb: u64,
1705    ) -> bool {
1706        let Some(spent) = self.signer_spent_cached(pool_id, signer).await else {
1707            return false;
1708        };
1709        let headroom = held_cap.saturating_sub(U256::from(spent));
1710        let floor_micro = U256::from(decdn_incentive::min_payment(
1711            self.credit_window(CHUNK_BYTES, 0),
1712            rate_per_mb,
1713        ));
1714        headroom < floor_micro
1715    }
1716
1717    /// The pool's funder (`getPool.owner`) for the ADR 011 mid-stream takedown
1718    /// re-check, or `None` when no pool-view is wired or the read faulted (the
1719    /// re-check then falls back to the open-time gates and the hash-denylist
1720    /// re-check). Cached, so a per-MB call is cheap.
1721    pub(super) async fn pool_funder(&self, pool_id: B256) -> Option<Address> {
1722        self.pool_view_status(pool_id).await.map(|s| s.owner)
1723    }
1724
1725    /// Recover an owner-signed capability's EIP-712 signer (ADR 003
1726    /// §Capability delegation), short-circuiting on the capability-verification
1727    /// cache (#1789 item 2). The caller compares the result against the pool
1728    /// owner; recovery says who signed, not whether the grant is accepted.
1729    ///
1730    /// `ecrecover` is a pure function of `(digest, signature)`, so the
1731    /// recovered owner for a given signed material is deterministic and can be
1732    /// cached safely: [`Self::capability_verify_cache`] maps the full
1733    /// `(signing hash, signature bytes)` to the recovered owner, and a client
1734    /// that re-sends the same capability (the documented recovery path) hits
1735    /// the cache instead of paying a fresh `ecrecover` per request. A
1736    /// tampered signature is a different key, so it can never be served a
1737    /// stale cached owner. Caching the recovered ADDRESS rather than an
1738    /// accept/reject verdict is what keeps this sound across an on-chain owner
1739    /// transfer: the comparison is re-made against the live owner every call.
1740    fn recover_capability_owner(&self, grant: &SignedCapability) -> CapabilityVerifyOutcome {
1741        let domain = &self.voucher_domain;
1742        let signing_hash = grant.capability.signing_hash(domain);
1743        let signature = grant.signature.as_bytes();
1744        // The lock is held only for the cache lookup/insert — a hash lookup plus
1745        // an `IndexMap` shift. The expensive `recover_owner` (secp256k1
1746        // ecrecover) runs OUTSIDE the lock, so a burst of distinct
1747        // capabilities at session open does not serialize on the cache mutex
1748        // and block a Tokio worker for the crypto. Concurrent races on the
1749        // same key re-run the recovery and overwrite the entry — duplicate
1750        // work under races is acceptable for a deterministic result.
1751        let recovered = {
1752            let mut cache = self
1753                .capability_verify_cache
1754                .lock()
1755                .unwrap_or_else(std::sync::PoisonError::into_inner);
1756            cache.get(signing_hash, signature)
1757        };
1758        if let Some(outcome) = recovered {
1759            return outcome;
1760        }
1761        let outcome = match grant.recover_owner(domain) {
1762            Ok(owner) => CapabilityVerifyOutcome::Owner(owner),
1763            Err(_) => CapabilityVerifyOutcome::Invalid,
1764        };
1765        self.capability_verify_cache
1766            .lock()
1767            .unwrap_or_else(std::sync::PoisonError::into_inner)
1768            .insert(signing_hash, signature, outcome);
1769        outcome
1770    }
1771
1772    /// Accept an owner-signed capability presented at session start (ADR 003
1773    /// §Capability delegation): verify the owner signature against the on-chain
1774    /// pool owner, register the lane so the voucher path serves it, and persist
1775    /// the grant so the redeemer can register the signer on its first on-chain
1776    /// redemption. `signer` is the request's bound Ethereum address; `pool_id`
1777    /// is [`StreamRequest::pool_id`]; `pool_owner` is `getPool.owner` from the
1778    /// cached pool-view.
1779    ///
1780    /// The owner check is authoritative here, not deferred: the on-chain
1781    /// `PaymentPool.redeemMany` verifies every capability's owner signature
1782    /// against `pools[poolId].owner` and reverts the WHOLE batch on one bad
1783    /// grant, which would strand every other lane's redemption in an
1784    /// indefinite-retry tick. So a grant whose owner signature does not verify
1785    /// against the pool owner is DROPPED — never persisted, never lane-registered.
1786    ///
1787    /// The verification recovers the 65-byte EOA signature via
1788    /// [`Self::recover_capability_owner`] (canonical-`s` ecrecover, matching the
1789    /// contract's verifiable set) and compares it to `pool_owner` — the confirmed
1790    /// on-chain owner the admit path already resolved (the dispatcher calls this
1791    /// only past the refuse-on-unknown-pool gate). A non-65-byte (contract-wallet /
1792    /// ERC-1271-shaped) owner signature drops rather than persists: it cannot be
1793    /// recovered off-chain, and this design does not accept contract wallets as
1794    /// pool owners.
1795    ///
1796    /// Best-effort and off the durability path otherwise: a persist failure never
1797    /// fails the stream. Lane registration is idempotent — a re-observed grant for
1798    /// an already-tracked lane does NOT reset the accepted-voucher watermark
1799    /// (#527); the lane's `cap`/`expiry` are set once at first registration.
1800    #[allow(clippy::cognitive_complexity)] // linear verify → register → persist sequence.
1801    fn intake_capability(
1802        &self,
1803        pool_id: B256,
1804        signer: Address,
1805        pool_owner: Address,
1806        capability: &decdn_protocol::client::WireCapability,
1807    ) {
1808        let spending_cap = capability.spending_cap;
1809        let expiry = capability.expiry;
1810        // Only the common EOA (65-byte) owner signature is recoverable off-chain;
1811        // a contract-wallet signature is dropped rather than persisted unverified.
1812        let Ok(sig_bytes) = <[u8; 65]>::try_from(capability.owner_signature.as_slice()) else {
1813            tracing::debug!(%pool_id, %signer, "dropping capability: owner signature is not a recoverable EOA signature");
1814            return;
1815        };
1816        let Ok(signature) = alloy::primitives::Signature::from_raw(&sig_bytes) else {
1817            tracing::debug!(%pool_id, %signer, "dropping capability: malformed owner signature");
1818            return;
1819        };
1820        let grant = SignedCapability {
1821            capability: Capability {
1822                signer,
1823                spending_cap,
1824                pool_id,
1825                expiry,
1826            },
1827            signature,
1828        };
1829        // #1789 item 2: the ecrecover is cached keyed by the full signed
1830        // material, so a client that re-sends the same capability on every
1831        // request (the documented recovery path for a lost lane) skips it. The
1832        // two drop reasons stay distinct in the log: a malformed signature is a
1833        // client-side signing bug, while a well-formed signature recovering to
1834        // the wrong address is operator-actionable — usually a client still
1835        // signing against an owner the pool has since transferred away.
1836        match self.recover_capability_owner(&grant) {
1837            CapabilityVerifyOutcome::Owner(owner) if owner == pool_owner => {}
1838            CapabilityVerifyOutcome::Owner(recovered) => {
1839                tracing::warn!(
1840                    %pool_id,
1841                    %signer,
1842                    error = %decdn_incentive::capability::CapabilityError::WrongOwner {
1843                        expected: pool_owner,
1844                        recovered,
1845                    },
1846                    "dropping capability: owner verification failed"
1847                );
1848                return;
1849            }
1850            CapabilityVerifyOutcome::Invalid => {
1851                tracing::debug!(
1852                    %pool_id,
1853                    %signer,
1854                    error = %decdn_incentive::capability::CapabilityError::InvalidSignature,
1855                    "dropping capability: owner verification failed"
1856                );
1857                return;
1858            }
1859        }
1860
1861        // The grant is authentic. Register the lane so the voucher path accepts
1862        // vouchers for `(pool_id, signer, this operator)` — without this a
1863        // brand-new lane's first request is never served, since the serve gate
1864        // admits only known lanes. Idempotent for an already-tracked lane.
1865        //
1866        // The verified owner signature rides the lane record itself, alongside
1867        // the `cap`/`expiry` it authorizes: it is the `ownerSig` the redeemer
1868        // submits as a `CapabilityReg` on the signer's first on-chain redemption.
1869        // Kept ON the lane — not in a side table — so it is written in the same
1870        // durable transaction as the voucher frontier and can never be lost while
1871        // the frontier survives (#1906). A buffered in-memory insert like every
1872        // lane `record`; the row lands on disk in the periodic lane flush's
1873        // fsynced commit, not a per-request fsync on the intake path.
1874        let mut lane = LaneState::hydrate(
1875            pool_id,
1876            signer,
1877            self.eth_signer.address(),
1878            // `LaneState.cap` stays a `U256`; the capability's `u64` cap
1879            // zero-extends into it losslessly.
1880            U256::from(spending_cap),
1881            expiry,
1882            U256::ZERO,
1883            U256::ZERO,
1884            None,
1885            decdn_incentive::LaneChain::NONE,
1886        );
1887        lane.owner_sig = Some(sig_bytes);
1888        if let Err(e) = self.register_lane(lane) {
1889            tracing::warn!(%pool_id, %signer, error = %e, "lane registration failed; the request refuses as an unknown lane and the client retries");
1890        }
1891    }
1892
1893    /// Register a lane so the voucher path accepts vouchers for it — its
1894    /// capability handle is registered on-chain and its [`LaneState`] recorded
1895    /// in the lane store, then inserted into the live map. Called both from the
1896    /// on-chain capability-registration consumer (#327) and from the
1897    /// seller-side capability intake on the first voucher of a new lane.
1898    ///
1899    /// **Idempotent:** a re-observed registration for an already-tracked lane is
1900    /// a no-op — it MUST NOT reset the accepted-voucher watermark and reopen the
1901    /// #527 replay window. The live map (hydrated from the store at
1902    /// construction, updated here) is the authority.
1903    ///
1904    /// # Errors
1905    ///
1906    /// Propagates a [`StoreError`] if the store `record` fails.
1907    pub fn register_lane(&self, state: LaneState) -> Result<(), StoreError> {
1908        let key = state.key();
1909        if self.lanes.contains_key(&key) {
1910            return Ok(());
1911        }
1912        // #1789 item 4: call the store's `record` inline. `register_lane` sits
1913        // ON the per-stream open path (the first capability of every stream
1914        // reaches it), so it cannot afford a `spawn_blocking` hop — and does
1915        // not need one: `PoolStateStore::record` is contractually non-blocking,
1916        // a buffered insert into the store's in-memory working set, with the
1917        // periodic lane flush doing the disk work.
1918        self.channel_state_store.record(&state)?;
1919
1920        // Only a REAL insertion (a vacant slot) tunes the lane gauge — the
1921        // entry guard is atomic, so racing first-streams on one lane still
1922        // count it once.
1923        if let dashmap::mapref::entry::Entry::Vacant(entry) = self.lanes.entry(key) {
1924            entry.insert(Arc::new(Mutex::new(LaneDeliveryState::hydrated(state))));
1925            self.tune_lane_gauge(1);
1926        }
1927        Ok(())
1928    }
1929
1930    /// Test-only: register `state` right after the next `serve_stream` resolves
1931    /// its lane. It stands in for a sibling stream whose capability intake lands
1932    /// while this request is past its lane resolution, the window in which the
1933    /// request must keep the answer it resolved.
1934    #[cfg(any(test, feature = "test-support"))]
1935    pub fn register_lane_after_next_resolution(&self, state: LaneState) {
1936        if let Ok(mut armed) = self.lane_after_resolution.lock() {
1937            *armed = Some(state);
1938        }
1939    }
1940
1941    /// Test-only: register the lane [`Self::register_lane_after_next_resolution`]
1942    /// armed, if any. `serve_stream` calls it once its lane is resolved.
1943    #[cfg(any(test, feature = "test-support"))]
1944    pub(super) fn fire_lane_after_resolution(&self) {
1945        let armed = self
1946            .lane_after_resolution
1947            .lock()
1948            .ok()
1949            .and_then(|mut armed| armed.take());
1950        if let Some(state) = armed
1951            && let Err(e) = self.register_lane(state)
1952        {
1953            tracing::warn!(error = %e, "test seam: armed lane registration failed");
1954        }
1955    }
1956
1957    /// Clamp a live lane to its signer's on-chain registration (ADR 003
1958    /// §Capability delegation): the lane holds `min(held, registered)` for `cap`
1959    /// and `expiry` and records the registered expiry as `registered_until`. On-chain
1960    /// registration is write-once, so the clamp is final for this `(pool, signer)`
1961    /// and the lane store persists it with the lane. A change is recorded to the
1962    /// buffered store under the per-lane lock, the same path as a voucher advance.
1963    /// A store fault is logged and leaves the in-memory clamp in place: the live
1964    /// lane is what the voucher path reads.
1965    async fn clamp_lane_to_registration(
1966        &self,
1967        lane: &Arc<Mutex<LaneDeliveryState>>,
1968        cap: u64,
1969        expiry: u64,
1970    ) {
1971        let mut guard = lane.lock().await;
1972        if !guard.state.clamp_to_registration(cap, expiry) {
1973            return;
1974        }
1975        tracing::debug!(
1976            pool_id = %guard.state.pool_id,
1977            signer = %guard.state.signer,
1978            held_cap = %guard.state.cap,
1979            held_expiry = guard.state.expiry,
1980            "lane clamped to the signer's on-chain registration"
1981        );
1982        if let Err(error) = self.channel_state_store.record(&guard.state) {
1983            tracing::warn!(
1984                pool_id = %guard.state.pool_id,
1985                signer = %guard.state.signer,
1986                %error,
1987                "failed to record a lane clamped to its signer's registration"
1988            );
1989        }
1990    }
1991
1992    /// Drop a settled lane from the live map and the lane store, where the removal
1993    /// is a buffered tombstone that the next flush applies to disk. Idempotent —
1994    /// forgetting an unknown lane is a no-op.
1995    ///
1996    /// # Errors
1997    ///
1998    /// Propagates a [`StoreError`] if the store cannot take the tombstone (its
1999    /// buffer mutex is poisoned), or if the blocking store task fails to join —
2000    /// a panic inside the store, or the runtime shutting down mid-call.
2001    pub async fn forget_lane(&self, key: LaneKey) -> Result<(), StoreError> {
2002        // Removing the lane row drops its `last_voucher_at` stamp with it (issue
2003        // #1733): the timestamp lives on the lane's delivery state, so its
2004        // lifecycle follows the lane automatically — no separate activity-map
2005        // eviction. Only a real removal tunes the lane gauge down.
2006        if self.lanes.remove(&key).is_some() {
2007            self.tune_lane_gauge(-1);
2008        }
2009        let store = Arc::clone(&self.channel_state_store);
2010        tokio::task::spawn_blocking(move || store.forget(key))
2011            .await
2012            .map_err(|e| StoreError::Backend(format!("forget_lane join: {e}")))?
2013    }
2014
2015    /// Minimum gap between insufficient-deposit `warn!` lines (#1520).
2016    ///
2017    /// A module const, not a config field: a log cadence does not justify the five
2018    /// config sites (schema, resolver, validate summary, template, docs) a
2019    /// `[payment]` knob costs, and no operator needs to tune it.
2020    pub(super) const DEPOSIT_REFUSAL_WARN_INTERVAL: Duration = Duration::from_mins(5);
2021
2022    /// Minimum gap between `warn!` lines a remote peer can trigger at will: a bad
2023    /// binding, a request with no binding, a request on an unknown lane. Each
2024    /// line carries the offending `peer` and the `suppressed` count.
2025    pub(super) const PEER_FAULT_WARN_INTERVAL: Duration = Duration::from_mins(1);
2026
2027    /// Emit the observable side of a floor-admission refusal, picking the message
2028    /// the refusing gate actually justifies.
2029    ///
2030    /// The gates need different words and different remedies.
2031    /// [`FloorRefusal::PoolExhausted`] is the pool running dry, which
2032    /// [`Self::log_deposit_refusal`] already describes. The per-signer live cap is the
2033    /// opposite situation: `try_reserve_floor` tests the pool ceiling FIRST, so
2034    /// reaching it proves the pool can pay. Reporting it as a deposit shortfall
2035    /// would print a headroom that visibly exceeds the ceiling beside a sentence
2036    /// denying it, and would send the operator to check RPC health while the real cause
2037    /// is one signer's node-local live cap.
2038    fn log_floor_refusal(&self, refusal: FloorRefusal, at: FloorRefusalSite) {
2039        let FloorRefusalSite {
2040            pool_id,
2041            signer,
2042            hash,
2043            remaining,
2044            ceiling,
2045        } = at;
2046        let headroom = remaining.saturating_sub(self.pool_min_remaining_deposit);
2047        match refusal {
2048            FloorRefusal::PoolExhausted => {
2049                self.log_deposit_refusal(pool_id, hash, headroom, ceiling);
2050            }
2051            FloorRefusal::SignerAtCap { signer_cap } => {
2052                self.log_signer_at_cap(pool_id, signer, hash, signer_cap, ceiling, headroom);
2053            }
2054        }
2055    }
2056
2057    /// The observable side of a per-signer LIVE-cap refusal
2058    /// ([`FloorRefusal::SignerAtCap`]).
2059    fn log_signer_at_cap(
2060        &self,
2061        pool_id: B256,
2062        signer: Address,
2063        hash: Hash,
2064        signer_cap: U256,
2065        ceiling: U256,
2066        headroom: U256,
2067    ) {
2068        tracing::debug!(
2069            %pool_id, %signer, %hash, %signer_cap, %ceiling, %headroom,
2070            "refusing delivery: this capability signer already holds its live concurrency cap \
2071             of un-vouchered floor reservation; the pool itself can still pay"
2072        );
2073        if let Some(suppressed) = self.signer_cap_refusal_warn.admit() {
2074            tracing::warn!(
2075                %pool_id, %signer, %signer_cap, %headroom, suppressed,
2076                interval = ?self.signer_cap_refusal_warn.interval(),
2077                "refusing a paying client: one capability signer is running its whole live \
2078                 concurrency cap of un-vouchered streams at once while the pool is solvent. This \
2079                 clears as those streams pay; a sustained rate means that signer runs more \
2080                 concurrent un-vouchered streams than its cap covers. Rotate the session key or \
2081                 widen pool_floor_signer_live_windows — see docs/runbook.md, which covers why \
2082                 widening it needs a restart"
2083            );
2084        }
2085    }
2086
2087    /// Emit the observable side of an insufficient-deposit refusal (#1520).
2088    ///
2089    /// Unconditional `debug!` so a support ticket is answerable at all, plus a
2090    /// throttled `warn!` ([`WarnThrottle`]). The wire class is deliberately
2091    /// lossy — it carries no reason and no amount — so without these the only
2092    /// trace of a refusal's numbers is a counter.
2093    ///
2094    /// `headroom` and `ceiling` are both in payment-token base units.
2095    pub(super) fn log_deposit_refusal(
2096        &self,
2097        pool_id: B256,
2098        hash: Hash,
2099        headroom: U256,
2100        ceiling: U256,
2101    ) {
2102        tracing::debug!(
2103            %pool_id, %hash, %headroom, %ceiling,
2104            "refusing delivery: pool's refundable remaining deposit cannot cover the reserved cost"
2105        );
2106        if let Some(suppressed) = self.deposit_refusal_warn.admit() {
2107            tracing::warn!(
2108                %pool_id, %headroom, %ceiling, suppressed,
2109                interval = ?self.deposit_refusal_warn.interval(),
2110                "refusing paying clients: pool's refundable remaining deposit below the reserved cost. \
2111                 A sustained rate here is either a client running dry (no action) or this \
2112                 node's chain watcher lagging behind an on-chain top-up (check RPC health) \
2113                 — see docs/runbook.md"
2114            );
2115        }
2116    }
2117
2118    /// The frame size to request from a frame producer, given `unvouchered` — the
2119    /// bytes delivered since the last payment-chunk boundary — and `interval_bytes`,
2120    /// the payment quantum.
2121    ///
2122    /// **A frame never crosses a payment-chunk boundary.** Both sides meter the same
2123    /// byte stream over the same frame sequence, and each waits for the other at
2124    /// every `interval_bytes`: the node accumulates `unvouchered` until it reaches a
2125    /// full interval and then demands a proof, while the payer releases one
2126    /// hash-chain preimage per interval. A frame that straddled a boundary would
2127    /// land the payer's counter PAST it, which is settled with a signed voucher for
2128    /// the residual instead of a preimage — a per-frame signature on the hot path,
2129    /// and a cadence the two sides no longer agree on. Cutting at the boundary keeps
2130    /// the preimage path exact, whatever the target.
2131    ///
2132    /// This also bounds the node's credit exposure. The serve loop checks the window
2133    /// BEFORE each send, so `delivered - paid` overshoots by at most one frame; with
2134    /// frames capped at one interval the overshoot is at most one payment chunk —
2135    /// the same granularity the node bills at, and the bound ADR 003 §Credit window
2136    /// states as "the window, plus at most one chunk".
2137    ///
2138    /// `room` — the window's unused remainder, `window - (delivered - paid)` — caps
2139    /// the request as well, and that cap is load-bearing on the **cache-miss** leg
2140    /// rather than merely tidy. Both loops prefetch one frame ahead of the window
2141    /// check, and on a miss the producer is fed by an upstream pull paced against
2142    /// this stream's own served-and-paid frontier. Asking a closed window for a full
2143    /// frame parks the prefetch on bytes that cannot arrive until the loop exits to
2144    /// recoup — which it cannot do while parked. Sizing the request to the room that
2145    /// actually remains keeps the prefetch satisfiable from what is already buffered.
2146    ///
2147    /// The room cap is floored at one bao chunk group so a shut window yields a
2148    /// short frame rather than a single byte; that runt goes out once the recoup
2149    /// reopens the window. A stream sitting at the one-chunk credit floor therefore
2150    /// spends about two frames per payment chunk — a full frame, then the runt the
2151    /// shut window produced — while a ramped stream keeps `room` far above the floor
2152    /// and spends one.
2153    ///
2154    /// **The floor is exactly one group because the pull side reserves exactly one.**
2155    /// On a cache miss the bytes this prefetch asks for can only come from the
2156    /// upstream pull, and `decdn_client::PULL_WINDOW_FLOOR` carries a third
2157    /// chunk group for precisely this request. Raising the floor here without
2158    /// raising it there parks the serve leg on bytes the pull may not draw —
2159    /// a hang, not a failed assertion. `frame_target_room_floor_matches_the_pull_reservation`
2160    /// pins the pair.
2161    pub(super) fn frame_target(&self, unvouchered: u64, interval_bytes: u64, room: u64) -> usize {
2162        // `unvouchered < interval_bytes` at every call site — both loops reset it to
2163        // zero the moment it reaches a full interval. Were that ever to break, a
2164        // saturating `0` would ask for one-byte frames forever: a stream that still
2165        // "progresses" at six orders of magnitude below line rate, with no error and
2166        // no metric. Fall back to a whole interval instead, which is wrong in the
2167        // same direction as the rest of the clamp rather than catastrophically.
2168        debug_assert!(
2169            unvouchered < interval_bytes,
2170            "unvouchered {unvouchered} must stay below the {interval_bytes}-byte interval"
2171        );
2172        let to_boundary = interval_bytes
2173            .checked_sub(unvouchered)
2174            .filter(|remaining| *remaining > 0)
2175            .unwrap_or(interval_bytes);
2176        let want = self
2177            .frame_target_bytes
2178            .min(to_boundary)
2179            .min(room.max(decdn_bao_range::CHUNK_GROUP_BYTES));
2180        // Saturating toward `usize::MAX` would tell a producer to buffer without
2181        // bound — the O(blob size) behaviour the framers exist to avoid. Every term
2182        // above is already `<= CHUNK_BYTES`, so this only ever runs on a 16-bit
2183        // target, where one chunk group is the safe answer.
2184        usize::try_from(want)
2185            .unwrap_or_else(|_| usize::try_from(decdn_bao_range::CHUNK_GROUP_BYTES).unwrap_or(1))
2186    }
2187
2188    /// The target size of a stream's first frame: [`Self::frame_target`] at the
2189    /// start of an interval, capped at [`FIRST_FRAME_BYTES`]. A serve writes no
2190    /// byte until its first frame is full, so the cap is what the first byte
2191    /// waits for: a few chunk groups of store read on a hit, or of upstream pull
2192    /// on a miss, rather than a whole interval. Every later frame keeps the
2193    /// full target, so the frames still land on the interval boundary.
2194    pub(super) fn first_frame_target(&self, interval_bytes: u64, opening_window: u64) -> usize {
2195        let cap = usize::try_from(FIRST_FRAME_BYTES).unwrap_or(usize::MAX);
2196        self.frame_target(0, interval_bytes, opening_window)
2197            .min(cap)
2198    }
2199
2200    /// The effective downstream credit window in bytes for a stream whose voucher
2201    /// interval is `interval_bytes` and whose ramp input is `paid` (ADR 003
2202    /// §Credit window): the stream's own confirmed payment plus the credit it
2203    /// carries from its lane ([`RampCarry::ramp_paid`](ramp::RampCarry::ramp_paid)).
2204    /// The window ramps from its floor toward `credit_max` as `paid` grows:
2205    /// `paid / credit_ramp_divisor` once that clears the floor, the floor itself
2206    /// below that point (including at `paid == 0`), and the full `credit_max`
2207    /// when `credit_ramp_divisor` is `0`. The floor is one interval plus
2208    /// [`decdn_client::VERIFY_LAG_BYTES`]: the payer pays only for bytes it has
2209    /// verified, so it must receive the leaf that straddles an interval boundary
2210    /// before it can pay for that interval, and the loop always makes progress.
2211    pub(super) fn credit_window(&self, interval_bytes: u64, paid: u64) -> u64 {
2212        decdn_incentive::ramped_credit_window(
2213            self.credit_ramp_divisor,
2214            interval_bytes.saturating_add(decdn_client::VERIFY_LAG_BYTES),
2215            self.credit_max,
2216            paid,
2217        )
2218    }
2219
2220    /// The most ramp credit a lane banks: the paid bytes that open one window at
2221    /// `credit_max`. `0` when the ramp is disabled, because the window then starts
2222    /// at the ceiling.
2223    const fn ramp_credit_cap(&self) -> u64 {
2224        self.credit_max.saturating_mul(self.credit_ramp_divisor)
2225    }
2226
2227    /// Take `lane`'s banked ramp credit for one new stream (ADR 003 §Credit
2228    /// window). A stream with no tracked lane carries nothing.
2229    async fn take_ramp_carry(
2230        &self,
2231        lane: Option<&Arc<Mutex<LaneDeliveryState>>>,
2232    ) -> ramp::RampCarry {
2233        let Some(lane) = lane else {
2234            return ramp::RampCarry::detached();
2235        };
2236        let pool = Arc::clone(&lane.lock().await.ramp_pool);
2237        ramp::RampCarry::take(pool, self.ramp_credit_cap())
2238    }
2239
2240    /// The refundable floor-`M` serving guard (shared-payment-pool model): the
2241    /// seller keeps serving a pool only while its on-chain **remaining**
2242    /// (`getPool.deposit − getPool.totalRedeemed`) minus the configured
2243    /// minimum-remaining-deposit floor `M` can still cover the reserved credit
2244    /// window, i.e. `remaining − M ≥ min_payment(reserved_bytes, rate)`. `M` is
2245    /// the refundable minimum the pool owner is guaranteed to keep, so the node
2246    /// refuses to serve into it. Pure and total (saturating), so it is testable
2247    /// without any chain access.
2248    ///
2249    /// `remaining` is a chain quantity read from `getPool`; the handler does not
2250    /// hold an RPC client, so **E4 threads `remaining` to every call site** (from
2251    /// the redemption/pool watcher's cached `getPool` view, cached like
2252    /// `withdrawn_cache`). This method owns only the policy arithmetic.
2253    pub(super) fn pool_remaining_covers_window(
2254        &self,
2255        remaining: U256,
2256        reserved_bytes: u64,
2257        rate_per_mb: u64,
2258    ) -> bool {
2259        let refundable_headroom = remaining.saturating_sub(self.pool_min_remaining_deposit);
2260        refundable_headroom >= min_payment(reserved_bytes, rate_per_mb)
2261    }
2262
2263    /// Open a span-capped [`FloorReservation`] against `pool_id`'s budget for one
2264    /// stream. The serve loop holds the returned guard for the stream's lifetime and
2265    /// releases the reservation once a floor is repaid; on any other drop the guard
2266    /// releases the live reservation, freeing the pool's floor headroom.
2267    // Test-only: the serve path admits through `try_reserve_floor`, which checks
2268    // every floor gate and reserves under one lock hold.
2269    #[cfg(test)]
2270    pub(super) fn reserve_floor(
2271        &self,
2272        pool_id: B256,
2273        signer: Address,
2274        reserved: U256,
2275    ) -> FloorReservation {
2276        FloorReservation::reserve(Arc::clone(&self.pool_floor), pool_id, signer, reserved)
2277    }
2278
2279    /// One credit window in `µUSDC` priced at `rate_per_mb`: the ramp-start credit
2280    /// window (one chunk normally, the full `credit_max` when
2281    /// `credit_ramp_divisor == 0`), which is exactly what a fresh stream reserves. It
2282    /// is the unit the per-signer live cap is denominated in.
2283    pub(super) fn one_window(&self, rate_per_mb: u64) -> U256 {
2284        min_payment(self.credit_window(CHUNK_BYTES, 0), rate_per_mb)
2285    }
2286
2287    /// The per-signer LIVE concurrency cap on un-vouchered floor reservation (ADR 003
2288    /// §Pool solvency, per-signer floor isolation): the most `live_reservation` any
2289    /// ONE capability signer may hold against this pool, `k · one_window`.
2290    ///
2291    /// **The window is the unit being rationed.** A signer's honest need for
2292    /// un-vouchered floor credit does not scale with the pool's size: every admission
2293    /// reserves at most one window, and a paying stream releases its reservation as
2294    /// soon as it covers it, so honest need is `concurrent un-vouchered streams × one
2295    /// window` whether the pool holds ten dollars or a hundred thousand. An ABSOLUTE
2296    /// `k`-window cap therefore fits the need directly, rather than a fraction of the
2297    /// deposit that would be loose in both directions on a large pool.
2298    ///
2299    /// `k` is lower-clamped to one window, so a lone signer's first stream is always
2300    /// admissible on any solvent pool. This carries no permanent memory: it is pure
2301    /// concurrency, released as each stream pays, so it never penalizes a signer for
2302    /// quitting.
2303    ///
2304    /// Pure and total (saturating), so it is testable without any chain access.
2305    pub(super) fn signer_floor_cap(&self, rate_per_mb: u64) -> U256 {
2306        let one_window = self.one_window(rate_per_mb);
2307        // Lower-clamp `k` to one window: `k = 0` (or an unset field) still admits a
2308        // lone signer's first stream rather than wedging it.
2309        let k = self.pool_floor_signer_live_windows.max(1);
2310        one_window.saturating_mul(U256::from(k))
2311    }
2312
2313    /// POOL solvency: does the pool's `remaining − M` cover its already-committed LIVE
2314    /// floor reservation across every signer, plus `new_reserve`? Reads the floor
2315    /// accumulator; pure arithmetic otherwise. A poisoned
2316    /// accumulator lock recovers the guard rather than panicking: every critical
2317    /// section over this mutex is panic-free by construction — saturating `U256`
2318    /// arithmetic and infallible map operations only, no indexing and no `unwrap` — so
2319    /// a poison cannot originate from a holder of this lock, and a recovered guard
2320    /// cannot observe a torn pool/signer split. Do NOT add a fallible or panicking
2321    /// call under this lock; that argument is what the recovery rests on, because the
2322    /// pool total and the signer entries must agree.
2323    ///
2324    /// (The recovery is not justified by the accumulator being "best-effort": it
2325    /// gates whether the node fronts upstream USDC.)
2326    ///
2327    /// Deliberately the pool level ONLY, which is what makes it the right check for a
2328    /// stream already in flight. The per-signer gates are ADMISSION controls; an
2329    /// admitted stream's reservation is already counted in the pool total, so leaving
2330    /// them out of the mid-stream re-check bounds nothing less.
2331    /// [`Self::try_reserve_floor`] is where every gate applies.
2332    pub(super) fn pool_budget_covers_reserve(
2333        &self,
2334        pool_id: B256,
2335        remaining: U256,
2336        new_reserve: U256,
2337    ) -> bool {
2338        let committed = {
2339            let guard = self
2340                .pool_floor
2341                .lock()
2342                .unwrap_or_else(std::sync::PoisonError::into_inner);
2343            guard
2344                .get(&pool_id)
2345                .map_or(U256::ZERO, PoolFloorState::live_committed)
2346        };
2347        decdn_incentive::pool_budget_covers(
2348            remaining,
2349            self.pool_min_remaining_deposit,
2350            committed,
2351            new_reserve,
2352        )
2353    }
2354
2355    /// Atomically check both floor gates and reserve one `floor` against the pool's
2356    /// budget, returning the [`FloorReservation`] guard on success or the gate that
2357    /// refused it.
2358    ///
2359    /// Two gates apply, both under ONE `pool_floor` lock hold so two concurrent
2360    /// admissions cannot both pass a check and then both reserve (the TOCTOU
2361    /// over-commit a separate check-then-reserve would allow):
2362    /// 1. **Pool solvency** — `remaining − M` must cover the pool's committed LIVE
2363    ///    reservation plus this floor. The hard money envelope.
2364    /// 2. **Per-signer live cap** — this signer's live reservation plus this floor must
2365    ///    stay within `k · one_window`.
2366    ///
2367    /// Only on success is the row inserted and the reservation charged, so probing a
2368    /// full pool with fresh signer keys cannot grow the accumulator. The guard is built
2369    /// from the already-charged state ([`FloorReservation::new_charged`]) so the
2370    /// reserved amount is charged exactly once. A poisoned lock recovers the guard
2371    /// rather than panicking (best-effort accounting, never a safety gate).
2372    pub(super) fn try_reserve_floor(
2373        &self,
2374        pool_id: B256,
2375        signer: Address,
2376        remaining: U256,
2377        rate_per_mb: u64,
2378        reserved: U256,
2379    ) -> Result<FloorReservation, FloorRefusal> {
2380        let signer_cap = self.signer_floor_cap(rate_per_mb);
2381        let epoch;
2382        {
2383            let mut guard = self
2384                .pool_floor
2385                .lock()
2386                .unwrap_or_else(std::sync::PoisonError::into_inner);
2387            // Read the pool's live total and this signer's live slice, without creating
2388            // a row: a refused admission leaves no new pool or signer row behind, so a
2389            // client probing a full pool with fresh signer keys cannot grow the map.
2390            let (pool_live, signer_live) = match guard.get(&pool_id) {
2391                Some(entry) => {
2392                    let signer_live = entry
2393                        .signers
2394                        .get(&signer)
2395                        .map_or(U256::ZERO, |lane| lane.live_reservation);
2396                    (entry.live_reservation, signer_live)
2397                }
2398                None => (U256::ZERO, U256::ZERO),
2399            };
2400            // Pool ceiling first: it is the solvency bound, and it is what an
2401            // operator reads as "this pool cannot pay".
2402            if !decdn_incentive::pool_budget_covers(
2403                remaining,
2404                self.pool_min_remaining_deposit,
2405                pool_live,
2406                reserved,
2407            ) {
2408                return Err(FloorRefusal::PoolExhausted);
2409            }
2410            // Per-signer live concurrency cap: one signer cannot hold more than `k`
2411            // windows of live un-vouchered reservation at once.
2412            if signer_live.saturating_add(reserved) > signer_cap {
2413                return Err(FloorRefusal::SignerAtCap { signer_cap });
2414            }
2415            let entry = guard.entry(pool_id).or_default();
2416            entry.live_reservation = entry.live_reservation.saturating_add(reserved);
2417            let lane = entry.signers.entry(signer).or_default();
2418            lane.live_reservation = lane.live_reservation.saturating_add(reserved);
2419            epoch = entry.epoch;
2420        }
2421        Ok(FloorReservation::new_charged(
2422            Arc::clone(&self.pool_floor),
2423            pool_id,
2424            signer,
2425            reserved,
2426            epoch,
2427        ))
2428    }
2429
2430    /// Drop a reclaimed pool's floor accounting: remove its in-memory
2431    /// `PoolFloorState`, which carries every signer's live slice with it. Called once
2432    /// when a pool is reclaimed on-chain; a reclaimed `pool_id` never recurs (monotonic
2433    /// open nonce), so the entry is permanently moot.
2434    pub(crate) fn forget_pool_floor(&self, pool_id: B256) {
2435        let mut guard = self
2436            .pool_floor
2437            .lock()
2438            .unwrap_or_else(std::sync::PoisonError::into_inner);
2439        guard.remove(&pool_id);
2440    }
2441
2442    /// Has a takedown landed on this stream since it opened (ADR 011 §On
2443    /// Blacklist Event: "In-flight streams for a blacklisted hash are terminated
2444    /// at the next MB boundary")?
2445    ///
2446    /// The open-time gates in `dispatch.rs` are not enough on their own: a
2447    /// multi-GB blob can still be streaming minutes after a one-hour removal
2448    /// order took effect, and serving past the compliance window is slashable
2449    /// (ADR 026 §Slashing and burn). Both halves are re-checked because both can
2450    /// land mid-stream — a hash via the local denylist reload, the governance
2451    /// blacklist, or an eviction; a funder via either origin list.
2452    ///
2453    /// Cheap enough for a per-MB call: three atomic loads and a hash-set probe
2454    /// each, against a boundary that already takes a channel lock and a network
2455    /// round trip to collect a voucher.
2456    pub(super) fn takedown_landed(&self, hash: Hash, funder: Option<Address>) -> bool {
2457        self.cache.refuses(hash)
2458            || funder.is_some_and(|addr| self.content_deny.is_origin_denied(&addr))
2459    }
2460
2461    /// Cut off an in-flight delivery whose hash or funder was taken down
2462    /// mid-stream, by resetting both directions.
2463    ///
2464    /// A reset, not a `StreamError` frame: ADR 005's stream-error domain split
2465    /// makes `VoucherRejected` the only code that travels mid-stream, and the
2466    /// client's cue here is the absence of the `StreamEnd` sentinel — the same
2467    /// convention every other mid-stream fault uses. The QUIC code is
2468    /// [`APP_ERR_NO_ERROR`] because this is a compliance action, not a protocol
2469    /// fault by either party, and ADR 013 §Application Error Codes reserves
2470    /// `0x03` for peers that misbehaved; coding it as a fault would have the
2471    /// client penalise this node's reputation for discharging a takedown. A
2472    /// client that re-requests the hash gets the signed `Declined`
2473    /// refusal from the open-time gate, which is where the reason belongs. The
2474    /// caller returns `Stopped { Takedown }`, which the dispatch sink counts on
2475    /// `decdn_serve_stream_terminated_takedown_total`.
2476    pub(super) fn terminate_for_takedown(send: &mut SendStream, recv: &mut RecvStream, hash: Hash) {
2477        tracing::warn!(
2478            %hash,
2479            "terminating an in-flight delivery: a takedown landed after the stream opened"
2480        );
2481        reset_stream(send, recv, APP_ERR_NO_ERROR);
2482    }
2483
2484    /// Apply one lane-lifecycle `delta` to the live-lane count and publish the
2485    /// resulting `decdn_lanes_open` gauge.
2486    ///
2487    /// The count comes from the atomic RMW's own result rather than a re-read,
2488    /// and [`Self::lane_gauge_publish`] orders the publish, so the gauge always
2489    /// reflects the most recent lifecycle event. The pool deposit behind these
2490    /// lanes is a pool-level on-chain quantity no lane carries, so it is
2491    /// published by the redeemer tick instead (#2072).
2492    fn tune_lane_gauge(&self, delta: isize) {
2493        let _publish = self
2494            .lane_gauge_publish
2495            .lock()
2496            .unwrap_or_else(std::sync::PoisonError::into_inner);
2497        let open = if delta >= 0 {
2498            self.lane_count
2499                .fetch_add(1, Ordering::Relaxed)
2500                .saturating_add(1)
2501        } else {
2502            self.lane_count
2503                .fetch_sub(1, Ordering::Relaxed)
2504                .saturating_sub(1)
2505        };
2506        self.metrics.set_lanes_open(open);
2507    }
2508}
2509
2510/// Terminal disposition of one voucher collected by
2511/// [`ClientHandler::commit_one_proof`].
2512enum VoucherStop {
2513    /// Verified and advanced in memory; keep serving. `credited_bytes` is the
2514    /// watermark-capped wire bytes to advance the serve loop's `paid` by (rule
2515    /// #1) — at most the amount the lane watermark advanced, so a benign
2516    /// already-satisfied voucher contributes zero.
2517    Continue { credited_bytes: u64 },
2518    /// Rejected — the reject frame was written and the stream finishes cleanly;
2519    /// the loop returns `Ok(())`.
2520    Rejected,
2521}
2522
2523/// Router-facing `ProtocolHandler` for `cdn/client/v1`.
2524///
2525/// Wraps the shared `Arc<ClientHandler>` so the serve loop can hand every
2526/// per-stream task a `'static` clone of the handler and `tokio::spawn` it
2527/// (#1788). The iroh [`ProtocolHandler::accept`] signature borrows `&self`, so
2528/// the handler itself cannot spawn tasks that outlive the borrow; owning the
2529/// `Arc` here and cloning it into `ClientHandler::serve` bridges that gap. The
2530/// wrapper is cheap to clone (one `Arc` bump) and the router holds one for the
2531/// process lifetime.
2532#[derive(Clone, Debug)]
2533pub struct ClientProtocol(Arc<ClientHandler>);
2534
2535impl ClientProtocol {
2536    /// The `cdn/client/v1` ALPN this handler answers.
2537    pub const ALPN: &'static [u8] = ALPN_CLIENT;
2538
2539    /// Wrap a shared handler for registration on the iroh `Router`.
2540    #[must_use]
2541    pub const fn new(handler: Arc<ClientHandler>) -> Self {
2542        Self(handler)
2543    }
2544}
2545
2546impl ProtocolHandler for ClientProtocol {
2547    async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
2548        Arc::clone(&self.0)
2549            .serve(connection)
2550            .await
2551            .map_err(|e| AcceptError::from_boxed(e.into()))
2552    }
2553}
2554
2555/// Error from the request read path carrying the ADR 013 app error code to
2556/// propagate to the peer.
2557struct StreamReadError {
2558    err: anyhow::Error,
2559    app_code: u32,
2560}
2561
2562const fn frame_err_code(e: &FrameError) -> u32 {
2563    match e {
2564        FrameError::TooLarge(_) => 0x02,
2565        FrameError::Io(_) => APP_ERR_NO_ERROR,
2566        FrameError::Varint | FrameError::Decode(_) => APP_ERR_MALFORMED_MESSAGE,
2567    }
2568}
2569
2570fn reset_stream(send: &mut SendStream, recv: &mut RecvStream, code: u32) {
2571    let v = VarInt::from_u32(code);
2572    let _ = send.reset(v);
2573    let _ = recv.stop(v);
2574}
2575
2576/// After a clean completion (`StreamEnd` written, send half finished), read the
2577/// client's send half to its FIN — bounded by [`POST_END_DRAIN_TIMEOUT`] —
2578/// discarding whatever is left, then return so `recv` drops cleanly.
2579///
2580/// Dropping an un-finished [`RecvStream`] issues an implicit `STOP_SENDING(0)` to
2581/// the peer. `StreamEnd` is written only once every interval, the closing voucher
2582/// included, is credited, but the client finishes its send half after that. Without
2583/// this drain the node's drop can stop the client's send before its FIN — surfacing
2584/// to the client as an opaque write failure at the very end of an otherwise
2585/// complete, fully-paid fetch. Draining to FIN first lets both halves close cleanly.
2586/// The bound keeps a client that finishes delivery but never FINs from pinning the
2587/// serve task here.
2588pub(super) async fn drain_recv_to_fin(recv: &mut RecvStream) {
2589    // Read to FIN and discard. Every proof is already read, so only the FIN is
2590    // expected; the cap leaves room for a few stray frames. A client that keeps
2591    // sending past it makes
2592    // `read_to_end` error, which — like the timeout — just ends the drain and lets
2593    // `recv` drop. Buffering onto the heap keeps this future small (no large stack
2594    // scratch array to inflate the serve future — `clippy::large_futures`).
2595    let _ = tokio::time::timeout(POST_END_DRAIN_TIMEOUT, recv.read_to_end(64 * 1024)).await;
2596}
2597
2598/// The first message on a fresh `cdn/client/v1` stream: a paid delivery
2599/// request. It opens a bidirectional stream and leads with one
2600/// [`ClientMessage::StreamRequest`].
2601enum FirstMessage {
2602    /// A paid delivery: [`StreamRequest`] plus its [`StreamRequestExt`].
2603    Delivery(StreamRequest, StreamRequestExt),
2604}
2605
2606/// Read the first framed [`ClientMessage`] on a stream with a timeout. A
2607/// [`ClientMessage::StreamRequest`] yields [`FirstMessage::Delivery`] (with its
2608/// [`StreamRequestExt`] parsed from the trailing bytes — the ADR 005 two-phase
2609/// pattern; an absent extension yields `StreamRequestExt::default()`). Any other
2610/// variant is a protocol fault.
2611async fn read_first_message(recv: &mut RecvStream) -> Result<FirstMessage, StreamReadError> {
2612    let frame = match tokio::time::timeout(REQUEST_READ_TIMEOUT, read_frame(recv)).await {
2613        Err(_) => {
2614            return Err(StreamReadError {
2615                err: anyhow::anyhow!("stream request timed out after {REQUEST_READ_TIMEOUT:?}"),
2616                app_code: APP_ERR_NO_ERROR,
2617            });
2618        }
2619        Ok(Err(e)) => {
2620            let app_code = frame_err_code(&e);
2621            return Err(StreamReadError {
2622                err: anyhow::anyhow!("stream request frame read failed: {e}"),
2623                app_code,
2624            });
2625        }
2626        Ok(Ok(frame)) => frame,
2627    };
2628    match decode_message::<ClientMessage>(&frame) {
2629        Ok((ClientMessage::StreamRequest(req), remainder)) => {
2630            let ext = decdn_protocol::parse_stream_request_ext(remainder).map_err(|e| {
2631                StreamReadError {
2632                    err: anyhow::anyhow!("stream request ext decode failed: {e}"),
2633                    app_code: APP_ERR_MALFORMED_MESSAGE,
2634                }
2635            })?;
2636            // Value checks are kept out of the parse so forward-compatible
2637            // trailing bytes don't couple to them (ADR 005 two-phase). Gate here
2638            // rather than at each use: a malformed client binding or capability
2639            // signature is a protocol error, and this wire boundary is its only
2640            // enforcement point.
2641            ext.validate().map_err(|e| StreamReadError {
2642                err: anyhow::anyhow!("stream request ext rejected: {e}"),
2643                app_code: APP_ERR_MALFORMED_MESSAGE,
2644            })?;
2645            Ok(FirstMessage::Delivery(req, ext))
2646        }
2647        Ok((_, _)) => Err(StreamReadError {
2648            err: anyhow::anyhow!("expected StreamRequest"),
2649            app_code: APP_ERR_UNSUPPORTED_MESSAGE,
2650        }),
2651        Err(e) => {
2652            // ADR 013: an unknown enum discriminant closes with
2653            // UNSUPPORTED_MESSAGE (0x01), not MALFORMED_MESSAGE (0x03). A
2654            // genuine parse fault (in-range discriminant, bad payload) stays
2655            // MALFORMED.
2656            let app_code = if is_unknown_variant::<ClientMessage>(&frame) {
2657                APP_ERR_UNSUPPORTED_MESSAGE
2658            } else {
2659                APP_ERR_MALFORMED_MESSAGE
2660            };
2661            Err(StreamReadError {
2662                err: anyhow::anyhow!("stream request decode failed: {e}"),
2663                app_code,
2664            })
2665        }
2666    }
2667}
2668
2669/// How many proofs the recoup phase will read for ONE outstanding chunk before
2670/// it gives up.
2671///
2672/// The budget counts every proof that leaves the chunk unsettled. That includes
2673/// a proof that credits nothing and a proof that pays only part of the chunk.
2674/// A proof pays part of a chunk when the lane headroom it finds is short — for
2675/// example, a sealed voucher that moves the watermark by less than the chunk. The
2676/// remainder stays owed, and the next proof answers it.
2677///
2678/// A whole chunk is paid by exactly one reveal, but the payer may legitimately
2679/// send metering vouchers ahead of it — the epoch's root voucher when this
2680/// stream has not carried it yet, and a rollover voucher when the chain is
2681/// spent. A metering voucher never credits a whole chunk, even when its
2682/// rollover advances the lane's watermark, so the outstanding chunk stays
2683/// outstanding until its reveal lands (see `voucher_credit_delta`). Three
2684/// messages is the real worst case for a well-behaved payer: two zero-credit
2685/// vouchers (re-anchor, roll) and then the reveal. The rest of the budget of 8
2686/// is slack for a duplicate or out-of-order reveal, which is ordinary once
2687/// several streams share one lane and which credits nothing when the lane holds
2688/// no headroom for it.
2689///
2690/// The bound matters because without it a payer could hold a stream open
2691/// indefinitely with a run of zero-credit vouchers, each one refreshing the
2692/// no-progress timeout while the delivered-but-unpaid balance never moves — the
2693/// same shape of stall the non-empty-`ChunkData` floor closes on the delivery
2694/// side. This budget bounds the number of proofs for one owed chunk, not the
2695/// time. [`proof_wait::ChunkProofs`] counts them, and it also holds the chunk's
2696/// time bound: every proof wait for the chunk ends at
2697/// [`proof_wait::PROOF_WAIT_CEILING`] after the chunk's first wait. So one owed
2698/// chunk holds the stream for at most that long, however many proofs it takes.
2699/// A stream that owes several chunks waits up to that long for each of them.
2700const MAX_PROOFS_PER_CHUNK: u32 = 8;
2701
2702/// One payment proof off the wire: a signed voucher, or a released hash-chain
2703/// preimage (ADR 003 §Two payment resolutions).
2704///
2705/// The two resolve different things and cost differently, which is why the
2706/// protocol keeps both rather than making either do the other's work. A
2707/// **voucher** settles any residual exactly, down to one token base unit, and
2708/// costs one signature. A **preimage** settles whole chunks past the anchor and
2709/// costs one keccak — no signature, no acknowledgement, nothing to persist
2710/// before the node sends the next chunk.
2711#[derive(Debug, Clone, PartialEq, Eq)]
2712pub(super) enum Proof {
2713    Voucher(decdn_protocol::client::Voucher),
2714    Preimage(decdn_protocol::client::ChunkPreimage),
2715}
2716
2717/// Cancellation-safe, buffered reader for `cdn/client/v1` payment-proof frames. Owns
2718/// a byte buffer that PERSISTS across [`Self::read`] calls, so a `read` future
2719/// dropped mid-frame loses no bytes: any partial frame stays buffered for the
2720/// next call.
2721///
2722/// [`read_frame`] is built on `read_exact` and is NOT cancellation-safe — a
2723/// `timeout` firing mid-frame would drop already-consumed bytes and desync the
2724/// stream. Reading instead via the cancel-safe [`tokio::io::AsyncReadExt::read`]
2725/// into an owned buffer, then splitting whole frames off it with
2726/// [`decdn_protocol::framing::parse_frame`], keeps every byte. Borrows the
2727/// `RecvStream` per call so the caller retains it for stream teardown.
2728///
2729/// All proof reads on a given stream MUST go through ONE instance: it may read
2730/// ahead (buffering the next pipelined proof, #1486) while the current one is
2731/// verified and recorded, and a second reader on the same `RecvStream` would
2732/// lose those buffered bytes.
2733#[derive(Default)]
2734pub(super) struct BufferedProofReader {
2735    /// Unconsumed bytes read from the stream, at a frame boundary or partway
2736    /// into the next frame's header/body.
2737    buf: Vec<u8>,
2738}
2739
2740impl BufferedProofReader {
2741    /// Read one framed payment proof — a [`ClientMessage::Voucher`] or a
2742    /// [`ClientMessage::ChunkPreimage`] — filling the buffer incrementally.
2743    /// **Cancellation-safe:** if the returned future is dropped (the proof wait
2744    /// faulted, or the stream is torn down), bytes already read stay in
2745    /// `self.buf` for the next call — no frame is torn.
2746    ///
2747    /// These two variants are the entire payer→node vocabulary after the
2748    /// opening `StreamRequest`, so this is the one place every proof passes
2749    /// through. Anything else on this stream is a protocol error.
2750    ///
2751    /// # Errors
2752    ///
2753    /// Everything the peer controls — a reset stream, a hang-up mid-frame, a
2754    /// malformed frame, an undecodable or unexpected message — carries
2755    /// [`PeerFault`](wire::PeerFault), so the dispatch sink logs it at `debug!`.
2756    /// The two bounds checks do not: those are this node's own invariants.
2757    pub(super) async fn read(&mut self, recv: &mut RecvStream) -> anyhow::Result<Proof> {
2758        loop {
2759            if let Some(result) = self.take_buffered()? {
2760                return result;
2761            }
2762            // Need more bytes. Use tokio's `AsyncReadExt::read` (explicitly, since
2763            // iroh's inherent Quinn `read` shadows it) — it is documented
2764            // cancel-safe: a dropped future consumes nothing, and on `Ready(n)` we
2765            // append to `self.buf` before the next await, so no bytes are ever lost
2766            // when the caller drops the read. `0` is EOF.
2767            let mut scratch = [0u8; 4096];
2768            let n = AsyncReadExt::read(recv, &mut scratch).await.map_err(|e| {
2769                anyhow::Error::new(wire::PeerFault)
2770                    .context(format!("proof stream read failed: {e}"))
2771            })?;
2772            if n == 0 {
2773                // The dominant abandon shape: the client finished its send side
2774                // and walked away between intervals.
2775                return Err(
2776                    anyhow::Error::new(wire::PeerFault).context("proof stream closed mid-frame")
2777                );
2778            }
2779            let chunk = scratch
2780                .get(..n)
2781                .ok_or_else(|| anyhow::anyhow!("short read length out of range"))?;
2782            self.buf.extend_from_slice(chunk);
2783        }
2784    }
2785
2786    /// Take the next whole frame out of the buffer, if one is there.
2787    ///
2788    /// `Ok(None)` means the buffer holds less than a frame and the caller must
2789    /// read more. The inner `Result` is the frame's own outcome: its bytes are
2790    /// consumed either way, so one bad frame cannot wedge the buffer.
2791    ///
2792    /// A malformed length prefix, an undecodable body, and a message that is
2793    /// neither a `Voucher` nor a `ChunkPreimage` are all the peer's doing and
2794    /// carry [`PeerFault`](wire::PeerFault). The bounds check between them is this
2795    /// node's own invariant and carries no marker.
2796    fn take_buffered(&mut self) -> anyhow::Result<Option<anyhow::Result<Proof>>> {
2797        let Some((header_len, payload_len)) = decdn_protocol::framing::parse_frame(&self.buf)
2798            .map_err(|e| {
2799                anyhow::Error::new(wire::PeerFault)
2800                    .context(format!("proof frame parse failed: {e}"))
2801            })?
2802        else {
2803            return Ok(None);
2804        };
2805        let total = header_len.saturating_add(payload_len);
2806        let payload = self
2807            .buf
2808            .get(header_len..total)
2809            .ok_or_else(|| anyhow::anyhow!("proof frame bounds out of range"))?;
2810        let decoded = decode_message::<ClientMessage>(payload);
2811        let result =
2812            match decoded {
2813                Ok((ClientMessage::Voucher(v), _)) => Ok(Proof::Voucher(v)),
2814                Ok((ClientMessage::ChunkPreimage(p), _)) => Ok(Proof::Preimage(p)),
2815                Ok((_, _)) => Err(anyhow::Error::new(wire::PeerFault)
2816                    .context("expected ClientMessage::Voucher or ClientMessage::ChunkPreimage")),
2817                Err(e) => Err(anyhow::Error::new(wire::PeerFault)
2818                    .context(format!("proof decode failed: {e}"))),
2819            };
2820        self.buf.drain(..total);
2821        Ok(Some(result))
2822    }
2823}
2824
2825/// A load-shed controller that never sheds, for handler-layer tests that are
2826/// not exercising the load-shed gate itself. Keeps every existing serve test
2827/// admitting exactly as it did before the gate was wired in.
2828#[cfg(test)]
2829fn always_admit_shed() -> Arc<crate::load_shed::LoadShedController> {
2830    crate::load_shed::LoadShedController::from_config(&decdn_common::config::ResolvedLoadShed {
2831        policy: decdn_common::config::LoadShedPolicyKind::AlwaysAdmit,
2832        ..Default::default()
2833    })
2834}
2835
2836/// Build a `ClientHandler` over an arbitrary [`PoolStateStore`] for the
2837/// sibling-module tests (e.g. `voucher.rs`'s #527 durability tests need a
2838/// fault-injecting store). Kept at module level (not inside `mod tests`) so a
2839/// child module's `#[cfg(test)]` can reach it as `super::handler_over_store`.
2840#[cfg(test)]
2841#[allow(clippy::expect_used)]
2842pub(super) async fn handler_over_store(
2843    metrics: &Arc<Metrics>,
2844    store: Arc<dyn PoolStateStore>,
2845) -> (Arc<ClientHandler>, tempfile::TempDir) {
2846    let dir = tempfile::tempdir().expect("tempdir");
2847    let cache = CacheEngine::open(dir.path(), Vec::new(), 16)
2848        .await
2849        .expect("cache");
2850    let domain = alloy::sol_types::eip712_domain! { name: "t", version: "1", };
2851    let deps = ClientHandlerDeps::new(
2852        iroh::SecretKey::generate().public(),
2853        Arc::clone(metrics),
2854        Arc::new(ConnectionLimiter::new(
2855            &decdn_common::config::ResolvedSecurity {
2856                max_concurrent_handlers: u32::MAX,
2857                per_source_rate_per_sec: 1e9,
2858                per_source_burst: u32::MAX,
2859                max_tracked_sources: 16,
2860            },
2861            Arc::clone(metrics),
2862        )),
2863        cache,
2864        Arc::new(alloy::signers::local::PrivateKeySigner::random()),
2865        domain.clone(),
2866        domain.clone(),
2867        domain,
2868        store,
2869        Arc::new(crate::receipt_log::DirectReceiptSink::new(Arc::new(
2870            crate::receipt_log::NoopReceiptLog,
2871        ))) as Arc<dyn ReceiptSink>,
2872        1,
2873        16,
2874        Arc::new(crate::content_deny::ContentDenylist::empty()),
2875        U256::ZERO,
2876        always_admit_shed(),
2877    );
2878    let handler = ClientHandler::new(deps).expect("handler");
2879    (Arc::new(handler), dir)
2880}
2881
2882#[cfg(test)]
2883#[allow(clippy::expect_used)]
2884mod tests;