decdn_protocol/dht.rs
1//! Wire message types for the `cdn/dht/v1` Kademlia content DHT (ADR 022).
2//!
3//! `cdn/dht/v1` is the primary content discovery mechanism: nodes publish
4//! `(hash → NodeId)` records to the K closest peers in keyspace, and clients
5//! iteratively look up providers in O(log N) hops. This module defines the
6//! wire surface only — routing-table, record-store, scheduler, and rate-limit
7//! logic live in `decdn-node`.
8//!
9//! # Authentication model
10//!
11//! [`StoreRequest`] has no per-record signature. The receiver instead checks
12//! that `holder` equals the authenticated `NodeId` of the inbound QUIC
13//! connection (`NodeId`s are 32-byte Ed25519 public keys; iroh's QUIC handshake
14//! binds the connection to that key). This depends on records being pushed
15//! directly by the holder to the K-closest nodes and never relayed
16//! peer-to-peer — if a future scheme introduces peer relay of records, a
17//! per-record signature MUST be reintroduced together with an in-scope sender
18//! timestamp in the signed body (ADR 022 §STORE Flow).
19//!
20//! # `BatchStore` wire types
21//!
22//! `BatchStoreRequest` / `BatchStoreAck` are part of `cdn/dht/v1` at
23//! [`DhtMessage`] discriminants 4 and 5, ahead of `FindNode` /
24//! `FindNodeResponse` at 6 and 7 to match ADR 022's variant order. They
25//! carry the bandwidth optimization in which one holder publishes many
26//! hashes to one receiver in a single RPC (ADR 022 §STORE Flow Batched
27//! STORE). The receiver-side admission and two-stage rate-limit
28//! accounting live in `decdn-node`; every DHT node implements them, so
29//! there is no per-hash fallback or support negotiation.
30//!
31//! # Bounded-Vec deserialization
32//!
33//! Network responses with `Vec` fields are bounded at decode time via
34//! `#[serde(deserialize_with = ...)]` hooks that enforce the wire-level
35//! caps below. The framing layer caps the whole message at 16 MiB
36//! ([`crate::framing::MAX_MESSAGE_SIZE`]), but the per-field caps stop
37//! peers from forcing the responder to allocate up to a megabyte of
38//! 32-byte `NodeId`s (or up to `~16M / 8B` booleans) inside one frame.
39//! Pattern mirrors `ProbeResponseBody::rate_per_mb`.
40
41use serde::de::{self, Deserializer};
42use serde::{Deserialize, Serialize};
43
44use crate::coverage::Coverage;
45use crate::identity::{ContentHash, NodeId};
46
47/// Wire-level maximum number of providers a [`FindValueResponse`] may carry
48/// (ADR 022 §Content Records and TTL). Bounds the response size at the
49/// `~2.3 KB` ceiling documented in ADR 022 §DHT Bandwidth Analysis.
50///
51/// Enforced at the wire boundary by `deserialize_providers`; oversize values
52/// fail decode rather than being silently truncated.
53pub const MAX_PROVIDERS_PER_HASH: usize = 50;
54
55/// Wire-level maximum number of `NodeId`s in a `closer_nodes` field, equal to
56/// the Kademlia bucket size K=20 (ADR 022 §Routing Table). Applies to both
57/// [`FindValueResponse::closer_nodes`] and [`FindNodeResponse::closer_nodes`].
58/// Enforced by the [`CloserNodes`] type — at construction
59/// ([`CloserNodes::try_new`]) and at the wire boundary (its [`Deserialize`]).
60pub const MAX_CLOSER_NODES: usize = 20;
61
62/// Wire-level maximum number of hashes a [`BatchStoreRequest`] may carry
63/// (ADR 022 §STORE Flow — "Batch size is bounded at 256 hashes"). Applies
64/// symmetrically to [`BatchStoreAck::results`] (one bool per request hash,
65/// same cap so a malicious responder can't reply with an oversize ack).
66/// Enforced at the wire boundary; ADR 022 spells out that oversize batches
67/// close the stream with `MALFORMED_MESSAGE` (`0x03`) — the decode-time
68/// reject here surfaces as that error code via the framing layer.
69pub const MAX_BATCH_STORE_HASHES: usize = 256;
70
71/// A bounded list of the K closest [`NodeId`]s a responder returns in a
72/// `closer_nodes` field (ADR 022 §Routing Table). Wraps `Vec<NodeId>` and
73/// enforces the [`MAX_CLOSER_NODES`] invariant **at construction** — both
74/// [`CloserNodes::try_new`] and the bounded [`Deserialize`] impl reject
75/// oversize lists, so an over-cap `closer_nodes` cannot exist in memory or on
76/// the wire. `#[repr(transparent)]` + `#[serde(transparent)]` keep the encoding
77/// byte-identical to a bare `Vec<NodeId>`.
78#[repr(transparent)]
79#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
80#[serde(transparent)]
81pub struct CloserNodes(Vec<NodeId>);
82
83/// Error returned by [`CloserNodes::try_new`] when the list exceeds
84/// [`MAX_CLOSER_NODES`].
85#[derive(Debug, thiserror::Error)]
86#[error("closer_nodes length {len} exceeds MAX_CLOSER_NODES ({MAX_CLOSER_NODES})")]
87pub struct CloserNodesError {
88 /// The rejected list length.
89 pub len: usize,
90}
91
92impl CloserNodes {
93 /// Wrap `nodes`, rejecting lists longer than [`MAX_CLOSER_NODES`].
94 ///
95 /// # Errors
96 /// Returns [`CloserNodesError`] when `nodes.len() > MAX_CLOSER_NODES`.
97 pub fn try_new(nodes: Vec<NodeId>) -> Result<Self, CloserNodesError> {
98 if nodes.len() > MAX_CLOSER_NODES {
99 return Err(CloserNodesError { len: nodes.len() });
100 }
101 Ok(Self(nodes))
102 }
103
104 /// Borrow the closer nodes as a slice.
105 #[must_use]
106 pub fn as_slice(&self) -> &[NodeId] {
107 &self.0
108 }
109
110 /// Iterate over the closer nodes.
111 pub fn iter(&self) -> std::slice::Iter<'_, NodeId> {
112 self.0.iter()
113 }
114
115 /// Consume into the inner `Vec<NodeId>`.
116 #[must_use]
117 pub fn into_inner(self) -> Vec<NodeId> {
118 self.0
119 }
120
121 /// Number of closer nodes (always `<= MAX_CLOSER_NODES`).
122 #[must_use]
123 pub const fn len(&self) -> usize {
124 self.0.len()
125 }
126
127 /// True if no closer nodes are present.
128 #[must_use]
129 pub const fn is_empty(&self) -> bool {
130 self.0.is_empty()
131 }
132}
133
134impl<'de> Deserialize<'de> for CloserNodes {
135 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
136 where
137 D: Deserializer<'de>,
138 {
139 // Reuses the shared bounded-vec hook so the decode-time cap and error
140 // message match the other `closer_nodes`-shaped fields exactly.
141 Ok(Self(deserialize_bounded_vec(
142 deserializer,
143 MAX_CLOSER_NODES,
144 "closer_nodes",
145 )?))
146 }
147}
148
149impl<'a> IntoIterator for &'a CloserNodes {
150 type Item = &'a NodeId;
151 type IntoIter = std::slice::Iter<'a, NodeId>;
152 fn into_iter(self) -> Self::IntoIter {
153 self.0.iter()
154 }
155}
156
157/// Top-level protocol enum for `cdn/dht/v1`. Variant order matches ADR 022
158/// §Message Types and is frozen per ADR 013.
159///
160/// ⚠️ **VARIANT ORDER FROZEN — ADR 013 §Protocol Enums**
161/// Postcard encodes each variant as its declaration-order index. Reordering,
162/// inserting, or removing a variant is a wire-breaking change requiring an
163/// ALPN version bump (`cdn/dht/v2`). Discriminants are pinned by the
164/// `dht_message_*_discriminant_is_*` tests in this module — if you change this
165/// enum, those tests will fail and tell you why.
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167pub enum DhtMessage {
168 /// discriminant 0 — asserted by `dht_message_find_value_discriminant_is_zero`
169 FindValue(FindValueRequest),
170 /// discriminant 1 — asserted by `dht_message_find_value_response_discriminant_is_one`
171 FindValueResponse(FindValueResponse),
172 /// discriminant 2 — asserted by `dht_message_store_discriminant_is_two`
173 Store(StoreRequest),
174 /// discriminant 3 — asserted by `dht_message_store_ack_discriminant_is_three`
175 StoreAck(StoreAck),
176 /// discriminant 4 — asserted by `dht_message_batch_store_discriminant_is_four`.
177 /// Batched publish; handled by `decdn-node` (see module docs).
178 BatchStore(BatchStoreRequest),
179 /// discriminant 5 — asserted by `dht_message_batch_store_ack_discriminant_is_five`.
180 /// Per-hash ack for [`Self::BatchStore`].
181 BatchStoreAck(BatchStoreAck),
182 /// discriminant 6 — asserted by `dht_message_find_node_discriminant_is_six`
183 FindNode(FindNodeRequest),
184 /// discriminant 7 — asserted by `dht_message_find_node_response_discriminant_is_seven`
185 FindNodeResponse(FindNodeResponse),
186}
187
188impl crate::framing::TopLevelEnum for DhtMessage {
189 /// `FindValue` (0) … `FindNodeResponse` (7). Pinned by
190 /// `dht_message_variant_count_matches_discriminants`.
191 const VARIANT_COUNT: u32 = 8;
192}
193
194/// Query for providers of a specific content hash (ADR 022 §Message Types).
195///
196/// The responder returns any known providers for `hash` and the K closest
197/// `NodeId`s it knows toward the hash in keyspace.
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
199pub struct FindValueRequest {
200 /// BLAKE3 hash of the content being sought (iroh `Hash`, 32 bytes).
201 pub hash: ContentHash,
202 /// The caller's self-declared `NodeId` (32-byte Ed25519 public key).
203 ///
204 /// **The responder MUST NOT use this wire value to update its routing
205 /// table.** It is attacker-controlled and trusting it reintroduces the
206 /// routing-table-poisoning bug class (an authenticated peer seeding
207 /// arbitrary, possibly unreachable ids it does not own). The responder
208 /// refreshes only the *authenticated* QUIC peer id (`conn.remote_id()`);
209 /// this field is carried for protocol symmetry and possible future use
210 /// (which would require a per-request signature first).
211 pub requester: NodeId,
212}
213
214/// A known holder of a content hash, plus which [`Coverage`] blocks it
215/// reports being able to serve (range-keyed discovery). Carried in
216/// [`FindValueResponse::providers`].
217///
218/// `coverage` is unsigned and responder-controlled, like the rest of a DHT
219/// record — see the module docs' authentication-model note. A lying
220/// responder can only ever cost the requester a wasted probe against a
221/// provider that turns out not to hold what it claimed; it cannot forge a
222/// record for a `node` it doesn't control (the STORE-time `holder ==
223/// authenticated NodeId` check still applies).
224#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
225pub struct Provider {
226 /// The holder's `NodeId`.
227 pub node: NodeId,
228 /// Discovery blocks this holder reports covering.
229 pub coverage: Coverage,
230}
231
232/// Response to a [`FindValueRequest`] (ADR 022 §Message Types).
233///
234/// `providers` is the responder's known holders for `hash` (may be empty);
235/// `closer_nodes` is up to K of the responder's nearest `NodeId`s toward `hash`
236/// in XOR keyspace, used by the requester to continue iterative lookup.
237///
238/// Responders MUST NOT emit more than [`MAX_PROVIDERS_PER_HASH`] providers or
239/// more than [`MAX_CLOSER_NODES`] closer nodes — the wire-cost ceiling
240/// (~2.3 KB) in ADR 022 §DHT Bandwidth Analysis depends on these bounds.
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242pub struct FindValueResponse {
243 /// Echoes the requested hash.
244 pub hash: ContentHash,
245 /// Nodes known to hold this hash, with per-holder coverage. May be
246 /// empty. Capped at [`MAX_PROVIDERS_PER_HASH`]; oversize values fail
247 /// decode.
248 #[serde(deserialize_with = "deserialize_providers")]
249 pub providers: Vec<Provider>,
250 /// K closest nodes to `hash` in the responder's routing table. The
251 /// [`CloserNodes`] type enforces the [`MAX_CLOSER_NODES`] cap at
252 /// construction and decode; oversize values fail decode.
253 pub closer_nodes: CloserNodes,
254}
255
256/// Publish a content record: "I hold this hash" (ADR 022 §Message Types,
257/// §STORE Flow). Sent by the holder to each of the K+3 nodes closest to
258/// `hash` in keyspace.
259///
260/// There is no per-record signature: the receiver MUST instead check that
261/// `holder` equals the authenticated `NodeId` of the inbound QUIC connection.
262/// Records are never relayed peer-to-peer; if that changes, a per-record
263/// signature MUST be reintroduced (see module docs).
264#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
265pub struct StoreRequest {
266 /// BLAKE3 hash of the content being advertised.
267 pub hash: ContentHash,
268 /// The `NodeId` claiming to hold this hash. MUST equal the authenticated
269 /// QUIC `NodeId` of the connection; the receiver rejects mismatches.
270 pub holder: NodeId,
271 /// Discovery blocks the holder reports covering for `hash`.
272 pub coverage: Coverage,
273}
274
275/// Optional [`FindValueResponse`] extension fields, carried as trailing bytes
276/// after the message via the two-phase pattern (ADR 013 §Tier 1; see
277/// [`encode_find_value_response`] / [`parse_find_value_response_ext`]).
278///
279/// Empty today. It names the append site for the first field a responder needs —
280/// a coverage hint, a record age, a freshness bound — so that field lands as a
281/// pure append rather than a `cdn/dht/v2` bump. Postcard fills no defaults for
282/// absent trailing fields, which is why the field must go in a separately encoded
283/// extension rather than onto [`FindValueResponse`] itself.
284///
285/// Anything added here is UNSIGNED and responder-controlled. DHT records carry
286/// no per-record signature (see the module docs), so an extension field is a
287/// hint the requester may act on only where a lying responder gains nothing.
288#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
289pub struct FindValueResponseExt {}
290
291/// Optional [`StoreRequest`] extension fields, carried as trailing bytes after
292/// the message via the two-phase pattern (ADR 013 §Tier 1; see
293/// [`encode_store_request`] / [`parse_store_request_ext`]).
294///
295/// Empty today, for the same reason as [`FindValueResponseExt`]: the publisher
296/// side is where a record would gain a TTL hint or a partial-holding range, and
297/// that append must stay Tier-1 after launch.
298#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
299pub struct StoreRequestExt {}
300
301/// Encode a [`DhtMessage::FindValueResponse`] with its optional trailing
302/// [`FindValueResponseExt`] (ADR 013 §Tier 1, two-phase).
303///
304/// Typed per variant rather than taking pre-serialized bytes, so an extension
305/// cannot be attached to a message it does not belong to.
306///
307/// # Errors
308///
309/// Propagates a [`postcard::Error`] if serialization fails.
310pub fn encode_find_value_response(
311 resp: &FindValueResponse,
312 ext: Option<&FindValueResponseExt>,
313) -> Result<Vec<u8>, postcard::Error> {
314 let mut buf = postcard::to_allocvec(&DhtMessage::FindValueResponse(resp.clone()))?;
315 if let Some(ext) = ext {
316 buf.extend_from_slice(&postcard::to_allocvec(ext)?);
317 }
318 Ok(buf)
319}
320
321/// Encode a [`DhtMessage::Store`] with its optional trailing [`StoreRequestExt`]
322/// (ADR 013 §Tier 1, two-phase). Typed per variant, as above.
323///
324/// # Errors
325///
326/// Propagates a [`postcard::Error`] if serialization fails.
327pub fn encode_store_request(
328 req: &StoreRequest,
329 ext: Option<&StoreRequestExt>,
330) -> Result<Vec<u8>, postcard::Error> {
331 let mut buf = postcard::to_allocvec(&DhtMessage::Store(req.clone()))?;
332 if let Some(ext) = ext {
333 buf.extend_from_slice(&postcard::to_allocvec(ext)?);
334 }
335 Ok(buf)
336}
337
338/// Parse the trailing [`FindValueResponseExt`] bytes returned as the remainder
339/// by [`crate::decode_message`] after a `DhtMessage::FindValueResponse`.
340///
341/// An empty remainder ⇒ [`FindValueResponseExt::default`]. Trailing bytes beyond
342/// the known fields are tolerated for forward compatibility (ADR 013 §Tier 1).
343///
344/// # Errors
345///
346/// Returns a [`postcard::Error`] if a non-empty remainder is not a valid
347/// `FindValueResponseExt` prefix. While [`FindValueResponseExt`] holds no fields it consumes
348/// no bytes and cannot fail; the signature carries the error so that adding the
349/// first field is a pure append here too.
350pub fn parse_find_value_response_ext(
351 remainder: &[u8],
352) -> Result<FindValueResponseExt, postcard::Error> {
353 if remainder.is_empty() {
354 Ok(FindValueResponseExt::default())
355 } else {
356 Ok(postcard::take_from_bytes::<FindValueResponseExt>(remainder)?.0)
357 }
358}
359
360/// Parse the trailing [`StoreRequestExt`] bytes returned as the remainder by
361/// [`crate::decode_message`] after a `DhtMessage::Store`.
362///
363/// An empty remainder ⇒ [`StoreRequestExt::default`]. Trailing bytes beyond the
364/// known fields are tolerated for forward compatibility (ADR 013 §Tier 1).
365///
366/// # Errors
367///
368/// Returns a [`postcard::Error`] if a non-empty remainder is not a valid
369/// `StoreRequestExt` prefix. While [`StoreRequestExt`] holds no fields it consumes
370/// no bytes and cannot fail; the signature carries the error so that adding the
371/// first field is a pure append here too.
372pub fn parse_store_request_ext(remainder: &[u8]) -> Result<StoreRequestExt, postcard::Error> {
373 if remainder.is_empty() {
374 Ok(StoreRequestExt::default())
375 } else {
376 Ok(postcard::take_from_bytes::<StoreRequestExt>(remainder)?.0)
377 }
378}
379
380/// Acknowledgement for a [`StoreRequest`] (ADR 022 §Message Types).
381///
382/// `accepted == false` means the record was rejected — most commonly the
383/// holder is over per-publisher quota or is not in the receiver's cached
384/// active-staker set (ADR 022 §Content Records and TTL, §STORE Flow).
385#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
386pub struct StoreAck {
387 /// Echoes the published hash.
388 pub hash: ContentHash,
389 /// True iff the record was admitted to the receiver's record store.
390 pub accepted: bool,
391}
392
393/// Kademlia node lookup — used during routing-table bootstrap and bucket
394/// refresh (ADR 022 §Bootstrap, §Routing Table). Distinct from
395/// [`FindValueRequest`] in that the target is a `NodeId`, not a content hash,
396/// and no provider lookup is performed.
397#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
398pub struct FindNodeRequest {
399 /// `NodeId` being sought.
400 pub target: NodeId,
401 /// The caller's self-declared `NodeId`.
402 ///
403 /// **The responder MUST NOT use this wire value to update its routing
404 /// table** — it is attacker-controlled, and trusting it reintroduces the
405 /// routing-table-poisoning bug class. The responder refreshes only the
406 /// *authenticated* QUIC peer id (`conn.remote_id()`); this field is carried
407 /// for protocol symmetry and possible future use (which would require a
408 /// per-request signature first).
409 pub requester: NodeId,
410}
411
412/// Response to a [`FindNodeRequest`] (ADR 022 §Message Types).
413///
414/// Responders MUST NOT emit more than [`MAX_CLOSER_NODES`] entries; oversize
415/// values fail decode at the wire boundary.
416#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
417pub struct FindNodeResponse {
418 /// Echoes the requested target.
419 pub target: NodeId,
420 /// K closest `NodeId`s to `target` from the responder's routing table.
421 /// [`CloserNodes`] enforces the [`MAX_CLOSER_NODES`] cap.
422 pub closer_nodes: CloserNodes,
423}
424
425/// Batched publication of multiple content records from one holder to one
426/// receiver (ADR 022 §STORE Flow). A bandwidth optimization over `n`
427/// separate [`StoreRequest`]s: the re-publish scheduler groups due hashes
428/// by receiver and sends each its set in one of these. Admission and
429/// two-stage rate-limit accounting live in the `decdn-node` handler.
430#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
431pub struct BatchStoreRequest {
432 /// `(hash, coverage)` pairs being published in this batch. Capped at
433 /// [`MAX_BATCH_STORE_HASHES`] (ADR 022 §STORE Flow); oversize values fail
434 /// decode (the handler maps that to `MALFORMED_MESSAGE` 0x03 per
435 /// ADR 013).
436 #[serde(deserialize_with = "deserialize_batch_entries")]
437 pub entries: Vec<(ContentHash, Coverage)>,
438 /// Single holder for the batch. Per ADR 022 §STORE Flow the receiver
439 /// checks this once against the authenticated QUIC `NodeId`; the
440 /// per-hash check is skipped because the field is batch-level.
441 pub holder: NodeId,
442}
443
444/// Per-hash acknowledgement for a [`BatchStoreRequest`] (ADR 022 §STORE
445/// Flow). `results[i]` corresponds to `BatchStoreRequest.entries[i]`.
446#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
447pub struct BatchStoreAck {
448 /// One `bool` per request hash, in request order. Capped at
449 /// [`MAX_BATCH_STORE_HASHES`] (matches the request cap so a malicious
450 /// responder can't reply with an oversize ack).
451 #[serde(deserialize_with = "deserialize_batch_results")]
452 pub results: Vec<bool>,
453}
454
455// --- Bounded-Vec deserialization hooks (see module docs).
456//
457// Pattern follows `ProbeResponseBody::rate_per_mb`'s field-level
458// `deserialize_with` so the postcard positional layout stays
459// derive-driven — no mirror struct, no `Deserialize` impl drift risk.
460
461fn deserialize_providers<'de, D>(d: D) -> Result<Vec<Provider>, D::Error>
462where
463 D: Deserializer<'de>,
464{
465 deserialize_bounded_vec(d, MAX_PROVIDERS_PER_HASH, "providers")
466}
467
468fn deserialize_batch_entries<'de, D>(d: D) -> Result<Vec<(ContentHash, Coverage)>, D::Error>
469where
470 D: Deserializer<'de>,
471{
472 deserialize_bounded_vec(d, MAX_BATCH_STORE_HASHES, "entries")
473}
474
475fn deserialize_batch_results<'de, D>(d: D) -> Result<Vec<bool>, D::Error>
476where
477 D: Deserializer<'de>,
478{
479 let v = Vec::<bool>::deserialize(d)?;
480 if v.len() > MAX_BATCH_STORE_HASHES {
481 return Err(de::Error::custom(format!(
482 "BatchStoreAck.results length {} exceeds MAX_BATCH_STORE_HASHES ({})",
483 v.len(),
484 MAX_BATCH_STORE_HASHES,
485 )));
486 }
487 Ok(v)
488}
489
490fn deserialize_bounded_vec<'de, D, T>(
491 d: D,
492 max: usize,
493 field: &'static str,
494) -> Result<Vec<T>, D::Error>
495where
496 D: Deserializer<'de>,
497 T: Deserialize<'de>,
498{
499 // `T` is either `#[serde(transparent)]` over `[u8; 32]` (a bare `NodeId`
500 // or `ContentHash`) or a small fixed-shape struct built from such types
501 // (`Provider`, `(ContentHash, Coverage)`), so the decoded length and
502 // per-element bytes are identical to the raw wire form; the cap is the
503 // only thing this hook adds.
504 //
505 // Allocation safety (#845): the cap is enforced *after* `Vec::deserialize`,
506 // so a malicious peer's length prefix is untrusted at the point of decode.
507 // This is sound because the up-front allocation is bounded independently of
508 // that prefix — serde's `Vec` impl uses `size_hint::cautious`, which caps
509 // `with_capacity` to a small byte budget, and postcard streams elements
510 // (a giant length prefix with a truncated body errors at EOF long before
511 // the cap check). See `bounded_vec_giant_length_prefix_truncated_body_*`.
512 let v = Vec::<T>::deserialize(d)?;
513 if v.len() > max {
514 return Err(de::Error::custom(format!(
515 "{field} length {} exceeds wire cap ({max})",
516 v.len(),
517 )));
518 }
519 Ok(v)
520}
521
522#[cfg(test)]
523#[allow(
524 clippy::unwrap_used,
525 clippy::expect_used,
526 clippy::panic,
527 clippy::indexing_slicing
528)]
529mod tests;