decdn_protocol/lib.rs
1//! The deCDN wire protocol: the ALPN identifiers, the message types each ALPN
2//! carries, and the framing that puts them on a QUIC stream.
3//!
4//! deCDN speaks three ALPNs ([ADR 005]):
5//!
6//! | ALPN | Constant | Messages | Purpose |
7//! |---|---|---|---|
8//! | `cdn/probe/v1` | [`ALPN_PROBE`] | [`ProbeMessage`] | Latency and availability probing |
9//! | `cdn/client/v1` | [`ALPN_CLIENT`] | [`ClientMessage`] | All paid delivery, client to node and node to node |
10//! | `cdn/dht/v1` | [`ALPN_DHT`] | [`DhtMessage`] | Content discovery over a Kademlia DHT ([ADR 022]) |
11//!
12//! # Modules
13//!
14//! | Module | Contents |
15//! |---|---|
16//! | [`framing`] | The length-prefixed frame, [`encode_message`] / [`decode_message`], and the async [`read_frame`] / [`write_frame`] |
17//! | [`message`] | `cdn/probe/v1`: [`ProbeRequest`], [`ProbeResponse`] |
18//! | [`client`] | `cdn/client/v1`: [`StreamRequest`], [`StreamResponse`], [`ChunkData`], the wire [`Voucher`], [`StreamError`] |
19//! | [`dht`] | `cdn/dht/v1`: store and find requests, provider records, closer-node lists |
20//! | [`coverage`] | The block coverage bitmap a partial holder advertises |
21//! | [`identity`] | The 32-byte [`NodeId`] and [`ContentHash`] newtypes |
22//! | [`region`] | ISO 3166-1 alpha-2 [`Region`] codes |
23//!
24//! # Frames and extensions
25//!
26//! Every message is a postcard encoding behind a varint length prefix. Each read
27//! caps the prefix at its frame ceiling — [`MAX_PROBE_FRAME`],
28//! [`MAX_CLIENT_CONTROL_FRAME`], [`MAX_CHUNK_FRAME`] or [`MAX_DHT_FRAME`]
29//! ([ADR 013 § Wire Framing]). A signed message has a frozen
30//! base: the fields its signature covers. Unsigned fields travel as trailing
31//! bytes after the base. [`decode_message`] returns them as a remainder, and the
32//! `parse_*_ext` functions read them ([ADR 013 § Three Tiers of Evolution]).
33//!
34//! ```
35//! use decdn_protocol::{ProbeMessage, ProbeRequest, decode_message, encode_message};
36//!
37//! let request = ProbeMessage::Request(ProbeRequest {
38//! hash: [7; 32],
39//! timestamp_us: 1_700_000_000_000_000,
40//! });
41//! let frame = encode_message(&request)?;
42//! let (decoded, remainder) = decode_message::<ProbeMessage>(&frame)?;
43//! assert_eq!(decoded, request);
44//! assert!(remainder.is_empty());
45//! # Ok::<(), decdn_protocol::FrameError>(())
46//! ```
47//!
48//! This crate holds no cryptography. The signatures carried in these messages
49//! are made and checked by [`decdn-incentive`]. It has few dependencies, so
50//! code that only speaks the protocol links no blob store, Ethereum client, or
51//! cloud SDK.
52//!
53//! [ADR 005]: https://github.com/decdn/decdn/blob/main/adr/005-protocol.md
54//! [ADR 022]: https://github.com/decdn/decdn/blob/main/adr/022-content-discovery.md
55//! [ADR 013 § Wire Framing]: https://github.com/decdn/decdn/blob/main/adr/013-schema-evolution.md#wire-framing
56//! [ADR 013 § Three Tiers of Evolution]: https://github.com/decdn/decdn/blob/main/adr/013-schema-evolution.md#three-tiers-of-evolution
57//! [`decdn-incentive`]: https://docs.rs/decdn-incentive
58
59pub mod client;
60pub mod coverage;
61pub mod dht;
62pub mod framing;
63pub mod identity;
64pub mod message;
65pub mod region;
66
67pub use client::{
68 BINDING_SIG_LEN, CHUNK_BYTES, ChunkData, ChunkPreimage, ClientBinding, ClientMessage,
69 MAX_CHAIN_LENGTH, MAX_CHUNK_FRAME, MAX_CLIENT_CONTROL_FRAME, MB_BYTES, StreamError,
70 StreamRequest, StreamRequestExt, StreamResponse, StreamResponseBody, StreamResponseExt,
71 VOUCHER_SIG_LEN, Voucher, VoucherRejectReason, encode_chunk_frame, encode_stream_request,
72 encode_stream_response, parse_stream_request_ext, parse_stream_response_ext,
73};
74pub use coverage::{Coverage, DISCOVERY_BLOCK_BYTES, discovery_block_bytes, num_blocks};
75#[cfg(feature = "test-support")]
76pub use coverage::{TestBlockSizeGuard, override_discovery_block_bytes_for_test};
77pub use dht::{
78 BatchStoreAck, BatchStoreRequest, CloserNodes, CloserNodesError, DhtMessage, FindNodeRequest,
79 FindNodeResponse, FindValueRequest, FindValueResponse, FindValueResponseExt,
80 MAX_BATCH_STORE_HASHES, MAX_CLOSER_NODES, MAX_DHT_FRAME, MAX_PROVIDERS_PER_HASH, StoreAck,
81 StoreRequest, StoreRequestExt, encode_find_value_response, encode_store_request,
82 parse_find_value_response_ext, parse_store_request_ext,
83};
84pub use framing::{
85 FrameError, TopLevelEnum, decode_message, encode_message, is_unknown_variant, read_frame,
86 write_frame,
87};
88pub use identity::{ContentHash, ID_LEN, NodeId};
89pub use message::{
90 EMPTY_ROOT, MAX_PROBE_FRAME, MAX_RATE_PER_MB, MessageValidationError, ProbeMessage,
91 ProbeRequest, ProbeResponse, ProbeResponseBody, ProbeResponseExt, SLASH_SIG_LEN,
92 encode_probe_response, parse_probe_response_ext,
93};
94pub use region::{InvalidRegion, Region, is_valid_region};
95
96/// ALPN protocol identifier for latency and availability probing.
97pub const ALPN_PROBE: &[u8] = b"cdn/probe/v1";
98
99/// ALPN protocol identifier for paid blob delivery (client-to-node and node-to-node).
100pub const ALPN_CLIENT: &[u8] = b"cdn/client/v1";
101
102/// ALPN protocol identifier for Kademlia-based content discovery (ADR 022).
103pub const ALPN_DHT: &[u8] = b"cdn/dht/v1";
104
105/// Connection rejected by the per-source or global rate limiter (ADR 013 §0x10).
106///
107/// Unlike `0x01`–`0x03`, this code is delivered via `CONNECTION_CLOSE` rather
108/// than `RESET_STREAM` because the rejection happens before any application
109/// stream is opened. The close-frame reason bytes carry a short layer label
110/// (e.g. `global-full`, `per-source`) so the peer can pick an appropriate
111/// backoff. Peers that receive this code SHOULD back off before
112/// reconnecting; they MUST NOT treat it as a protocol error (the server is
113/// functioning normally — the client is overloading it). The requester side
114/// (`decdn-client`) types this code as `UpstreamRateLimited` so the pull
115/// orchestrator suppresses the peer briefly instead of scoring it unreachable.
116pub const APP_ERR_RATE_LIMITED: u32 = 0x10;