Skip to main content

irontide_core/
peer_id.rs

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/// A `BitTorrent` peer ID (20 bytes).
10///
11/// Uses Azureus-style encoding: `-FE0100-` followed by 12 random bytes.
12/// FE = Torrent (formerly Ferrite), 0100 = version 0.1.0.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct PeerId(pub Id20);
15
16impl PeerId {
17    /// Client identifier prefix.
18    const PREFIX: &'static [u8] = b"-FE0100-";
19
20    /// Generate a new random peer ID with the default Torrent prefix.
21    #[must_use]
22    pub fn generate() -> Self {
23        Self::generate_with_prefix(Self::PREFIX)
24    }
25
26    /// Generate an anonymous peer ID with a generic client prefix.
27    ///
28    /// Uses `-XX0000-` prefix (generic/unknown client) instead of `-FE0100-`
29    /// to avoid identifying the client software.
30    #[must_use]
31    pub fn generate_anonymous() -> Self {
32        Self::generate_with_prefix(b"-XX0000-")
33    }
34
35    /// Generate a peer ID with the given 8-byte Azureus-style prefix.
36    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    /// Return the raw 20 bytes.
46    #[must_use]
47    pub fn as_bytes(&self) -> &[u8; 20] {
48        self.0.as_bytes()
49    }
50
51    /// Return the client prefix (e.g., "-FE0100-").
52    #[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        // Show prefix as ASCII, rest as hex
61        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
67/// Simple random byte using thread-local state seeded from system time.
68///
69/// Backed by the shared [`crate::xorshift64_step`] helper to keep peer
70/// ID generation in lockstep with other in-tree consumers (sim
71/// per-link RNG state).
72pub(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); // 8 ASCII prefix + 12 bytes as hex
128    }
129
130    /// Regression guard for the [`random_byte`] refactor onto
131    /// [`crate::xorshift64_step`]. Fills a 1024-byte buffer with output
132    /// and asserts (a) at least one byte is non-zero and (b) the
133    /// stddev sits in the band a uniform distribution would produce.
134    /// Catches the failure mode where a re-implementation accidentally
135    /// loses entropy (e.g. by misordering the shifts).
136    #[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        // Empirical stddev for u8 uniform-ish is ~73.9; a uniform sample
148        // typically produces 60..80 over a 1024-sample window.
149        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}