Skip to main content

decdn_protocol/
client.rs

1//! Wire message payload types for the `cdn/client/v1` paid-delivery protocol.
2//!
3//! `cdn/client/v1` is the paid byte-transfer path (ADR 005 §`cdn/client/v1`):
4//! a payer opens a bidirectional QUIC stream, sends a [`StreamRequest`], and
5//! the delivering node answers with a [`StreamResponse`] followed by a loop of
6//! [`ChunkData`] interleaved with cumulative payment [`Voucher`]s, terminating
7//! in [`ClientMessage::StreamEnd`]. Acceptance of a voucher is implicit —
8//! delivery simply continues; only rejection is signalled, via
9//! [`ClientMessage::StreamError`]. A delivery or payment fault is signalled by
10//! [`StreamError`].
11//!
12//! Like [`crate::message`] this is a leaf crate with **no crypto dependency**.
13//! The two signed artifacts on this protocol are produced/verified by
14//! `decdn_incentive`:
15//!   - `StreamResponse.slash_sig` — an EIP-712 secp256k1 signature over the
16//!     signed body fields `{hash, ok, rate_per_mb, total_bytes, pool_id,
17//!     timestamp_us}` (ADR 014 §1), produced by the stream-response slash
18//!     signer in `decdn_incentive` (analogous to its `ProbeSlashData`).
19//!     `error` is unsigned.
20//!   - `Voucher.signature` — an EIP-712 secp256k1 voucher signature; the wire
21//!     carries `{signature, amount}` and the receiver reconstructs the full typed
22//!     data `{poolId, signer, provider, amount, bytesDelivered}` from stream
23//!     context (ADR 005 §Voucher wire format, `decdn_incentive::Voucher`).
24//!
25//! # Signed-field freezing (ADR 013 §Signed Field Freezing)
26//!
27//! The signed set on [`StreamResponseBody`] is the `cdn/client/v1` v1 baseline.
28//! Any change to it is a Tier-3 ALPN bump. New unsigned fields go on
29//! [`StreamResponse`] as optional fields, following the `total_bytes` exemplar
30//! on [`crate::message::ProbeResponse`].
31//!
32//! # Variant order
33//!
34//! [`ClientMessage`], [`StreamError`], and [`VoucherRejectReason`] all have
35//! **frozen** variant order — postcard encodes each variant as its
36//! declaration-order index. Reordering is a wire-breaking change; the
37//! `*_discriminant_is_*` / `*_variant_order` tests lock the assignments.
38
39use serde::{Deserialize, Serialize};
40
41use crate::message::{MAX_RATE_PER_MB, MessageValidationError, SLASH_SIG_LEN};
42
43/// One megabyte in bytes (ADR 003: 1 MB = 1,048,576 bytes, exactly). The unit
44/// of `rate_per_mb` and, by the identity below, of [`CHUNK_BYTES`].
45pub const MB_BYTES: u64 = 1_048_576;
46
47/// The payment quantum: one chunk of delivery, 1 MiB (ADR 003 §Chunk Cadence).
48///
49/// A protocol constant, never negotiated. No message carries it, no node
50/// advertises it, and governance does not move it — a chunk is the unit one
51/// hash-chain tick pays for, so payer and node disagreeing on it would make one
52/// released preimage worth two different amounts.
53///
54/// `CHUNK_BYTES == MB_BYTES` by identity, which is what makes a chunk cost
55/// exactly the advertised `rate_per_mb` with no rounding at any rate.
56pub const CHUNK_BYTES: u64 = MB_BYTES;
57
58/// The highest chain index: the hash chain's incremental range over its anchor
59/// (ADR 003 §Chain length and rollover).
60///
61/// The index space is the `u8` domain `0..=255` — 256 slots, exactly one byte.
62/// Index 0 names `chain_root` and resolves to the voucher's own `amount`, so
63/// the base voucher is payable with no chain at all; it adds no increment,
64/// which is why it never travels the wire. Indices `1..=255` each release one
65/// preimage and add one `chunk_price` over that anchor, so a chain adds up to
66/// 255 MiB at [`CHUNK_BYTES`]. 256 slots = one payable base + 255 increments.
67pub const MAX_CHAIN_LENGTH: u8 = 255;
68
69/// Frame ceiling for every `cdn/client/v1` message except
70/// [`ClientMessage::ChunkData`], in payload bytes (ADR 013 § Wire Framing).
71///
72/// The node reads a [`StreamRequest`] and every later payment proof against it,
73/// and a payer reads the [`StreamResponse`] against it. The largest legitimate
74/// message is a [`StreamRequest`] with a client binding and a capability: 301
75/// bytes, since every signature on the wire is a 65-byte EOA signature. The
76/// ceiling leaves headroom for Tier-1 extension fields and refuses a larger
77/// frame before the reader buffers it. It is also the wire bound on
78/// `owner_signature`, which decode checks only for being non-empty. The
79/// `client_control_frame_ceiling_holds_the_largest_message` test pins the margin.
80pub const MAX_CLIENT_CONTROL_FRAME: u32 = 4 * 1024;
81
82/// Frame ceiling for the node → payer delivery stream, in payload bytes
83/// (ADR 013 § Wire Framing): one [`ClientMessage::ChunkData`] that carries a
84/// full [`CHUNK_BYTES`] payload — 1,048,580 bytes.
85///
86/// ADR 005 §`cdn/client/v1` forbids a sender to cross a [`CHUNK_BYTES`] payment
87/// boundary inside one frame, so no legitimate `ChunkData` carries more. The
88/// payer reads every post-response frame (`ChunkData`, `StreamEnd`,
89/// `StreamError`) against this ceiling, and every `encode_chunk_*` helper
90/// refuses a frame above it.
91#[expect(
92    clippy::cast_possible_truncation,
93    reason = "CHUNK_BYTES is 1 MiB, so its frame (1 MiB + 4) is far inside u32"
94)]
95pub const MAX_CHUNK_FRAME: u32 = chunk_frame_postcard_len(CHUNK_BYTES as usize) as u32;
96
97/// Exact byte length of an EOA secp256k1 voucher signature (`r‖s‖v`, 32+32+1).
98/// Mirrors [`SLASH_SIG_LEN`]; both are the EOA off-chain signing form (ADR 024
99/// §Off-Chain Signature Verification — EOA Recovery Only). Carried as a `Vec<u8>` on the wire (serde
100/// derives array impls only up to `[T; 32]`), with the length pinned by
101/// [`Voucher::validate`].
102pub const VOUCHER_SIG_LEN: usize = 65;
103
104/// Exact byte length of a `BindNodeId` client-binding signature (`r‖s‖v`,
105/// 32+32+1). The same EOA off-chain EIP-712 signing form as [`SLASH_SIG_LEN`] /
106/// [`VOUCHER_SIG_LEN`] (ADR 024 §Off-Chain Signature Verification — EOA Recovery Only); pinned by
107/// [`ClientBinding::validate`].
108pub const BINDING_SIG_LEN: usize = 65;
109
110/// Top-level protocol enum for `cdn/client/v1`. Variant order is frozen per
111/// ADR 013 — new variants MUST be appended at the end.
112///
113/// ⚠️ **VARIANT ORDER FROZEN — ADR 013 §Protocol Enums**
114/// Postcard encodes each variant as its declaration-order index. Reordering,
115/// inserting, or removing a variant is a wire-breaking change requiring an ALPN
116/// version bump (`cdn/client/v2`). The discriminant assignments are locked by
117/// the `client_message_*_discriminant_is_*` tests.
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119pub enum ClientMessage {
120    /// discriminant 0 — payer → node, opens a delivery.
121    StreamRequest(StreamRequest),
122    /// discriminant 1 — node → payer, answers a [`StreamRequest`].
123    StreamResponse(StreamResponse),
124    /// discriminant 2 — node → payer, one sequential blob chunk.
125    ChunkData(ChunkData),
126    /// discriminant 3 — payer → node, cumulative payment voucher. Acceptance
127    /// is implicit — delivery simply continues; only rejection is signalled,
128    /// via [`Self::StreamError`].
129    Voucher(Voucher),
130    /// discriminant 4 — payer → node, one released hash-chain preimage,
131    /// advancing the lane's claim by one chunk without a signature (ADR 003
132    /// §Hash-chain metering (`PayWord`)). Acceptance is implicit, exactly as for
133    /// [`Self::Voucher`].
134    ChunkPreimage(ChunkPreimage),
135    /// discriminant 5 — node → payer: the node delivered the whole requested
136    /// range, holds payment for every byte of it, and ends the stream.
137    StreamEnd,
138    /// discriminant 6 — node → payer, mid-stream failure (carries
139    /// [`StreamError::VoucherRejected`]); delivery-side errors instead ride in
140    /// [`StreamResponseExt::error`].
141    StreamError(StreamError),
142}
143
144impl crate::framing::TopLevelEnum for ClientMessage {
145    /// `StreamRequest` (0) … `StreamError` (6). Pinned by
146    /// `client_message_variant_count_matches_discriminants`.
147    const VARIANT_COUNT: u32 = 7;
148}
149
150impl ClientMessage {
151    /// Validate the payload's requester-side invariants, dispatching to the
152    /// per-variant `validate()`. This is the single decode-then-validate seam a
153    /// receive-path handler should call after [`crate::decode_message`] so the
154    /// zero-rate / signature-length / `ok`-`error` checks can't be silently
155    /// skipped (they are not enforced at decode time — see
156    /// [`StreamResponse::validate`]).
157    ///
158    /// The `ok`/`error` agreement is NOT among them: `error` lives in the trailing
159    /// [`StreamResponseExt`], which this method cannot see. Neither extension is
160    /// reachable from here — validate [`StreamRequestExt`] via its own `validate`
161    /// after [`parse_stream_request_ext`], and [`StreamResponseExt`] via
162    /// [`StreamResponseExt::validate`] with `body.ok` after
163    /// [`parse_stream_response_ext`].
164    /// Variants with no value invariants (`StreamEnd`, `StreamError`) return
165    /// `Ok(())`.
166    ///
167    /// [`ChunkData`] is dispatched here for TOTALITY over the enum, and for nothing more
168    /// (#1145 review). Two facts about this method are easy to misread, and either mistake
169    /// leads the next reader to rely on a check that is not here:
170    ///
171    /// - No receive loop calls this. `read_client_message` decodes and returns; only
172    ///   `StreamResponse::validate` runs on the receive path, explicitly, via
173    ///   `verify_response`. This aggregate is a convenience, not a load-bearing seam.
174    /// - The `ChunkData` arm cannot fail. Its field is private behind `#[serde(try_from)]`,
175    ///   which makes `ChunkData::validate` total — its own doc says so ("always `Ok` for a
176    ///   frame that exists").
177    ///
178    /// The empty-frame floor #1088 needs is enforced by the DECODE GATE, not by this
179    /// dispatch: an empty frame cannot be constructed *or* deserialized, so no receive loop
180    /// has to remember anything. That is what makes the floor structural rather than a
181    /// convention.
182    ///
183    /// # Errors
184    ///
185    /// Propagates the [`MessageValidationError`] from the wrapped payload's
186    /// `validate()`.
187    pub const fn validate(&self) -> Result<(), MessageValidationError> {
188        match self {
189            Self::StreamResponse(resp) => resp.validate(),
190            Self::Voucher(voucher) => voucher.validate(),
191            Self::ChunkData(chunk) => chunk.validate(),
192            Self::ChunkPreimage(preimage) => preimage.validate(),
193            Self::StreamRequest(_) | Self::StreamEnd | Self::StreamError(_) => Ok(()),
194        }
195    }
196}
197
198/// Payer → node request opening a paid delivery (ADR 005 §`cdn/client/v1`).
199///
200/// `timestamp_us` is a requester-generated microsecond timestamp echoed back in
201/// [`StreamResponseBody::timestamp_us`]; it both correlates the response and
202/// drives rate-manipulation slashing (ADR 005, ADR 014). `byte_offset` supports
203/// seek/resume on failover — the requester reconnects to another node and
204/// resumes from the last BLAKE3-verified byte.
205///
206/// This struct holds **only the frozen base fields** (ADR 013 §Tier 1). The
207/// optional [`StreamRequestExt`] is carried as **separate trailing bytes** after
208/// this message in the frame, parsed via [`parse_stream_request_ext`] — *not* as
209/// an embedded field. This is the ADR 005 §Client identity binding two-phase
210/// deserialization pattern: keeping the base frozen and parsing extension bytes
211/// separately means a future field added to [`StreamRequestExt`] stays
212/// backward-compatible (old senders simply emit fewer trailing bytes; new
213/// optional fields are appended and old receivers skip them). Embedding `ext` as
214/// a nested struct field instead would break old `Some(...)` senders the moment
215/// a field is added (postcard would hit EOF inside the nested struct). Use
216/// [`encode_stream_request`] to build the wire payload with an optional `ext`.
217#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
218pub struct StreamRequest {
219    /// BLAKE3 hash of the requested blob (iroh `Hash`, 32 bytes).
220    pub hash: [u8; 32],
221    /// Namespace the content is published under, as a big-endian `uint256`
222    /// (ADR 002 §Retrieval by namespace, ADR 005 §Namespace routing). All-zero
223    /// = no namespace: the node serves best-effort from local cache / DHT only,
224    /// with no authorized origins. A non-zero value routes cache-miss origin
225    /// pulls to that namespace's `OriginAssignment` set. It is a **routing hint,
226    /// not a trust anchor** — returned bytes are verified against `hash`
227    /// independently, so a wrong/hostile value can only fail the fetch, never
228    /// corrupt delivery. The node layer converts this to an alloy `U256`;
229    /// keeping it `[u8; 32]` here (like `hash`/`pool_id`) leaves `protocol`
230    /// alloy-free. Billing-agnostic but load-bearing for routing, so it lives in
231    /// the frozen base — every node reads it for origin routing (a node with no
232    /// chain origin directory configured resolves nothing from it).
233    pub namespace_id: [u8; 32],
234    /// `poolId = keccak256(owner, poolNonce)` (ADR 005).
235    pub pool_id: [u8; 32],
236    /// Resume position in bytes; `0` for a full-blob fetch.
237    pub byte_offset: u64,
238    /// Upper bound on the requested range: the request covers the half-open span
239    /// `[byte_offset, byte_offset + byte_len)`. `0` means "to end-of-blob" (the
240    /// whole-tail default, so an unset value preserves the prior behavior). A
241    /// non-zero `byte_len` lets a node scope a cache-miss origin fetch to exactly
242    /// the requested bytes and meters payment over the range (ADR 005 §Bounded
243    /// byte ranges, ADR 037 §Origin-tier pull-through). Billing-relevant, so it
244    /// lives in the frozen base — every node must understand it.
245    pub byte_len: u64,
246    /// Requester-generated microseconds since the Unix epoch, echoed back.
247    pub timestamp_us: u64,
248}
249
250/// The reserved "no namespace" [`StreamRequest::namespace_id`] value (all-zero
251/// big-endian `uint256`): content published without a namespace, served
252/// best-effort from cache / DHT with no authorized origins (ADR 002 §Namespace 0).
253pub const NO_NAMESPACE: [u8; 32] = [0u8; 32];
254
255/// Optional [`StreamRequest`] extension fields (ADR 005 §Client identity
256/// binding), carried as trailing bytes after the `StreamRequest` message via the
257/// two-phase pattern (see [`encode_stream_request`] / [`parse_stream_request_ext`]).
258#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
259pub struct StreamRequestExt {
260    /// Off-chain client identity binding (address + attesting signature). Grouped
261    /// so a half-populated state (address without signature, or vice versa) is
262    /// unrepresentable; absent ⇒ a registered/on-chain client.
263    pub binding: Option<ClientBinding>,
264    /// The pool owner's spending capability for this stream's `signer`,
265    /// attached at session start so the delivering node can register the
266    /// signer on that signer's first on-chain redemption (ADR 003 §Capability
267    /// delegation). Node-agnostic: the same capability is valid at every node
268    /// the client streams from, since it grants spend against the pool, not
269    /// against a specific delivering node. Absent ⇒ the node already has the
270    /// signer registered (or the client is relying on a capability it sent on
271    /// an earlier stream to a different node this session).
272    pub capability: Option<WireCapability>,
273}
274
275/// Off-chain ephemeral client identity binding (ADR 003 §Off-Chain Ephemeral
276/// Binding), carried inside [`StreamRequestExt`]. The address and its attesting
277/// signature are paired so neither can appear without the other.
278#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
279pub struct ClientBinding {
280    /// Ephemeral client Ethereum address (20 bytes) issuing vouchers for an
281    /// off-chain (unregistered) client.
282    pub ethereum_address: [u8; 20],
283    /// EIP-712 `BindNodeId(nodeId, nonce=0)` signature attesting the
284    /// `NodeId`↔`ethereum_address` mapping for the connection's lifetime. Exactly
285    /// [`BINDING_SIG_LEN`] bytes; verified by `decdn_incentive` via `ecrecover`.
286    pub binding_signature: Vec<u8>,
287}
288
289impl ClientBinding {
290    /// Validate the wire-level `binding_signature` length ([`BINDING_SIG_LEN`]).
291    /// The cryptographic attestation check (`ecrecover` over the `BindNodeId`
292    /// typed data) happens in `decdn_incentive`.
293    ///
294    /// # Errors
295    ///
296    /// [`MessageValidationError::InvalidBindingSigLen`] if the signature is not
297    /// exactly [`BINDING_SIG_LEN`] bytes.
298    pub const fn validate(&self) -> Result<(), MessageValidationError> {
299        if self.binding_signature.len() != BINDING_SIG_LEN {
300            return Err(MessageValidationError::InvalidBindingSigLen {
301                len: self.binding_signature.len(),
302            });
303        }
304        Ok(())
305    }
306}
307
308/// A pool owner's spending capability, carried inside [`StreamRequestExt`]
309/// (ADR 003 §Capability delegation). `signer` and `pool_id` are not on the
310/// wire — they are derived from stream context, exactly like a [`Voucher`]'s
311/// implicit fields: `signer` is the request's bound Ethereum address
312/// ([`ClientBinding::ethereum_address`], or the registered on-chain signer
313/// when `binding` is absent) and `pool_id` is [`StreamRequest::pool_id`].
314#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
315pub struct WireCapability {
316    /// Maximum cumulative amount the signer may spend against the pool under
317    /// this grant. A `u64`, matching the `PaymentPool.spendingCap` on-chain
318    /// storage/calldata width exactly — the node zero-extends it to a `uint256`
319    /// word when it reconstructs the EIP-712 capability digest (just as it does
320    /// [`Voucher::amount`]).
321    pub spending_cap: u64,
322    /// Unix-seconds expiry. After it, vouchers under this capability are no
323    /// longer redeemable.
324    pub expiry: u64,
325    /// EIP-712 owner signature over the capability (`r‖s‖v`, 65 bytes for an
326    /// EOA owner; may be longer for an ERC-1271 contract owner, so unlike
327    /// [`VOUCHER_SIG_LEN`]-pinned signatures this is only checked non-empty at
328    /// the wire boundary — [`WireCapability::validate`]). The cryptographic
329    /// recovery/verification happens in `decdn_incentive`.
330    pub owner_signature: Vec<u8>,
331}
332
333impl WireCapability {
334    /// Validate the wire-level `owner_signature` non-emptiness. Unlike
335    /// [`ClientBinding::validate`] / [`Voucher::validate`] this cannot pin an
336    /// exact length — an ERC-1271 contract signature may be longer than the
337    /// 65-byte EOA form — so this is a floor, not a full shape check.
338    ///
339    /// # Errors
340    ///
341    /// [`MessageValidationError::EmptyCapabilitySignature`] if `owner_signature`
342    /// is empty.
343    pub const fn validate(&self) -> Result<(), MessageValidationError> {
344        if self.owner_signature.is_empty() {
345            return Err(MessageValidationError::EmptyCapabilitySignature);
346        }
347        Ok(())
348    }
349}
350
351impl StreamRequestExt {
352    /// Validate the (if present) client binding and capability. Called by the
353    /// node on the receive path after [`parse_stream_request_ext`]; kept
354    /// separate from parsing so forward-compatible trailing bytes don't couple
355    /// to value checks.
356    ///
357    /// # Errors
358    ///
359    /// [`MessageValidationError::InvalidBindingSigLen`] if a present `binding`
360    /// has a wrong-length signature; [`MessageValidationError::EmptyCapabilitySignature`]
361    /// if a present `capability` has an empty `owner_signature`.
362    pub const fn validate(&self) -> Result<(), MessageValidationError> {
363        if let Some(binding) = &self.binding {
364            // `?` is not yet stable in `const fn`; match-return instead.
365            match binding.validate() {
366                Ok(()) => {}
367                Err(e) => return Err(e),
368            }
369        }
370        if let Some(capability) = &self.capability {
371            match capability.validate() {
372                Ok(()) => {}
373                Err(e) => return Err(e),
374            }
375        }
376        Ok(())
377    }
378}
379
380/// Encode a `cdn/client/v1` `StreamRequest` frame payload with an optional
381/// trailing [`StreamRequestExt`] (ADR 005 §Client identity binding, two-phase).
382///
383/// The result is the full frame payload to pass to [`crate::write_frame`]: the
384/// postcard-encoded `ClientMessage::StreamRequest(req)` followed, when `ext` is
385/// `Some`, by the postcard-encoded extension. The receiver recovers `req` from
386/// [`crate::decode_message`] and the extension from that call's returned
387/// remainder via [`parse_stream_request_ext`].
388///
389/// # Errors
390///
391/// Propagates a [`postcard::Error`] if serialization fails.
392pub fn encode_stream_request(
393    req: &StreamRequest,
394    ext: Option<&StreamRequestExt>,
395) -> Result<Vec<u8>, postcard::Error> {
396    let mut buf = postcard::to_allocvec(&ClientMessage::StreamRequest(req.clone()))?;
397    if let Some(ext) = ext {
398        buf.extend_from_slice(&postcard::to_allocvec(ext)?);
399    }
400    Ok(buf)
401}
402
403/// Parse the trailing [`StreamRequestExt`] bytes returned as the remainder by
404/// [`crate::decode_message`] after a `ClientMessage::StreamRequest`.
405///
406/// An empty remainder ⇒ [`StreamRequestExt::default`] (no client binding, no
407/// capability). Trailing bytes beyond the known fields are tolerated for
408/// forward compatibility (ADR 013 §Tier 1): a future optional field appended to
409/// [`StreamRequestExt`] is read by new receivers and skipped by old ones.
410///
411/// # Errors
412///
413/// Returns a [`postcard::Error`] if a non-empty remainder is not a valid
414/// `StreamRequestExt` prefix.
415pub fn parse_stream_request_ext(remainder: &[u8]) -> Result<StreamRequestExt, postcard::Error> {
416    if remainder.is_empty() {
417        Ok(StreamRequestExt::default())
418    } else {
419        Ok(postcard::take_from_bytes::<StreamRequestExt>(remainder)?.0)
420    }
421}
422
423/// Node → payer response to a [`StreamRequest`] (ADR 005 §`cdn/client/v1`).
424///
425/// **This struct holds only the frozen base** (ADR 013 §Tier 1): the signed
426/// [`StreamResponseBody`] and the `slash_sig` covering it. Unsigned fields live
427/// in [`StreamResponseExt`], which travels as separate trailing bytes — see
428/// [`encode_stream_response`] / [`parse_stream_response_ext`].
429///
430/// `slash_sig` is mandatory and non-empty (exactly [`SLASH_SIG_LEN`] bytes);
431/// requesters MUST reject missing/zero-length or zero-`rate_per_mb` responses
432/// (enforced via [`StreamResponse::validate`]).
433#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
434pub struct StreamResponse {
435    /// Signed body. Its wire layout is frozen per ADR 013.
436    pub body: StreamResponseBody,
437    /// EIP-712 secp256k1 signature over `body`'s signed fields (ADR 014 §1;
438    /// produced by the `decdn_incentive` stream slash signer, analogous to its
439    /// `ProbeSlashData`). Always exactly [`SLASH_SIG_LEN`] bytes — *not* a
440    /// signature over postcard bytes.
441    pub slash_sig: Vec<u8>,
442}
443
444/// Optional [`StreamResponse`] extension fields, carried as trailing bytes after
445/// the `StreamResponse` message via the two-phase pattern (ADR 013 §Tier 1; see
446/// [`encode_stream_response`] / [`parse_stream_response_ext`]).
447///
448/// Nothing here is covered by `slash_sig`. New fields are appended to the END
449/// and MUST be `Option<T>` or have a meaningful `Default`; insertions and
450/// reordering are Tier-3.
451#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
452pub struct StreamResponseExt {
453    /// Open-time refusal class when `body.ok == false` (`NotFound`,
454    /// `Declined`, `Unfunded`).
455    /// Unsigned and informational. `VoucherRejected` never rides here — it is
456    /// delivered mid-stream via [`ClientMessage::StreamError`].
457    ///
458    /// Its agreement with the signed `body.ok` is a cross-half invariant, so it
459    /// is checked by [`StreamResponseExt::validate`], which takes `ok` — the
460    /// base-only [`StreamResponse::validate`] cannot see this field.
461    pub error: Option<StreamError>,
462}
463
464impl StreamResponseExt {
465    /// Check the unsigned half against the signed `ok` flag (ADR 005
466    /// §`cdn/client/v1`): `ok == true` ⇒ no `error`; `ok == false` ⇒ exactly one
467    /// delivery-side `error`, never the mid-stream-only
468    /// [`StreamError::VoucherRejected`].
469    ///
470    /// Separate from parsing so forward-compatible trailing bytes do not couple
471    /// to value checks, and separate from [`StreamResponse::validate`] because
472    /// the rule spans the frozen base and the extension — a receiver holds both
473    /// and must call each.
474    ///
475    /// # Errors
476    ///
477    /// [`MessageValidationError::StreamErrorWithOk`],
478    /// [`MessageValidationError::MissingStreamError`], or
479    /// [`MessageValidationError::VoucherRejectedInResponse`].
480    pub const fn validate(&self, ok: bool) -> Result<(), MessageValidationError> {
481        match (ok, &self.error) {
482            (true, Some(_)) => Err(MessageValidationError::StreamErrorWithOk),
483            (false, None) => Err(MessageValidationError::MissingStreamError),
484            (false, Some(code)) if !code.is_delivery_side() => {
485                Err(MessageValidationError::VoucherRejectedInResponse)
486            }
487            _ => Ok(()),
488        }
489    }
490}
491
492/// Encode a [`StreamResponse`] with its optional trailing [`StreamResponseExt`]
493/// (ADR 013 §Tier 1, two-phase). See [`encode_stream_request`] for why the two
494/// values are encoded separately.
495///
496/// # Errors
497///
498/// Propagates a [`postcard::Error`] if serialization fails.
499pub fn encode_stream_response(
500    resp: &StreamResponse,
501    ext: Option<&StreamResponseExt>,
502) -> Result<Vec<u8>, postcard::Error> {
503    let mut buf = postcard::to_allocvec(&ClientMessage::StreamResponse(resp.clone()))?;
504    if let Some(ext) = ext {
505        buf.extend_from_slice(&postcard::to_allocvec(ext)?);
506    }
507    Ok(buf)
508}
509
510/// Parse the trailing [`StreamResponseExt`] bytes returned as the remainder by
511/// [`crate::decode_message`] after a `ClientMessage::StreamResponse`.
512///
513/// An empty remainder ⇒ [`StreamResponseExt::default`] (no error code, which is
514/// only valid alongside `ok == true`). Trailing bytes beyond the known fields
515/// are tolerated for forward compatibility (ADR 013 §Tier 1).
516///
517/// # Errors
518///
519/// Returns a [`postcard::Error`] if a non-empty remainder is not a valid
520/// `StreamResponseExt` prefix.
521pub fn parse_stream_response_ext(remainder: &[u8]) -> Result<StreamResponseExt, postcard::Error> {
522    if remainder.is_empty() {
523        Ok(StreamResponseExt::default())
524    } else {
525        Ok(postcard::take_from_bytes::<StreamResponseExt>(remainder)?.0)
526    }
527}
528
529/// Signed fields of a [`StreamResponse`] (ADR 014 §1). Layout is frozen per
530/// ADR 013 — future additions go on [`StreamResponse`] as optional unsigned
531/// fields, not here.
532///
533/// `rate_per_mb` is bounded by [`MAX_RATE_PER_MB`] at the wire boundary via the
534/// shared `deserialize_rate_per_mb` hook (#378), keeping the bound bilateral
535/// with the node's config layer.
536#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
537pub struct StreamResponseBody {
538    /// BLAKE3 hash this response answers for, echoed from the request.
539    pub hash: [u8; 32],
540    /// Whether the node will serve the blob. A node MUST NOT sign `true` unless
541    /// it can deliver within the slashing window.
542    pub ok: bool,
543    /// Quoted rate in token base units per MB. Bounded by [`MAX_RATE_PER_MB`].
544    #[serde(deserialize_with = "crate::message::deserialize_rate_per_mb")]
545    pub rate_per_mb: u64,
546    /// Total blob size in bytes, as the signing node claims it. A pulling node
547    /// does not enforce its `max_blob_size` against this claim: it enforces the
548    /// ceiling on the bytes it receives (ADR 005 §`max_blob_size` enforcement).
549    pub total_bytes: u64,
550    /// `pool_id` echoed from the [`StreamRequest`] (signed, so a node cannot
551    /// silently re-bind the response to a different pool).
552    pub pool_id: [u8; 32],
553    /// Requester-generated microsecond timestamp from the [`StreamRequest`],
554    /// echoed back unchanged.
555    pub timestamp_us: u64,
556}
557
558impl StreamResponse {
559    /// Validate requester-side invariants (ADR 005, ADR 014 §1, #252):
560    ///
561    /// - `rate_per_mb` within `(0, MAX_RATE_PER_MB]` (the decode path already
562    ///   enforces the upper bound via `deserialize_rate_per_mb`; this adds the
563    ///   zero-rate rule),
564    /// - `slash_sig` exactly [`SLASH_SIG_LEN`] bytes.
565    ///
566    /// The `ok`/`error` consistency rule is NOT here: `error` lives in
567    /// [`StreamResponseExt`], which this method cannot see. A receiver must also
568    /// call [`StreamResponseExt::validate`] with `body.ok`.
569    ///
570    /// Exposed so requesters re-check on receive and construction sites assert
571    /// validity before signing.
572    pub const fn validate(&self) -> Result<(), MessageValidationError> {
573        if self.body.rate_per_mb > MAX_RATE_PER_MB {
574            return Err(MessageValidationError::RateTooLarge {
575                rate: self.body.rate_per_mb,
576            });
577        }
578        // #252: requesters MUST reject a zero rate on receive — same rule as
579        // ProbeResponse. Shared `RateIsZero` so both paths fail identically.
580        if self.body.rate_per_mb == 0 {
581            return Err(MessageValidationError::RateIsZero);
582        }
583        if self.slash_sig.len() != SLASH_SIG_LEN {
584            return Err(MessageValidationError::InvalidSlashSigLen {
585                len: self.slash_sig.len(),
586            });
587        }
588        Ok(())
589    }
590}
591
592/// Node → payer chunk of blob bytes. The payload carries at least one byte; its
593/// length is the sender's choice. ADR 005 §`cdn/client/v1` requires a sender not
594/// to cross a `CHUNK_BYTES` payment boundary, so a frame runs to at most one
595/// interval, and the payer reads it against [`MAX_CHUNK_FRAME`]. Frames need not be uniform, and the frame before
596/// [`ClientMessage::StreamEnd`] is commonly shorter than the ones before it.
597///
598/// Frame size is independent of both verification and payment. The bao codec is
599/// the sole verifier and resolves chunk-group boundaries itself (ADR 038), and
600/// the payment meter ticks on cumulative bytes crossing [`CHUNK_BYTES`] rather
601/// than on frames. A sender therefore picks whatever size suits it; nothing is
602/// negotiated and no message carries the choice.
603///
604/// The lower bound is load-bearing, not cosmetic (#1088). A shorter final frame is
605/// permitted, never an *empty* one: an empty frame carries no
606/// payload, so it advances neither the receiver's cumulative byte count nor its
607/// voucher accounting. An unbounded run of them therefore drives the receive
608/// loops without making application-level progress, and the `cumulative >
609/// expected_wire` overrun guard — which only ever trips on bytes — never fires.
610/// An empty frame cannot be obtained at all — [`ChunkData::new`], the `try_from` decode
611/// gate, and every `encode_chunk_*` helper ([`encode_chunk_frame`],
612/// [`encode_chunk_frame_headers`], and the crate-private one the latter builds on)
613/// each reject one, and between them they are every route to a frame body. That is the invariant the pull
614/// paths' inactivity deadline rests on: with empty frames banned, "a frame arrived" and
615/// "bytes made progress" are the same statement, so a peer cannot refresh the deadline
616/// with padding.
617///
618/// # The bounds are enforced by construction
619///
620/// The field is private and [`ChunkData::new`] is the only constructor, so an invalid
621/// frame cannot be built — and `#[serde(try_from)]` routes decoding through the same
622/// check, so it cannot be *decoded* either. A receive loop therefore holds a valid frame
623/// by having one at all, and a serve path cannot emit an empty frame even by accident.
624///
625/// The invariant lives on the type, not in a convention, because a convention here is
626/// silently skippable. Were the floor an advisory `validate()`, it would ride on every
627/// receive loop remembering to call it and every emitter avoiding an empty frame — and
628/// the serve side avoids one only *incidentally*, differently on each path:
629///
630/// each serve path cuts its byte stream into frames and emits whatever has
631/// accumulated, so an empty frame is avoided only because an exhausted stream yields
632/// nothing to emit. The empty blob (whose bao encoding is zero bytes — see
633/// `decdn_bao_range::align_range`) reaches [`ClientMessage::StreamEnd`] the same way,
634/// with no frame sent first (#1054).
635///
636/// That is a fact about unrelated code, and it could change without anyone noticing
637/// which invariant they had just removed — while the requester's receive loop depends on it
638/// (#1797): an empty frame advances neither the cumulative delivered bytes nor the voucher
639/// accounting, so a run of them would drive that loop without delivering any content. The
640/// requester's throughput floor counts bytes off the wire, envelopes included, so it is this
641/// floor — not that counter — that keeps wire progress equal to content progress.
642/// That is too much weight for a convention, so the type carries the floor instead: the field
643/// is private, both `ChunkData` doors reject an empty payload, and the
644/// `encode_chunk_*` helpers — which build no `ChunkData`, and among them
645/// [`encode_chunk_frame_headers`] is the door both serve paths use — restate the same
646/// check on `payload_len`. Every route is closed.
647#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
648#[serde(try_from = "ChunkDataWire")]
649pub struct ChunkData {
650    /// Sequential blob bytes, at least one. Private: see the type's docs — this is
651    /// what keeps every frame a unit of delivered content (#1797).
652    bytes: Vec<u8>,
653}
654
655/// Decode shape for [`ChunkData`], which cannot deserialize directly without giving up
656/// its private field and with it the invariant. Structurally identical, so the wire
657/// format is unchanged; it exists only to be validated on the way in.
658#[derive(Deserialize)]
659struct ChunkDataWire {
660    bytes: Vec<u8>,
661}
662
663impl TryFrom<ChunkDataWire> for ChunkData {
664    type Error = MessageValidationError;
665
666    fn try_from(wire: ChunkDataWire) -> Result<Self, Self::Error> {
667        Self::new(wire.bytes)
668    }
669}
670
671impl ChunkData {
672    /// The only constructor. Enforces the payload floor: non-empty.
673    ///
674    /// There is no ceiling here. An oversized frame is refused by the framing
675    /// layer against [`MAX_CHUNK_FRAME`], which is the bound that runs before
676    /// the receiver allocates.
677    ///
678    /// # Errors
679    ///
680    /// [`MessageValidationError::EmptyChunk`] for a zero-length payload.
681    pub fn new(bytes: Vec<u8>) -> Result<Self, MessageValidationError> {
682        if bytes.is_empty() {
683            return Err(MessageValidationError::EmptyChunk);
684        }
685        Ok(Self { bytes })
686    }
687
688    /// The payload. Non-empty by construction.
689    #[must_use]
690    pub fn bytes(&self) -> &[u8] {
691        &self.bytes
692    }
693
694    /// Consume the frame for its payload, avoiding a copy on the receive path.
695    #[must_use]
696    pub fn into_bytes(self) -> Vec<u8> {
697        self.bytes
698    }
699
700    /// Re-check the payload bounds.
701    ///
702    /// Always `Ok` for a frame that exists — [`Self::new`] and the `try_from` decode gate
703    /// are the only ways to obtain one. Retained because [`ClientMessage::validate`]
704    /// dispatches to it, so the aggregate validator stays total over the message enum
705    /// rather than silently skipping this variant.
706    ///
707    /// `pub(crate)`, deliberately: it is total by construction, and a public function that
708    /// cannot fail is a check the API advertises and does not have (#1145 review). Leaving it
709    /// on the committed surface would invite a caller to depend on it and make removing it a
710    /// breaking change — for a validator whose only honest answer is `Ok`. The dispatcher is
711    /// in this crate, so `pub(crate)` costs nothing and says the truth.
712    ///
713    /// # Errors
714    ///
715    /// [`MessageValidationError::EmptyChunk`], which a constructed frame cannot produce.
716    pub(crate) const fn validate(&self) -> Result<(), MessageValidationError> {
717        if self.bytes.is_empty() {
718            return Err(MessageValidationError::EmptyChunk);
719        }
720        Ok(())
721    }
722}
723
724/// The `ClientMessage::ChunkData` discriminant, as postcard writes it: the
725/// declaration index, varint-encoded, which is one byte for indices 0..=127.
726/// Pinned by `client_message_discriminants_are_frozen`.
727const CHUNK_DATA_DISCRIMINANT: u8 = 2;
728
729/// Stack-buffer size for a `ChunkData` header (discriminant + varint payload
730/// length). Sized to the unconditioned `u32` varint width — 1 + 5 — so the buffer
731/// needs no reasoning about [`MAX_CHUNK_FRAME`]. That cap keeps the bytes
732/// actually written to at most 4.
733pub(crate) const CHUNK_DATA_HEADER_MAX: usize = 6;
734
735/// Stack-buffer size for the full framing + `ChunkData` header: varint
736/// `postcard_len` + the one-byte `ChunkData` discriminant + varint `payload_len`.
737/// Sized to
738/// the unconditioned `u32` varint width — 5 + 1 + 5 — for the same reason as the
739/// `ChunkData` header buffer; the cap keeps the bytes actually written to at most 7.
740pub const CHUNK_FRAME_HEADERS_MAX: usize = 11;
741
742/// Encode one [`ClientMessage::ChunkData`] frame body directly from its payload.
743///
744/// Byte-identical to `encode_message(&ClientMessage::ChunkData(ChunkData::new(payload)?))`
745/// — pinned by `encode_chunk_frame_matches_the_generic_encoder` — but it copies the
746/// payload once instead of twice. The generic path owns the bytes to build the frame
747/// (`Vec<u8>`) and postcard copies them again into its output buffer.
748///
749/// This is the single-buffer door, for callers that need the whole frame as one
750/// `Vec<u8>`. The serve paths do not use it: they encode headers only
751/// ([`encode_chunk_frame_headers`]) and send the payload uncopied beside them.
752///
753/// The non-empty floor is checked here rather than inherited from [`ChunkData::new`],
754/// since no [`ChunkData`] is built. That keeps the ADR 005 §Non-empty chunk invariant
755/// true of every frame this crate can emit, by either door.
756///
757/// # Errors
758///
759/// [`MessageValidationError::EmptyChunk`] for a zero-length payload, or
760/// [`MessageValidationError::ChunkTooLarge`] if the frame would exceed
761/// [`MAX_CHUNK_FRAME`].
762pub fn encode_chunk_frame(payload: &[u8]) -> Result<Vec<u8>, MessageValidationError> {
763    if payload.is_empty() {
764        return Err(MessageValidationError::EmptyChunk);
765    }
766    // Postcard payload length `1 + varint(payload_len) + payload_len` must fit.
767    let postcard_len = chunk_frame_postcard_len(payload.len());
768    if postcard_len > MAX_CHUNK_FRAME as usize {
769        return Err(MessageValidationError::ChunkTooLarge {
770            frame_len: postcard_len,
771        });
772    }
773    // Discriminant, then the `Vec<u8>` field's postcard length prefix, then the
774    // bytes. Postcard writes a sequence length as a LEB128 varint: seven bits per
775    // byte, low group first, high bit set on every byte but the last.
776    let mut out = Vec::with_capacity(1 + 5 + payload.len());
777    out.push(CHUNK_DATA_DISCRIMINANT);
778    let mut len = payload.len();
779    loop {
780        let mut byte = u8::try_from(len & 0x7F).unwrap_or(0);
781        len >>= 7;
782        if len != 0 {
783            byte |= 0x80;
784        }
785        out.push(byte);
786        if len == 0 {
787            break;
788        }
789    }
790    out.extend_from_slice(payload);
791    Ok(out)
792}
793
794/// Encode the bare `ChunkData` postcard prefix (discriminant + varint payload
795/// length) into `out`, returning the number of bytes written.
796///
797/// There is no framing length in front of it, so this alone is not a valid frame
798/// and must never be written to a stream. [`encode_chunk_frame_headers`] builds on
799/// it and is what the serve paths send.
800///
801/// # Errors
802///
803/// [`MessageValidationError::EmptyChunk`] for `payload_len == 0`, or
804/// [`MessageValidationError::ChunkTooLarge`] if the frame would exceed
805/// [`MAX_CHUNK_FRAME`].
806pub(crate) fn encode_chunk_data_header(
807    payload_len: usize,
808    out: &mut [u8; CHUNK_DATA_HEADER_MAX],
809) -> Result<usize, MessageValidationError> {
810    if payload_len == 0 {
811        return Err(MessageValidationError::EmptyChunk);
812    }
813    // Postcard payload length `1 + varint(payload_len) + payload_len` must fit.
814    let postcard_len = chunk_frame_postcard_len(payload_len);
815    if postcard_len > MAX_CHUNK_FRAME as usize {
816        return Err(MessageValidationError::ChunkTooLarge {
817            frame_len: postcard_len,
818        });
819    }
820    // The cap above bounds `payload_len` well inside `u32`, so this conversion and
821    // the `get_mut` guards below cannot fail. They are `indexing_slicing` lint
822    // appeasement; each still errors rather than writing a short header, so a future
823    // cap change cannot turn one into a wrong length prefix.
824    let payload_len_u32 =
825        u32::try_from(payload_len).map_err(|_| MessageValidationError::ChunkTooLarge {
826            frame_len: postcard_len,
827        })?;
828    let Some(slot) = out.get_mut(0) else {
829        return Err(MessageValidationError::ChunkTooLarge {
830            frame_len: postcard_len,
831        });
832    };
833    *slot = CHUNK_DATA_DISCRIMINANT;
834    let mut varint_buf = [0u8; 5];
835    let varint_len = crate::framing::encode_varint_u32(payload_len_u32, &mut varint_buf);
836    let total = 1usize.saturating_add(varint_len);
837    let (Some(dst), Some(src)) = (out.get_mut(1..total), varint_buf.get(..varint_len)) else {
838        return Err(MessageValidationError::ChunkTooLarge {
839            frame_len: postcard_len,
840        });
841    };
842    dst.copy_from_slice(src);
843    Ok(total)
844}
845
846/// Encode the full frame headers for a `ChunkData` payload of `payload_len`
847/// bytes: the framing varint `postcard_len` followed by the `ChunkData` header
848/// (discriminant + varint `payload_len`). The payload itself is **not**
849/// included — the caller sends it as `Bytes` alongside this header via a
850/// vectored write.
851///
852/// Returns the number of header bytes written into `out` (≤
853/// [`CHUNK_FRAME_HEADERS_MAX`]).
854///
855/// # Errors
856///
857/// [`MessageValidationError::EmptyChunk`] for `payload_len == 0`, or
858/// [`MessageValidationError::ChunkTooLarge`] if the frame would exceed
859/// [`MAX_CHUNK_FRAME`].
860pub fn encode_chunk_frame_headers(
861    payload_len: usize,
862    out: &mut [u8; CHUNK_FRAME_HEADERS_MAX],
863) -> Result<usize, MessageValidationError> {
864    if payload_len == 0 {
865        return Err(MessageValidationError::EmptyChunk);
866    }
867    // The `ChunkData` header first, into a temporary, so the framing varint can
868    // carry its measured length rather than a second guess at it.
869    let mut hdr = [0u8; CHUNK_DATA_HEADER_MAX];
870    let hdr_len = encode_chunk_data_header(payload_len, &mut hdr)?;
871    let postcard_len = hdr_len.saturating_add(payload_len);
872    if postcard_len > MAX_CHUNK_FRAME as usize {
873        return Err(MessageValidationError::ChunkTooLarge {
874            frame_len: postcard_len,
875        });
876    }
877    // As in `encode_chunk_data_header`: the cap bounds this inside `u32` and the
878    // slice guards below cannot fail, but each errors rather than writing a short
879    // header so a future cap change cannot turn one into a wrong length prefix.
880    let postcard_len_u32 =
881        u32::try_from(postcard_len).map_err(|_| MessageValidationError::ChunkTooLarge {
882            frame_len: postcard_len,
883        })?;
884    let mut framing_buf = [0u8; 5];
885    let framing_len = crate::framing::encode_varint_u32(postcard_len_u32, &mut framing_buf);
886    let total = framing_len.saturating_add(hdr_len);
887    let (Some(dst), Some(src)) = (out.get_mut(..framing_len), framing_buf.get(..framing_len))
888    else {
889        return Err(MessageValidationError::ChunkTooLarge {
890            frame_len: postcard_len,
891        });
892    };
893    dst.copy_from_slice(src);
894    let (Some(dst), Some(src)) = (out.get_mut(framing_len..total), hdr.get(..hdr_len)) else {
895        return Err(MessageValidationError::ChunkTooLarge {
896            frame_len: postcard_len,
897        });
898    };
899    dst.copy_from_slice(src);
900    Ok(total)
901}
902
903/// Length of the postcard payload (`CHUNK_DATA_DISCRIMINANT` + varint
904/// `payload_len` + payload) for a given `payload_len`. This is the size gate the
905/// encoders admit on; `encode_chunk_frame_headers` sizes its framing varint from the
906/// header it measured rather than from a second guess at the same number.
907///
908/// Every caller rejects `payload_len == 0` before reaching here, but the answer for
909/// it is still right: postcard writes a zero-length sequence as a single `0x00`
910/// varint, so an empty `ChunkData` body is 2 bytes.
911#[must_use]
912pub(crate) const fn chunk_frame_postcard_len(payload_len: usize) -> usize {
913    let mut len = payload_len;
914    let mut varint = 0usize;
915    loop {
916        varint = varint.saturating_add(1);
917        len >>= 7;
918        if len == 0 {
919            break;
920        }
921    }
922    1usize.saturating_add(varint).saturating_add(payload_len)
923}
924
925/// Payer → node cumulative payment voucher (ADR 005 §Voucher wire format).
926///
927/// The wire carries `{signature, amount, bytes_delivered, chain_root,
928/// chunk_price}`; the receiver reconstructs the full EIP-712 typed data
929/// `{poolId, signer, provider, amount, bytesDelivered, chainRoot, chunkPrice}`
930/// from stream context (`poolId`/`signer`/`provider` fixed for the stream) plus
931/// the self-described fields. There is no nonce: `amount` is the sole ordering
932/// and replay key — a voucher whose `amount` is no greater than the highest
933/// accepted is stale. `bytes_delivered` is an additional signed, monotone
934/// cumulative that must not regress below the highest accepted; it is what the
935/// node verifies against (rather than reconstructing) so same-lane vouchers
936/// settle independent of arrival order. Both are `u64` cumulative totals
937/// matching the contract's on-chain `uint64` `Lane`/`LaneVoucher` storage; the
938/// node zero-extends them to `uint256` to reconstruct the EIP-712 signature.
939///
940/// `amount` is the **settlement anchor**, and `chain_root` heads the optional
941/// hash chain that advances it between signatures (ADR 003 §Hash-chain metering
942/// (`PayWord`)). Redemption resolves both with one formula:
943/// `claimed = amount + chain_index × chunk_price`.
944#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
945pub struct Voucher {
946    /// EOA secp256k1 EIP-712 signature (`r‖s‖v`, exactly [`VOUCHER_SIG_LEN`]).
947    pub signature: Vec<u8>,
948    /// Cumulative payment in token base units.
949    pub amount: u64,
950    /// Cumulative bytes delivered. Signed by the client and transmitted so
951    /// the node verifies against exactly what was signed, independent of
952    /// same-lane stream ordering.
953    pub bytes_delivered: u64,
954    /// Head of the hash chain this voucher opens: `keccak^MAX_CHAIN_LENGTH` of
955    /// the payer's per-lane seed. All-zero **seals** the voucher at exactly
956    /// `amount` — no value that hashes to zero is findable, so no index above 0
957    /// can redeem against it (ADR 003 §The sealed voucher (zero-hash root)).
958    pub chain_root: [u8; 32],
959    /// The price one chunk of delivery adds over `amount`, in token base units.
960    /// Signed so the claim arithmetic is fixed at signing time. A metering
961    /// voucher MUST carry the node's quoted `rate_per_mb` (`CHUNK_BYTES ==
962    /// MB_BYTES`, so a chunk costs exactly one MB); a sealed voucher meters no
963    /// chunk and MUST carry `0`. The node rejects otherwise with
964    /// [`VoucherRejectReason::ChunkPriceMismatch`] (ADR 003 §Chunk Cadence).
965    pub chunk_price: u64,
966}
967
968impl Voucher {
969    /// Validate the wire-level `signature` length ([`VOUCHER_SIG_LEN`]). The
970    /// cryptographic checks (recovery, monotonicity, deposit bound) happen in
971    /// `decdn_incentive` once the typed data is reconstructed.
972    pub const fn validate(&self) -> Result<(), MessageValidationError> {
973        if self.signature.len() != VOUCHER_SIG_LEN {
974            return Err(MessageValidationError::InvalidVoucherSigLen {
975                len: self.signature.len(),
976            });
977        }
978        Ok(())
979    }
980}
981
982/// Payer → node released hash-chain preimage, advancing the lane's claim by one
983/// chunk with no signature (ADR 003 §Hash-chain metering (`PayWord`), ADR 005
984/// §Payment quantum and credit window).
985///
986/// Released after the payer has received and verified the chunk it pays for, so
987/// the payer's exposure stays at zero. Preimage resistance makes the value
988/// self-proving: nobody derives `keccak^(N−k−1)(s)` from `keccak^(N−k)(s)`
989/// without the seed, so a deeper preimage **is** the receipt for every chunk
990/// below it.
991///
992/// # Wire encoding
993///
994/// `preimage ‖ index`, 33 bytes flat — postcard writes a `[u8; 32]` raw and a
995/// `u8` as one byte, so there is no length prefix and no varint. The byte IS
996/// the index, with no offset on send and no increment on receipt.
997///
998/// # Index domain
999///
1000/// Releasable indices are `1..=`[`MAX_CHAIN_LENGTH`]. `index == 0` is a
1001/// protocol error — index 0 names `chain_root` and is the settlement case at
1002/// redemption, so it proves nothing the voucher does not already say. It is
1003/// rejected **in band** by the delivery handler as
1004/// [`VoucherRejectReason::ChainIndexZero`] rather than at decode, so the
1005/// payer learns why instead of seeing an opaque stream close. An index above
1006/// [`MAX_CHAIN_LENGTH`] cannot be encoded at all: the walk is bounded at 255
1007/// hashes by the type, which is stronger than a runtime comparison.
1008#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1009pub struct ChunkPreimage {
1010    /// The released value, `keccak^(MAX_CHAIN_LENGTH − index)` of the payer's
1011    /// per-lane seed.
1012    pub preimage: [u8; 32],
1013    /// Depth of `preimage` in the chain. Verified as
1014    /// `keccak^(index − verified)(preimage) == tip` against the receiving
1015    /// stream's anchor.
1016    pub index: u8,
1017}
1018
1019impl ChunkPreimage {
1020    /// Always `Ok`. Both fields are fixed-width, so a decoded `ChunkPreimage`
1021    /// has no shape to check — and the one value invariant (`index != 0`) is
1022    /// deliberately **not** enforced here: it must surface as an in-band
1023    /// [`VoucherRejectReason::ChainIndexZero`] from the delivery handler,
1024    /// which holds the stream it has to answer on. Validating it here would
1025    /// collapse that reason into a decode failure and close the stream mute.
1026    ///
1027    /// Present so [`ClientMessage::validate`] stays total over the enum, for
1028    /// the same reason `ChunkData::validate` is.
1029    ///
1030    /// # Errors
1031    ///
1032    /// Never.
1033    pub const fn validate(&self) -> Result<(), MessageValidationError> {
1034        Ok(())
1035    }
1036}
1037
1038/// The node's true watermark for a pool capability, echoed back on a gated
1039/// [`StreamError::VoucherRejected`] so a wallet-less client can self-heal
1040/// (issue #1481). `amount`/`bytes_delivered` mirror the seller-side
1041/// `PoolState::last_*` fields as `u64` cumulative totals, for the same reason
1042/// as [`Voucher`] — matching the contract's on-chain `uint64` storage.
1043/// `last_signature` is the node's stored last-accepted **client** signature
1044/// (`r‖s‖v`, exactly [`VOUCHER_SIG_LEN`]) — not a node signature over this
1045/// bundle — so the client can confirm which of its own vouchers the node
1046/// holds.
1047#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1048pub struct WatermarkBundle {
1049    /// Cumulative amount of the node's last-accepted voucher.
1050    pub amount: u64,
1051    /// Cumulative bytes delivered as of the node's last-accepted voucher.
1052    pub bytes_delivered: u64,
1053    /// Root the lane is currently metering against; all-zero when the lane
1054    /// holds no live chain.
1055    pub chain_root: [u8; 32],
1056    /// Deepest chain index the node has verified under `chain_root`.
1057    pub verified_index: u8,
1058    /// The preimage bytes at `verified_index`; all-zero when the node has
1059    /// verified none. Echoed so a re-seeding signer folds
1060    /// `verified_index × chunk_price` back into the amount it re-signs rather
1061    /// than dropping the frontier (ADR 005 §Watermark bundle).
1062    pub tip: [u8; 32],
1063    /// The `chunk_price` the node's last-accepted voucher was signed at.
1064    pub chunk_price: u64,
1065    /// The client's own signature (`r‖s‖v`, exactly [`VOUCHER_SIG_LEN`]) on the
1066    /// node's last-accepted voucher. `Vec<u8>` rather than a fixed array,
1067    /// mirroring [`Voucher::signature`] — postcard/serde signature fields on
1068    /// this wire are length-prefixed `Vec<u8>`, not `[u8; N]` (serde's array
1069    /// impls top out at N=32).
1070    pub last_signature: Vec<u8>,
1071}
1072
1073impl WatermarkBundle {
1074    /// Validate the wire-level `last_signature` length ([`VOUCHER_SIG_LEN`]) —
1075    /// the same client-voucher-signature length [`Voucher::validate`] checks,
1076    /// since this IS a client voucher signature (issue #1481). A caller MUST
1077    /// call this before trusting a bundle enough to `reseed` a ledger from it:
1078    /// the bundle rides inside an application-level `StreamError`, not signed
1079    /// itself, so this is a shape check, not an authentication check — it stops
1080    /// a malformed/truncated bundle from reaching a fixed-length conversion
1081    /// downstream, nothing more.
1082    pub const fn validate(&self) -> Result<(), MessageValidationError> {
1083        if self.last_signature.len() != VOUCHER_SIG_LEN {
1084            return Err(MessageValidationError::InvalidVoucherSigLen {
1085                len: self.last_signature.len(),
1086            });
1087        }
1088        Ok(())
1089    }
1090}
1091
1092/// A stream failure code (ADR 005 §Open-time refusal classes).
1093///
1094/// `NotFound`, `Declined`, and `Unfunded` are the open-time refusal classes and
1095/// ride in [`StreamResponseExt::error`] alongside `ok: false`.
1096/// `VoucherRejected` is the only variant delivered mid-stream, inside a
1097/// [`ClientMessage::StreamError`]. All codes are unsigned and informational,
1098/// never on-chain evidence. A node keeps its finer refusal reason in its own
1099/// per-reason metrics. The wire carries only the class, because a requester
1100/// acts on the class alone.
1101///
1102/// Every refusal scopes to the node that sent it, for the current fetch. A
1103/// requester never ends a fetch on the word of one node.
1104///
1105/// Postcard encodes a struct variant's payload positionally, in declaration
1106/// order, so `VoucherRejected`'s `bundle` field sits after `reason` and any
1107/// further field appends after `bundle`.
1108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1109pub enum StreamError {
1110    /// "No": this node cannot serve the request now. The requester tries
1111    /// other nodes. This node can answer differently later.
1112    NotFound,
1113    /// "No, and stop asking": this node will not serve this hash or request
1114    /// during this fetch. The requester removes it from the candidates for the
1115    /// hash for the rest of the fetch.
1116    Declined,
1117    /// "No at the current funding": this node will not serve this request at
1118    /// the requester's current pool deposit or signer capability (ADR 003
1119    /// §Funding recovery). The requester parks it at the current funding for
1120    /// this fetch, and asks it again only after a funding recovery step.
1121    ///
1122    /// Spoken ONLY to a requester that passed the lane-ownership proof or holds
1123    /// a verified signer binding. Any other requester gets [`Self::NotFound`]
1124    /// for the same state, so pool and signer state stay unmappable off the
1125    /// wire.
1126    Unfunded,
1127    /// Mid-stream payment-voucher rejection carried in a
1128    /// [`ClientMessage::StreamError`] message (never in the initial
1129    /// [`StreamResponse`]).
1130    VoucherRejected {
1131        /// The specific validation failure.
1132        reason: VoucherRejectReason,
1133        /// The node's true watermark plus the client's own last-accepted
1134        /// signature, attached ONLY on the watermark-gated reasons
1135        /// ([`VoucherRejectReason::is_watermark_gated`]) and ONLY
1136        /// when the rejected voucher's signature recovers to the
1137        /// capability's pinned `voucher_signer` (issue #1481 §5 security
1138        /// property — otherwise anyone who guessed the chain-derivable
1139        /// `pool_id` could pull the node's watermark). A wallet-less client
1140        /// cannot reconstruct its watermark from chain (the claim watermark
1141        /// is `0` until settlement), so this lets it self-heal: re-seed the
1142        /// ledger's PAYMENT BASELINE to `bytes_delivered` (a pool-cumulative
1143        /// counter, NOT a blob `byte_offset`) and re-sign from the new
1144        /// baseline. `None` for every reason that is not watermark-gated and
1145        /// whenever the signer does not recover to `voucher_signer`.
1146        bundle: Option<WatermarkBundle>,
1147    },
1148}
1149
1150impl StreamError {
1151    /// `true` for the delivery-side codes that ride in [`StreamResponseExt::error`]
1152    /// alongside `ok: false` — everything except `VoucherRejected`. Expresses
1153    /// the enum's domain split in code rather than only in prose, and backs the
1154    /// [`StreamResponseExt::validate`] mid-stream-only exclusion.
1155    pub const fn is_delivery_side(&self) -> bool {
1156        !self.is_mid_stream()
1157    }
1158
1159    /// `true` for the mid-stream-only [`StreamError::VoucherRejected`], which
1160    /// rides exclusively in [`ClientMessage::StreamError`].
1161    pub const fn is_mid_stream(&self) -> bool {
1162        matches!(self, Self::VoucherRejected { .. })
1163    }
1164}
1165
1166/// Why a [`Voucher`] was rejected (ADR 005 §`VoucherRejected` semantics).
1167///
1168/// The first seven variants, [`Self::BadPreimage`], and [`Self::UnderFold`]
1169/// mirror `decdn_incentive::PoolError` ∪ `VoucherError` one-to-one; the
1170/// handler-side conversion `voucher_reject_reason` matches those exhaustively
1171/// so a new `PoolError` variant fails to compile until this enum is extended
1172/// (ADR 005 §Mirror obligation). The other variants are emitted directly by the
1173/// `cdn/client/v1` handler: [`Self::CapabilityExpired`] fires when the signer's
1174/// capability has passed its expiry; [`Self::PoolExhausted`] fires when the
1175/// pool's remaining deposit can no longer fund further credit;
1176/// [`Self::SignerCapExhausted`] fires when the signer's shared cap headroom can
1177/// no longer cover a serve floor; the three remaining hash-chain reasons
1178/// ([`Self::ChainIndexZero`], [`Self::UnanchoredPreimage`],
1179/// [`Self::ChunkPriceMismatch`]) are raised where the handler holds the
1180/// per-stream chain anchor or quoted rate a validation enum cannot see; and
1181/// [`Self::Underpaid`] fires when the span a voucher adds over the lane's
1182/// accepted watermark pays below the quoted `rate_per_mb`.
1183/// Variant order is the postcard wire discriminant, so a reorder is a
1184/// wire-breaking change.
1185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1186pub enum VoucherRejectReason {
1187    /// Signature malformed (corrupted bytes, non-canonical `s`, invalid
1188    /// recovery id). `VoucherError::InvalidSignature`.
1189    BadSignature,
1190    /// Signature well-formed but recovers to the wrong signer.
1191    /// `VoucherError::WrongSigner`.
1192    WrongSigner,
1193    /// `voucher.pool_id` mismatch (also: unknown pool). `PoolError::WrongPool`.
1194    WrongPool,
1195    /// The voucher names a provider node other than the one receiving it —
1196    /// a capability voucher scoped to one node redeemed against another.
1197    /// `PoolError::WrongProvider`.
1198    WrongProvider,
1199    /// Cumulative amount regressed. `PoolError::AmountRegression`.
1200    AmountRegression,
1201    /// Cumulative bytes delivered regressed. `PoolError::BytesRegression`.
1202    BytesRegression,
1203    /// The signer's remaining spending cap is exhausted — the voucher amount
1204    /// exceeds what the capability has left to spend (`cap − spent`).
1205    /// Recovery: the pool **owner** raises this signer's cap or delegates a new
1206    /// capability. `PoolError::CapExceeded`.
1207    SpendingCapExhausted,
1208    /// The signer's capability has passed its `expiry`, or lies within the node's
1209    /// expiry margin of it. The node's serve-side check compares its LOCAL wall
1210    /// clock (operator-settable) plus that margin against `expiry`, refuses the
1211    /// voucher or preimage, and stops serving the lane. The margin is one redeem
1212    /// interval plus a landing slack, so the lane's claim is final when the
1213    /// margin starts, and the node's redeem sweep one second later has time to
1214    /// land before the expiry (ADR 003 §Revocation). A lane below the redemption
1215    /// floor can still wait past it. On-chain settlement separately gates redemption on
1216    /// `block.timestamp`. Already-earned vouchers stay redeemable until expiry at
1217    /// settlement. Recovery: the owner mints a **fresh capability** with a new expiry —
1218    /// a watermark resync or top-up does not help. Emitted directly by the
1219    /// `cdn/client/v1` handler (the node holds the clock), not via the `PoolError`
1220    /// bridge.
1221    CapabilityExpired,
1222    /// The pool's on-chain remaining deposit, minus the refundable floor `M` and
1223    /// the pool's already-committed concurrent floor credit, can no longer fund
1224    /// further credit for this stream (ADR 003 §Pool solvency). A pool-wide
1225    /// condition, not this signer's. Recovery: the pool **owner tops up the
1226    /// deposit**. Emitted mid-stream by the serve loop after the client has proved
1227    /// capability ownership; the open-time equivalent is [`StreamError::Unfunded`],
1228    /// spoken only to a proven requester. Not watermark-gated.
1229    PoolExhausted,
1230    /// The signer's shared on-chain `cap − spent` headroom, tracked across every
1231    /// provider, can no longer cover a serve floor — the signer has drained its
1232    /// `cap` at OTHER nodes since this stream was admitted, so further vouchers
1233    /// here would redeem `min(desired, cap − spent) ≈ 0` and the node would eat the
1234    /// delivered bytes (ADR 003 §Pool solvency). A per-SIGNER condition, distinct
1235    /// from [`Self::PoolExhausted`]: the pool's `remaining` can be healthy on other
1236    /// signers' budgets while this one signer's cap is spent. Recovery: the pool
1237    /// **owner** raises this signer's cap or delegates a fresh capability. Emitted
1238    /// mid-stream after the client has proved capability ownership, so naming the
1239    /// condition is post-auth and leaks nothing an open-time refusal must hide (the
1240    /// admit-time equivalent is [`StreamError::Unfunded`], spoken only to a proven
1241    /// requester). Not watermark-gated.
1242    SignerCapExhausted,
1243    /// A released [`ChunkPreimage`] does not hash to the stream's deepest
1244    /// verified preimage in `index − verified` steps (ADR 003 §Concurrent
1245    /// Streams, Rule 2). A payer bug — a wrong seed, a wrong chain, or a
1246    /// mis-derived index. On-chain the same mismatch reverts `BadPreimage`,
1247    /// which is caller error and not transient state, so this is terminal and
1248    /// carries **no** watermark bundle: a preimage has no signature of its own,
1249    /// and a payment watermark cannot repair a hash-chain mismatch.
1250    BadPreimage,
1251    /// A [`ChunkPreimage`] arrived with `index == 0`, which never travels the
1252    /// wire — index 0 names `chain_root` and is the settlement case at
1253    /// redemption. A payer bug; do not retry.
1254    ///
1255    /// Zero is the whole of it. There is no over-large index to report: the wire
1256    /// index is a `u8` and [`MAX_CHAIN_LENGTH`] is 255, so an index past the end
1257    /// of the chain cannot be encoded. A chain that has run out of indices is
1258    /// not this reason either — the payer rolls to a fresh `chain_root` first.
1259    ChainIndexZero,
1260    /// A [`ChunkPreimage`] arrived on a stream that holds no chain anchor, so
1261    /// the node cannot name the chain the reveal belongs to (ADR 003
1262    /// §Concurrent Streams, Rule 1). Per-stream and therefore decidable, which
1263    /// a lane-wide reading would not be. **Not fatal**: the payer sends the
1264    /// current epoch's `chain_root` voucher on this stream and resends the
1265    /// preimage. The resend is free — an at-or-below-watermark voucher is
1266    /// already-satisfied rather than rejected.
1267    UnanchoredPreimage,
1268    /// The voucher's `chunk_price` is not the node's quoted `rate_per_mb` on a
1269    /// metering voucher, or is non-zero on a sealed one (`chain_root == 0`).
1270    ///
1271    /// `chunk_price` is signed by the *payer* and a preimage carries no price
1272    /// of its own, so a voucher signed at the governance floor against a node
1273    /// quoting ten times that would meter every later chunk at a tenth of the
1274    /// quote, with no per-tick moment revealing it. The node therefore checks
1275    /// the price it is being paid before it meters against it (ADR 003 §Chunk
1276    /// Cadence). A payer bug: re-read `rate_per_mb` from the `StreamResponse`
1277    /// and re-sign; do not retry with the same price.
1278    ChunkPriceMismatch,
1279    /// The voucher advances the lane, but the span it adds over the node's
1280    /// last-accepted watermark pays below the quoted `rate_per_mb` (ADR 003
1281    /// §Voucher withholding). The node stops delivery and credits nothing.
1282    ///
1283    /// An honest payer reaches this only when its local watermark has diverged
1284    /// from the node's: its cumulative carries bytes the node never accepted a
1285    /// voucher for, so every later span looks short. The reject therefore carries
1286    /// the node's [`WatermarkBundle`]. Recovery: the payer verifies the bundle
1287    /// against its own key, rebases its lane to it once, and retries (ADR 005
1288    /// §`VoucherRejected` semantics).
1289    Underpaid,
1290    /// The voucher is at or above the signed anchor and names a `chain_root`
1291    /// other than the live one, which retires the lane's live chain, but its
1292    /// `amount` or `bytes_delivered` does not fold the frontier that chain
1293    /// proved: the signed anchor plus `verified_index` chunks (ADR 003 §Chain
1294    /// length and rollover). At exactly the anchor, only a voucher that opens a
1295    /// chain qualifies; a sealed one there is [`Self::AmountRegression`].
1296    /// Accepting it would discard chunks the node holds preimages for, so the
1297    /// node adopts nothing.
1298    ///
1299    /// A payer reaches this when it resumes from a watermark that trails the
1300    /// node's verified frontier. The reject carries the node's
1301    /// [`WatermarkBundle`]. Recovery: the payer verifies the bundle against its
1302    /// own key, reseeds to it with the chain frontier folded in, and retries.
1303    /// Concurrent streams on the lane get the same bundle; one that finds its
1304    /// ledger already at the bundle retries without a reseed. A voucher that
1305    /// already covers the claim's `amount` but falls short on
1306    /// `bytes_delivered` cannot heal this way — the bundle does not advance the
1307    /// payer's amount — so the payer surfaces it as terminal.
1308    /// `PoolError::UnderFold`.
1309    UnderFold,
1310}
1311
1312impl VoucherRejectReason {
1313    /// Whether a [`StreamError::VoucherRejected`] carrying this reason is
1314    /// eligible for a [`WatermarkBundle`] (issue #1481 §5): exactly five
1315    /// reasons. The three regression/exhaustion reasons are ones a wallet-less
1316    /// client cannot distinguish from chain, since its local watermark is the
1317    /// only thing that could be wrong. [`Self::Underpaid`] and
1318    /// [`Self::UnderFold`] are the other two: a payer reaches them only through
1319    /// that same watermark divergence, and the bundle states the watermark it
1320    /// has to rebase or fold to.
1321    /// Every other reason (`CapabilityExpired`, `PoolExhausted`,
1322    /// `SignerCapExhausted`, the chain reasons) and the signer/pool/provider
1323    /// mismatches are never eligible — a bundle would not help there, since the
1324    /// fix is not "resync the watermark".
1325    ///
1326    /// Single source of truth for the gate: the node checks this before
1327    /// attaching a bundle (`crates/node/src/handlers/client/voucher.rs`) and
1328    /// the client checks it again before trusting one enough to self-heal
1329    /// (`crates/client/src/lib.rs`) — both call this rather than each
1330    /// keeping their own copy of the match.
1331    #[must_use]
1332    pub const fn is_watermark_gated(self) -> bool {
1333        matches!(
1334            self,
1335            Self::SpendingCapExhausted
1336                | Self::AmountRegression
1337                | Self::BytesRegression
1338                | Self::Underpaid
1339                | Self::UnderFold
1340        )
1341    }
1342}
1343
1344#[cfg(test)]
1345mod tests;