jam-std-common 0.1.26

Common datatypes and utilities for the JAM nodes and tooling
Documentation
use crate::ed25519;
use codec::{Decode, Encode, MaxEncodedLen};

/// Identity of a network peer. This is just an Ed25519 public key. The corresponding secret key is
/// used to sign connection handshakes (the TLS protocol is used).
#[derive(Clone, Copy, Encode, Decode, MaxEncodedLen, Hash, PartialEq, Eq)]
pub struct PeerId(pub ed25519::Public);

impl PeerId {
	/// Returns the text form of the peer ID.
	pub fn to_text(&self) -> Text {
		self.into()
	}
}

impl From<ed25519::Public> for PeerId {
	fn from(pk: ed25519::Public) -> Self {
		Self(pk)
	}
}

impl core::fmt::Debug for PeerId {
	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
		f.pad(self.to_text().as_str())
	}
}

impl core::fmt::Display for PeerId {
	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
		f.pad(self.to_text().as_str())
	}
}

// Just lower-case letters and digits to allow the text forms of peer IDs to be used in many
// places. In particular we want them to be usable as DNS names in X.509 certificates.
const BITS_TO_CHAR: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";

const fn gen_char_to_bits() -> [u8; 256] {
	let mut char_to_bits = [255; 256];
	let mut i = 0;
	while i < BITS_TO_CHAR.len() {
		char_to_bits[BITS_TO_CHAR[i] as usize] = i as u8;
		i += 1;
	}
	char_to_bits
}

const CHAR_TO_BITS: [u8; 256] = gen_char_to_bits();

// In some places, identifiers beginning with a digit are not allowed, so the text form of a peer
// ID always begins with "e"
const TEXT_SIZE: usize = 1 + (ed25519::PUBLIC_LEN * 8).div_ceil(5);
/// Text form of a [`PeerId`]. Alphanumeric, beginning with a letter.
pub struct Text([u8; TEXT_SIZE]);

impl Text {
	/// Returns the text form as a `&str`.
	pub fn as_str(&self) -> &str {
		core::str::from_utf8(&self.0).expect("BITS_TO_CHAR only contains ASCII characters")
	}
}

impl From<&PeerId> for Text {
	fn from(peer_id: &PeerId) -> Self {
		let mut text = [0; TEXT_SIZE];
		text[0] = b'e';
		for (char, i) in core::iter::zip(&mut text[1..], (0..).step_by(5)) {
			let low = peer_id.0.as_bytes()[i / 8];
			let high = peer_id.0.as_bytes().get((i / 8) + 1).copied().unwrap_or(0);
			let window = (low as u16) | ((high as u16) << 8);
			let bits = (window >> (i % 8)) & 0x1f;
			*char = BITS_TO_CHAR[bits as usize];
		}
		Self(text)
	}
}

/// Error parsing the text form of a peer ID.
#[derive(Debug, thiserror::Error)]
pub enum ParseErr {
	#[error("Bad length {0}; peer IDs are always {TEXT_SIZE} characters long")]
	BadLength(usize),
	#[error("Bad prefix; peer IDs always begin with 'e'")]
	BadPrefix,
	#[error("Bad character; peer IDs only contain characters from the set {}",
		core::str::from_utf8(BITS_TO_CHAR).expect("BITS_TO_CHAR only contains ASCII characters"))]
	BadChar,
	#[error("Non-zero trailing bits")]
	NonZeroTrailingBits,
}

impl TryFrom<&str> for PeerId {
	type Error = ParseErr;

	fn try_from(text: &str) -> Result<Self, Self::Error> {
		let text = text.as_bytes();
		if text.len() != TEXT_SIZE {
			return Err(ParseErr::BadLength(text.len()));
		}
		if text[0] != b'e' {
			return Err(ParseErr::BadPrefix);
		}
		let mut public = [0; ed25519::PUBLIC_LEN];
		let mut i = 0;
		let mut acc_bits: u16 = 0;
		let mut num_acc_bits = 0;
		for char in &text[1..] {
			let bits = CHAR_TO_BITS[*char as usize];
			if bits > 0x1f {
				return Err(ParseErr::BadChar);
			}
			acc_bits |= (bits as u16) << num_acc_bits;
			num_acc_bits += 5;
			if num_acc_bits >= 8 {
				public[i] = acc_bits as u8;
				i += 1;
				acc_bits >>= 8;
				num_acc_bits -= 8;
			}
		}
		assert_eq!(i, ed25519::PUBLIC_LEN);
		if acc_bits != 0 {
			return Err(ParseErr::NonZeroTrailingBits);
		}
		Ok(Self(ed25519::Public::from(public)))
	}
}

impl core::str::FromStr for PeerId {
	type Err = ParseErr;

	fn from_str(text: &str) -> Result<Self, Self::Err> {
		text.try_into()
	}
}

#[cfg(any(test, feature = "rand"))]
impl rand::distr::Distribution<PeerId> for rand::distr::StandardUniform {
	fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> PeerId {
		PeerId(rng.random())
	}
}

#[cfg(test)]
mod tests {
	use super::*;
	use crate::ed25519::Secret;

	#[test]
	fn text_round_trip() {
		let mut rng = rand::rng();
		for _ in 0..1000 {
			let peer_id = PeerId(Secret::new(&mut rng).public());
			assert_eq!(peer_id, peer_id.to_text().as_str().try_into().unwrap());
		}
	}
}