decdn_protocol/identity.rs
1//! Strongly-typed 32-byte identity newtypes for the deCDN wire protocol.
2//!
3//! [`NodeId`] (a 32-byte Ed25519 public key) and [`ContentHash`] (a 32-byte
4//! BLAKE3 content address) are distinct types even though both wrap `[u8; 32]`.
5//! Keeping them distinct turns a class of confusion — passing a content hash
6//! where a node id is expected, or treating a wire-provided id as an
7//! authenticated one — into a **compile error** rather than a silent logic bug
8//! (see the routing-table-poisoning class behind ADR 022 §STORE Flow).
9//!
10//! # Wire compatibility
11//!
12//! Both newtypes are `#[repr(transparent)]` + `#[serde(transparent)]`, so they
13//! lay out and (de)serialize byte-for-byte identically to a bare `[u8; 32]`.
14//! Postcard sees the same 32 raw bytes — no discriminant moves, no ALPN bump.
15//! `nodeid_postcard_identical_to_array` pins this invariant.
16//!
17//! # Trust boundary
18//!
19//! A plain [`NodeId`] is freely constructible from wire bytes *by design*:
20//! Kademlia legitimately learns peer ids from `FindNode` responses and inserts
21//! them as probe candidates. The authenticated-identity boundary is enforced
22//! one layer up by `decdn_node::dht::AuthenticatedNodeId`, which can only be
23//! minted from a live QUIC connection's `remote_id()`.
24
25use serde::{Deserialize, Serialize};
26
27/// Length in bytes of a [`NodeId`] or [`ContentHash`] — the size of both an
28/// Ed25519 public key and a BLAKE3 digest.
29pub const ID_LEN: usize = 32;
30
31/// A 32-byte node identifier (Ed25519 public key).
32///
33/// Distinct from [`ContentHash`] at the type level; identical on the wire.
34#[repr(transparent)]
35#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
36#[serde(transparent)]
37pub struct NodeId([u8; ID_LEN]);
38
39/// A 32-byte content address (BLAKE3 hash).
40///
41/// Distinct from [`NodeId`] at the type level; identical on the wire. Named
42/// `ContentHash` (not `Hash`) to avoid colliding with `decdn_config_types::Hash`
43/// and the `std::hash::Hash` trait.
44#[repr(transparent)]
45#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
46#[serde(transparent)]
47pub struct ContentHash([u8; ID_LEN]);
48
49macro_rules! id_newtype_impls {
50 ($t:ty) => {
51 impl $t {
52 /// Wrap raw bytes. `const` to keep the newtype usable in const
53 /// contexts (and consistent with the sibling accessors below).
54 #[must_use]
55 pub const fn from_bytes(bytes: [u8; ID_LEN]) -> Self {
56 Self(bytes)
57 }
58
59 /// Borrow the inner bytes. The explicit accessor (rather than a
60 /// `Deref<Target = [u8; ID_LEN]>`) keeps panicking slice indexing
61 /// off the type, which the workspace `indexing_slicing` lint forbids.
62 #[must_use]
63 pub const fn as_bytes(&self) -> &[u8; ID_LEN] {
64 &self.0
65 }
66
67 /// Consume into the inner bytes.
68 #[must_use]
69 pub const fn to_bytes(self) -> [u8; ID_LEN] {
70 self.0
71 }
72 }
73
74 impl From<[u8; ID_LEN]> for $t {
75 fn from(bytes: [u8; ID_LEN]) -> Self {
76 Self(bytes)
77 }
78 }
79
80 impl From<$t> for [u8; ID_LEN] {
81 fn from(value: $t) -> Self {
82 value.0
83 }
84 }
85
86 /// Lowercase hex with no prefix — the same rendering as
87 /// `iroh::PublicKey` and `iroh_blobs::Hash`, so a log or span field
88 /// written from this type matches one written from the iroh type.
89 impl std::fmt::Display for $t {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 self.0.iter().try_for_each(|b| write!(f, "{b:02x}"))
92 }
93 }
94 };
95}
96
97id_newtype_impls!(NodeId);
98id_newtype_impls!(ContentHash);
99
100#[cfg(test)]
101#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
102mod tests;