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