jam_std_common/net/
peer_id.rs

1use crate::ed25519;
2use codec::{Decode, Encode, MaxEncodedLen};
3
4/// Identity of a network peer. This is just an Ed25519 public key. The corresponding secret key is
5/// used to sign connection handshakes (the TLS protocol is used).
6#[derive(Clone, Copy, Encode, Decode, MaxEncodedLen, Hash, PartialEq, Eq)]
7pub struct PeerId(pub ed25519::Public);
8
9impl PeerId {
10	/// Returns the text form of the peer ID.
11	pub fn to_text(&self) -> Text {
12		self.into()
13	}
14}
15
16impl From<ed25519::Public> for PeerId {
17	fn from(pk: ed25519::Public) -> Self {
18		Self(pk)
19	}
20}
21
22impl core::fmt::Debug for PeerId {
23	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
24		f.pad(self.to_text().as_str())
25	}
26}
27
28impl core::fmt::Display for PeerId {
29	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
30		f.pad(self.to_text().as_str())
31	}
32}
33
34// Just lower-case letters and digits to allow the text forms of peer IDs to be used in many
35// places. In particular we want them to be usable as DNS names in X.509 certificates.
36const BITS_TO_CHAR: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
37
38const fn gen_char_to_bits() -> [u8; 256] {
39	let mut char_to_bits = [255; 256];
40	let mut i = 0;
41	while i < BITS_TO_CHAR.len() {
42		char_to_bits[BITS_TO_CHAR[i] as usize] = i as u8;
43		i += 1;
44	}
45	char_to_bits
46}
47
48const CHAR_TO_BITS: [u8; 256] = gen_char_to_bits();
49
50// In some places, identifiers beginning with a digit are not allowed, so the text form of a peer
51// ID always begins with "e"
52const TEXT_SIZE: usize = 1 + (ed25519::PUBLIC_LEN * 8).div_ceil(5);
53/// Text form of a [`PeerId`]. Alphanumeric, beginning with a letter.
54pub struct Text([u8; TEXT_SIZE]);
55
56impl Text {
57	/// Returns the text form as a `&str`.
58	pub fn as_str(&self) -> &str {
59		core::str::from_utf8(&self.0).expect("BITS_TO_CHAR only contains ASCII characters")
60	}
61}
62
63impl From<&PeerId> for Text {
64	fn from(peer_id: &PeerId) -> Self {
65		let mut text = [0; TEXT_SIZE];
66		text[0] = b'e';
67		for (char, i) in core::iter::zip(&mut text[1..], (0..).step_by(5)) {
68			let low = peer_id.0.as_bytes()[i / 8];
69			let high = peer_id.0.as_bytes().get((i / 8) + 1).copied().unwrap_or(0);
70			let window = (low as u16) | ((high as u16) << 8);
71			let bits = (window >> (i % 8)) & 0x1f;
72			*char = BITS_TO_CHAR[bits as usize];
73		}
74		Self(text)
75	}
76}
77
78/// Error parsing the text form of a peer ID.
79#[derive(Debug, thiserror::Error)]
80pub enum ParseErr {
81	#[error("Bad length {0}; peer IDs are always {TEXT_SIZE} characters long")]
82	BadLength(usize),
83	#[error("Bad prefix; peer IDs always begin with 'e'")]
84	BadPrefix,
85	#[error("Bad character; peer IDs only contain characters from the set {}",
86		core::str::from_utf8(BITS_TO_CHAR).expect("BITS_TO_CHAR only contains ASCII characters"))]
87	BadChar,
88	#[error("Non-zero trailing bits")]
89	NonZeroTrailingBits,
90}
91
92impl TryFrom<&str> for PeerId {
93	type Error = ParseErr;
94
95	fn try_from(text: &str) -> Result<Self, Self::Error> {
96		let text = text.as_bytes();
97		if text.len() != TEXT_SIZE {
98			return Err(ParseErr::BadLength(text.len()));
99		}
100		if text[0] != b'e' {
101			return Err(ParseErr::BadPrefix);
102		}
103		let mut public = [0; ed25519::PUBLIC_LEN];
104		let mut i = 0;
105		let mut acc_bits: u16 = 0;
106		let mut num_acc_bits = 0;
107		for char in &text[1..] {
108			let bits = CHAR_TO_BITS[*char as usize];
109			if bits > 0x1f {
110				return Err(ParseErr::BadChar);
111			}
112			acc_bits |= (bits as u16) << num_acc_bits;
113			num_acc_bits += 5;
114			if num_acc_bits >= 8 {
115				public[i] = acc_bits as u8;
116				i += 1;
117				acc_bits >>= 8;
118				num_acc_bits -= 8;
119			}
120		}
121		assert_eq!(i, ed25519::PUBLIC_LEN);
122		if acc_bits != 0 {
123			return Err(ParseErr::NonZeroTrailingBits);
124		}
125		Ok(Self(ed25519::Public::from(public)))
126	}
127}
128
129impl core::str::FromStr for PeerId {
130	type Err = ParseErr;
131
132	fn from_str(text: &str) -> Result<Self, Self::Err> {
133		text.try_into()
134	}
135}
136
137#[cfg(any(test, feature = "rand"))]
138impl rand::distr::Distribution<PeerId> for rand::distr::StandardUniform {
139	fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> PeerId {
140		PeerId(rng.random())
141	}
142}
143
144#[cfg(test)]
145mod tests {
146	use super::*;
147	use crate::ed25519::Secret;
148
149	#[test]
150	fn text_round_trip() {
151		let mut rng = rand::rng();
152		for _ in 0..1000 {
153			let peer_id = PeerId(Secret::new(&mut rng).public());
154			assert_eq!(peer_id, peer_id.to_text().as_str().try_into().unwrap());
155		}
156	}
157}