Skip to main content

parley_core/
ids.rs

1//! Identifier newtypes.
2//!
3//! All identifiers are kept as strongly-typed newtypes to prevent accidental
4//! cross-use (a `ChannelId` cannot be passed where a `MessageId` is expected).
5//! Wire encoding for binary IDs is base64url without padding (RFC 4648 §5).
6
7use base64::engine::general_purpose::URL_SAFE_NO_PAD;
8use base64::Engine as _;
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10use std::fmt;
11use std::str::FromStr;
12
13use crate::error::CoreError;
14
15// ---------------------------------------------------------------------------
16// NetworkId
17// ---------------------------------------------------------------------------
18
19/// Network identifier, e.g. `"parley-mainnet"`. Format: `[a-z0-9-]{1,64}`,
20/// no leading or trailing hyphen. See spec §4.
21#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
22#[serde(transparent)]
23pub struct NetworkId(String);
24
25impl NetworkId {
26    pub fn new(s: impl Into<String>) -> Result<Self, CoreError> {
27        let s: String = s.into();
28        validate_network_id(&s)?;
29        Ok(Self(s))
30    }
31
32    pub fn as_str(&self) -> &str {
33        &self.0
34    }
35}
36
37impl fmt::Display for NetworkId {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        f.write_str(&self.0)
40    }
41}
42
43impl FromStr for NetworkId {
44    type Err = CoreError;
45    fn from_str(s: &str) -> Result<Self, Self::Err> {
46        Self::new(s)
47    }
48}
49
50fn validate_network_id(s: &str) -> Result<(), CoreError> {
51    let invalid = |reason: &str| CoreError::InvalidNetworkId(format!("{s:?}: {reason}"));
52    if s.is_empty() {
53        return Err(invalid("empty"));
54    }
55    if s.len() > 64 {
56        return Err(invalid("longer than 64 bytes"));
57    }
58    if s.starts_with('-') || s.ends_with('-') {
59        return Err(invalid("leading or trailing hyphen"));
60    }
61    if !s
62        .bytes()
63        .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
64    {
65        return Err(invalid("contains characters outside [a-z0-9-]"));
66    }
67    Ok(())
68}
69
70// ---------------------------------------------------------------------------
71// AgentPubkey
72// ---------------------------------------------------------------------------
73
74/// Ed25519 public key (32 bytes). Wire format is base64url-no-pad (43 chars).
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
76pub struct AgentPubkey([u8; 32]);
77
78impl AgentPubkey {
79    pub fn from_bytes(bytes: [u8; 32]) -> Self {
80        Self(bytes)
81    }
82
83    pub fn as_bytes(&self) -> &[u8; 32] {
84        &self.0
85    }
86}
87
88impl fmt::Display for AgentPubkey {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        f.write_str(&URL_SAFE_NO_PAD.encode(self.0))
91    }
92}
93
94impl FromStr for AgentPubkey {
95    type Err = CoreError;
96    fn from_str(s: &str) -> Result<Self, Self::Err> {
97        let bytes = URL_SAFE_NO_PAD.decode(s)?;
98        let arr: [u8; 32] = bytes.try_into().map_err(|v: Vec<u8>| {
99            CoreError::InvalidAgentPubkey(format!("expected 32 bytes, got {}", v.len()))
100        })?;
101        Ok(Self(arr))
102    }
103}
104
105impl Serialize for AgentPubkey {
106    fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
107        ser.serialize_str(&self.to_string())
108    }
109}
110
111impl<'de> Deserialize<'de> for AgentPubkey {
112    fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
113        let s = String::deserialize(de)?;
114        s.parse().map_err(serde::de::Error::custom)
115    }
116}
117
118// ---------------------------------------------------------------------------
119// ChannelId / MessageId — 16-byte opaque identifiers
120// ---------------------------------------------------------------------------
121
122macro_rules! opaque_id {
123    ($name:ident, $err_variant:ident, $ctx:literal) => {
124        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
125        pub struct $name([u8; 16]);
126
127        impl $name {
128            pub fn from_bytes(bytes: [u8; 16]) -> Self {
129                Self(bytes)
130            }
131
132            pub fn as_bytes(&self) -> &[u8; 16] {
133                &self.0
134            }
135
136            /// Generate a fresh random identifier.
137            pub fn generate() -> Self {
138                use rand::RngCore as _;
139                let mut bytes = [0u8; 16];
140                rand::thread_rng().fill_bytes(&mut bytes);
141                Self(bytes)
142            }
143        }
144
145        impl fmt::Display for $name {
146            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147                f.write_str(&URL_SAFE_NO_PAD.encode(self.0))
148            }
149        }
150
151        impl FromStr for $name {
152            type Err = CoreError;
153            fn from_str(s: &str) -> Result<Self, Self::Err> {
154                let bytes = URL_SAFE_NO_PAD.decode(s)?;
155                let arr: [u8; 16] = bytes.try_into().map_err(|v: Vec<u8>| {
156                    CoreError::$err_variant(format!(
157                        concat!($ctx, ": expected 16 bytes, got {}"),
158                        v.len()
159                    ))
160                })?;
161                Ok(Self(arr))
162            }
163        }
164
165        impl Serialize for $name {
166            fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
167                ser.serialize_str(&self.to_string())
168            }
169        }
170
171        impl<'de> Deserialize<'de> for $name {
172            fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
173                let s = String::deserialize(de)?;
174                s.parse().map_err(serde::de::Error::custom)
175            }
176        }
177    };
178}
179
180opaque_id!(ChannelId, InvalidChannelId, "channel_id");
181opaque_id!(MessageId, InvalidMessageId, "message_id");
182opaque_id!(BlobId, InvalidBlobId, "blob_id");
183
184// ---------------------------------------------------------------------------
185// Seq
186// ---------------------------------------------------------------------------
187
188/// Monotonic per-channel message sequence number. Starts at 1, dense (no gaps).
189#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
190#[serde(transparent)]
191pub struct Seq(pub u64);
192
193impl fmt::Display for Seq {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        self.0.fmt(f)
196    }
197}
198
199// ---------------------------------------------------------------------------
200// Nonce — 16 random bytes, base64url-no-pad on the wire
201// ---------------------------------------------------------------------------
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
204pub struct Nonce([u8; 16]);
205
206impl Nonce {
207    pub fn from_bytes(bytes: [u8; 16]) -> Self {
208        Self(bytes)
209    }
210
211    pub fn as_bytes(&self) -> &[u8; 16] {
212        &self.0
213    }
214
215    pub fn generate() -> Self {
216        use rand::RngCore as _;
217        let mut bytes = [0u8; 16];
218        rand::thread_rng().fill_bytes(&mut bytes);
219        Self(bytes)
220    }
221}
222
223impl fmt::Display for Nonce {
224    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225        f.write_str(&URL_SAFE_NO_PAD.encode(self.0))
226    }
227}
228
229impl FromStr for Nonce {
230    type Err = CoreError;
231    fn from_str(s: &str) -> Result<Self, Self::Err> {
232        let bytes = URL_SAFE_NO_PAD.decode(s)?;
233        let arr: [u8; 16] = bytes.try_into().map_err(|v: Vec<u8>| {
234            CoreError::InvalidNonce(format!("expected 16 bytes, got {}", v.len()))
235        })?;
236        Ok(Self(arr))
237    }
238}
239
240impl Serialize for Nonce {
241    fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
242        ser.serialize_str(&self.to_string())
243    }
244}
245
246impl<'de> Deserialize<'de> for Nonce {
247    fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
248        let s = String::deserialize(de)?;
249        s.parse().map_err(serde::de::Error::custom)
250    }
251}
252
253#[cfg(test)]
254#[allow(clippy::unwrap_used)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn network_id_validates() {
260        assert!(NetworkId::new("parley-mainnet").is_ok());
261        assert!(NetworkId::new("a").is_ok());
262        assert!(NetworkId::new("").is_err());
263        assert!(NetworkId::new("-leading").is_err());
264        assert!(NetworkId::new("trailing-").is_err());
265        assert!(NetworkId::new("UPPER").is_err());
266        assert!(NetworkId::new("under_score").is_err());
267        assert!(NetworkId::new("x".repeat(65)).is_err());
268    }
269
270    #[test]
271    fn channel_id_roundtrip() {
272        let id = ChannelId::generate();
273        let s = id.to_string();
274        assert_eq!(s.len(), 22);
275        let parsed: ChannelId = s.parse().unwrap();
276        assert_eq!(id, parsed);
277    }
278
279    #[test]
280    fn agent_pubkey_roundtrip() {
281        let pk = AgentPubkey::from_bytes([7u8; 32]);
282        let s = pk.to_string();
283        assert_eq!(s.len(), 43);
284        let parsed: AgentPubkey = s.parse().unwrap();
285        assert_eq!(pk, parsed);
286    }
287}