Skip to main content

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;