1#![allow(
2 clippy::cast_possible_truncation,
3 clippy::cast_sign_loss,
4 reason = "M175: peer ID — fixed 20-byte BEP 3 client identifier"
5)]
6
7use crate::hash::Id20;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct PeerId(pub Id20);
15
16impl PeerId {
17 const PREFIX: &'static [u8] = b"-FE0100-";
19
20 #[must_use]
22 pub fn generate() -> Self {
23 Self::generate_with_prefix(Self::PREFIX)
24 }
25
26 #[must_use]
31 pub fn generate_anonymous() -> Self {
32 Self::generate_with_prefix(b"-XX0000-")
33 }
34
35 fn generate_with_prefix(prefix: &[u8]) -> Self {
37 let mut bytes = [0u8; 20];
38 bytes[..8].copy_from_slice(prefix);
39 for byte in &mut bytes[8..] {
40 *byte = random_byte();
41 }
42 Self(Id20(bytes))
43 }
44
45 #[must_use]
47 pub fn as_bytes(&self) -> &[u8; 20] {
48 self.0.as_bytes()
49 }
50
51 #[must_use]
53 pub fn prefix(&self) -> &[u8] {
54 &self.0.0[..8]
55 }
56}
57
58impl std::fmt::Display for PeerId {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 let prefix = std::str::from_utf8(&self.0.0[..8]).unwrap_or("????????");
62 let suffix = hex::encode(&self.0.0[8..]);
63 write!(f, "{prefix}{suffix}")
64 }
65}
66
67pub(crate) fn random_byte() -> u8 {
73 use std::cell::Cell;
74 use std::time::SystemTime;
75
76 thread_local! {
77 static STATE: Cell<u64> = Cell::new(
78 SystemTime::now()
79 .duration_since(SystemTime::UNIX_EPOCH)
80 .unwrap_or_default()
81 .as_nanos() as u64
82 );
83 }
84
85 STATE.with(|s| {
86 let next = crate::xorshift64_step(s.get().max(1));
87 s.set(next);
88 next as u8
89 })
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn peer_id_has_prefix() {
98 let id = PeerId::generate();
99 assert_eq!(id.prefix(), b"-FE0100-");
100 }
101
102 #[test]
103 fn peer_ids_are_unique() {
104 let a = PeerId::generate();
105 let b = PeerId::generate();
106 assert_ne!(a, b);
107 }
108
109 #[test]
110 fn anonymous_peer_id_has_generic_prefix() {
111 let id = PeerId::generate_anonymous();
112 assert_eq!(id.prefix(), b"-XX0000-");
113 }
114
115 #[test]
116 fn anonymous_peer_ids_are_unique() {
117 let a = PeerId::generate_anonymous();
118 let b = PeerId::generate_anonymous();
119 assert_ne!(a, b);
120 }
121
122 #[test]
123 fn peer_id_display() {
124 let id = PeerId::generate();
125 let s = format!("{id}");
126 assert!(s.starts_with("-FE0100-"));
127 assert_eq!(s.len(), 8 + 24); }
129
130 #[test]
137 fn random_byte_regression_uniform_distribution() {
138 let mut buf = [0u8; 1024];
139 for slot in &mut buf {
140 *slot = random_byte();
141 }
142 let nonzero = buf.iter().filter(|&&b| b != 0).count();
143 assert!(
144 nonzero >= 1000,
145 "≤4 non-zero bytes in 1024 samples is suspicious — got {nonzero} non-zero"
146 );
147 let mean: f64 = buf.iter().map(|&b| f64::from(b)).sum::<f64>() / 1024.0;
150 let var: f64 = buf
151 .iter()
152 .map(|&b| {
153 let d = f64::from(b) - mean;
154 d * d
155 })
156 .sum::<f64>()
157 / 1024.0;
158 let stddev = var.sqrt();
159 assert!(
160 (60.0..=80.0).contains(&stddev),
161 "stddev {stddev:.2} fell outside the [60.0, 80.0] uniform-distribution band"
162 );
163 }
164}