Skip to main content

jam_std_common/net/
peer_id.rs

1use crate::ed25519;
2use codec::{ConstEncodedLen, Decode, Encode, MaxEncodedLen};
3use std::io::Write;
4
5// blake2b("jam_anonymous_peer_id")[8..]
6const ANONYMOUS_PEER_ID_SUFFIX: [u8; 24] = [
7	0x96, 0x29, 0x01, 0x7b, 0x62, 0xef, 0x4c, 0x58, 0x58, 0x0a, 0x92, 0x32, 0x15, 0x46, 0x82, 0x0d,
8	0xee, 0x7e, 0xa0, 0xde, 0x28, 0x44, 0x16, 0xcd,
9];
10
11/// Identity of a network peer.
12///
13/// This is just an Ed25519 public key. When forming a connection, each peer presents its identity
14/// in an X.509 certificate. The corresponding secret key is used to sign the connection handshake
15/// (the TLS protocol is used).
16///
17/// Note that the peer initiating a connection needn't provide a certificate. In this case their
18/// identity is not known. For convenience, a tiny portion of the ID space is reserved for such
19/// anonymous peers. A mapping is provided between `u64` and this space. It is assumed to be
20/// practically impossible to generate a secret key whose public key lies in this space.
21#[derive(Clone, Copy, Encode, Decode, MaxEncodedLen, Hash, PartialEq, Eq)]
22pub struct PeerId(pub ed25519::Public);
23
24impl ConstEncodedLen for PeerId {}
25
26impl PeerId {
27	/// Returns the "anonymous" peer ID with the given index.
28	pub fn anonymous(index: u64) -> Self {
29		let mut public = [0; ed25519::PUBLIC_LEN];
30		public[..8].copy_from_slice(&index.to_le_bytes());
31		public[8..].copy_from_slice(&ANONYMOUS_PEER_ID_SUFFIX);
32		Self(public.into())
33	}
34
35	/// Returns `true` if the peer ID is in the space reserved for anonymous peers.
36	pub fn is_anonymous(&self) -> bool {
37		self.0.as_bytes()[8..] == ANONYMOUS_PEER_ID_SUFFIX
38	}
39
40	/// Returns `Some(index)` if the peer ID is in the space reserved for anonymous peers.
41	pub fn anonymous_index(&self) -> Option<u64> {
42		if self.is_anonymous() {
43			Some(u64::from_le_bytes(
44				*self.0.as_bytes()[..8].as_array().expect("Slice has correct length"),
45			))
46		} else {
47			None
48		}
49	}
50
51	/// Returns the text form of the peer ID.
52	pub fn to_text(&self) -> Text {
53		self.into()
54	}
55}
56
57impl From<ed25519::Public> for PeerId {
58	fn from(pk: ed25519::Public) -> Self {
59		Self(pk)
60	}
61}
62
63impl core::fmt::Debug for PeerId {
64	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
65		f.pad(self.to_text().as_str())
66	}
67}
68
69impl core::fmt::Display for PeerId {
70	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
71		f.pad(self.to_text().as_str())
72	}
73}
74
75/// ECDSA P-256 identity of a network peer.
76///
77/// A `P256PeerId` is essentially the same as a `PeerId` but with an ECDSA P-256 public key instead
78/// of an Ed25519 public key. This exists primarily to support WebTransport; at least as of June
79/// 2026, most browsers do not support using Ed25519 for TLS handshake signing.
80///
81/// Note that unlike `PeerId` no portion of the ID space is reserved for anonymous peers.
82#[derive(Clone, Copy, Encode, Decode, MaxEncodedLen, PartialEq, Eq)]
83pub struct P256PeerId {
84	pub y_odd: bool,
85	pub x: [u8; 32],
86}
87
88impl ConstEncodedLen for P256PeerId {}
89
90impl P256PeerId {
91	pub const ZERO: Self = Self { y_odd: false, x: [0; 32] };
92
93	pub fn is_zero(&self) -> bool {
94		self == &Self::ZERO
95	}
96
97	/// Returns the text form of the peer ID.
98	pub fn to_text(&self) -> Text {
99		self.into()
100	}
101}
102
103impl From<&p256::ecdsa::VerifyingKey> for P256PeerId {
104	fn from(pk: &p256::ecdsa::VerifyingKey) -> Self {
105		let point = pk.to_encoded_point(true);
106		use p256::elliptic_curve::sec1::Tag;
107		let y_odd = match point.tag() {
108			Tag::CompressedEvenY => false,
109			Tag::CompressedOddY => true,
110			_ => unreachable!(),
111		};
112		let x = point.x().expect("Verifying key should never be the identity point!");
113		Self { y_odd, x: (*x).into() }
114	}
115}
116
117impl core::fmt::Debug for P256PeerId {
118	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
119		f.pad(self.to_text().as_str())
120	}
121}
122
123impl core::fmt::Display for P256PeerId {
124	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
125		f.pad(self.to_text().as_str())
126	}
127}
128
129// Just lower-case letters and digits to allow the text forms of peer IDs to be used in many
130// places. In particular we want them to be usable as DNS names in X.509 certificates.
131const BITS_TO_CHAR: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
132
133const fn gen_char_to_bits() -> [u8; 256] {
134	let mut char_to_bits = [255; 256];
135	let mut i = 0;
136	while i < BITS_TO_CHAR.len() {
137		char_to_bits[BITS_TO_CHAR[i] as usize] = i as u8;
138		i += 1;
139	}
140	char_to_bits
141}
142
143const CHAR_TO_BITS: [u8; 256] = gen_char_to_bits();
144
145// The text form of an Ed25519 peer ID begins with "e" and the text form of an ECDSA P-256 peer ID
146// begins with "o" or "v". 32 bytes follow, base-32 encoded.
147const TEXT_SIZE: usize = 1 + 256_usize.div_ceil(5);
148/// Text form of a [`PeerId`] or [`P256PeerId`]. Alphanumeric, beginning with a letter.
149pub struct Text([u8; TEXT_SIZE]);
150
151impl Text {
152	fn new(prefix: u8, bytes: &[u8; 32]) -> Self {
153		let mut text = [0; TEXT_SIZE];
154		text[0] = prefix;
155		for (char, i) in core::iter::zip(&mut text[1..], (0..).step_by(5)) {
156			let low = bytes[i / 8];
157			let high = bytes.get((i / 8) + 1).copied().unwrap_or(0);
158			let window = (low as u16) | ((high as u16) << 8);
159			let bits = (window >> (i % 8)) & 0x1f;
160			*char = BITS_TO_CHAR[bits as usize];
161		}
162		Self(text)
163	}
164
165	/// Returns the text form as a `&str`.
166	pub fn as_str(&self) -> &str {
167		core::str::from_utf8(&self.0).expect("Only contains ASCII characters")
168	}
169}
170
171impl From<&PeerId> for Text {
172	fn from(peer_id: &PeerId) -> Self {
173		if let Some(index) = peer_id.anonymous_index() {
174			let mut text = [0; TEXT_SIZE];
175			let mut w = text.as_mut_slice();
176			write!(w, "anonymous{index:044}").expect("Buffer is exactly big enough");
177			assert!(w.is_empty());
178			Self(text)
179		} else {
180			Self::new(b'e', peer_id.0.as_bytes())
181		}
182	}
183}
184
185impl From<&P256PeerId> for Text {
186	fn from(peer_id: &P256PeerId) -> Self {
187		let prefix = if peer_id.y_odd { b'o' } else { b'v' };
188		Self::new(prefix, &peer_id.x)
189	}
190}
191
192/// Error parsing the text form of a peer ID.
193#[derive(Debug, thiserror::Error)]
194pub enum ParseErr {
195	#[error("Bad length {0}; peer IDs are always {TEXT_SIZE} characters long")]
196	BadLength(usize),
197	#[error("ECDSA P-256 peer IDs (beginning with 'o' or 'v') are not supported")]
198	P256Prefix,
199	#[error("Bad prefix; peer IDs always begin with 'e'")]
200	BadPrefix,
201	#[error("Bad character; peer IDs only contain characters from the set {}",
202		core::str::from_utf8(BITS_TO_CHAR).expect("BITS_TO_CHAR only contains ASCII characters"))]
203	BadChar,
204	#[error("Non-zero trailing bits")]
205	NonZeroTrailingBits,
206}
207
208impl TryFrom<&str> for PeerId {
209	type Error = ParseErr;
210
211	fn try_from(text: &str) -> Result<Self, Self::Error> {
212		if !text.is_ascii() {
213			return Err(ParseErr::BadChar)
214		}
215		let text = text.as_bytes();
216		if text.len() != TEXT_SIZE {
217			return Err(ParseErr::BadLength(text.len()));
218		}
219		if (text[0] == b'o') || (text[0] == b'v') {
220			return Err(ParseErr::P256Prefix);
221		}
222		// We intentionally do not support parsing "anonymous" peer IDs here. They generally do not
223		// make sense in places where a peer ID must be provided.
224		if text[0] != b'e' {
225			return Err(ParseErr::BadPrefix);
226		}
227		let mut public = [0; ed25519::PUBLIC_LEN];
228		let mut i = 0;
229		let mut acc_bits: u16 = 0;
230		let mut num_acc_bits = 0;
231		for char in &text[1..] {
232			let bits = CHAR_TO_BITS[*char as usize];
233			if bits > 0x1f {
234				return Err(ParseErr::BadChar);
235			}
236			acc_bits |= (bits as u16) << num_acc_bits;
237			num_acc_bits += 5;
238			if num_acc_bits >= 8 {
239				public[i] = acc_bits as u8;
240				i += 1;
241				acc_bits >>= 8;
242				num_acc_bits -= 8;
243			}
244		}
245		assert_eq!(i, ed25519::PUBLIC_LEN);
246		if acc_bits != 0 {
247			return Err(ParseErr::NonZeroTrailingBits);
248		}
249		Ok(Self(ed25519::Public::from(public)))
250	}
251}
252
253impl core::str::FromStr for PeerId {
254	type Err = ParseErr;
255
256	fn from_str(text: &str) -> Result<Self, Self::Err> {
257		text.try_into()
258	}
259}
260
261#[cfg(any(test, feature = "rand"))]
262impl rand::distr::Distribution<PeerId> for rand::distr::StandardUniform {
263	fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> PeerId {
264		PeerId(rng.random())
265	}
266}
267
268#[cfg(any(test, feature = "rand"))]
269impl rand::distr::Distribution<P256PeerId> for rand::distr::StandardUniform {
270	fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> P256PeerId {
271		P256PeerId { y_odd: rng.random(), x: rng.random() }
272	}
273}
274
275#[cfg(test)]
276mod tests {
277	use super::*;
278	use crate::ed25519::Secret;
279	use rand::Rng;
280
281	#[test]
282	fn text_round_trip() {
283		let mut rng = rand::rng();
284		for _ in 0..1000 {
285			let peer_id = PeerId(Secret::new(&mut rng).public());
286			assert_eq!(peer_id, peer_id.to_text().as_str().try_into().unwrap());
287		}
288	}
289
290	#[test]
291	fn anonymous_text() {
292		assert_eq!(
293			PeerId::anonymous(1234).to_text().as_str(),
294			"anonymous00000000000000000000000000000000000000001234"
295		);
296	}
297
298	#[test]
299	fn anonymous_round_trip() {
300		let mut rng = rand::rng();
301		for _ in 0..100 {
302			let index: u64 = rng.random();
303			assert_eq!(PeerId::anonymous(index).anonymous_index(), Some(index));
304		}
305	}
306
307	#[test]
308	fn non_anonymous() {
309		let mut rng = rand::rng();
310		for _ in 0..100 {
311			let peer_id = PeerId(Secret::new(&mut rng).public());
312			assert_eq!(peer_id.anonymous_index(), None);
313		}
314	}
315}