1use crate::PeerId;
4use alloc::{
5 format,
6 string::{String, ToString},
7};
8use alloy_rlp::{RlpDecodable, RlpEncodable};
9use core::{
10 fmt,
11 fmt::Write,
12 net::{IpAddr, Ipv4Addr, SocketAddr},
13 num::ParseIntError,
14 str::FromStr,
15};
16use serde_with::{DeserializeFromStr, SerializeDisplay};
17
18#[cfg(feature = "secp256k1")]
19use enr::Enr;
20
21#[derive(
25 Clone,
26 Copy,
27 Debug,
28 Eq,
29 PartialEq,
30 Hash,
31 SerializeDisplay,
32 DeserializeFromStr,
33 RlpEncodable,
34 RlpDecodable,
35)]
36pub struct NodeRecord {
37 pub address: IpAddr,
39 pub udp_port: u16,
41 pub tcp_port: u16,
43 pub id: PeerId,
45}
46
47impl NodeRecord {
48 #[cfg(feature = "secp256k1")]
52 pub fn from_secret_key(addr: SocketAddr, sk: &secp256k1::SecretKey) -> Self {
53 let pk = secp256k1::PublicKey::from_secret_key(secp256k1::SECP256K1, sk);
54 let id = PeerId::from_slice(&pk.serialize_uncompressed()[1..]);
55 Self::new(addr, id)
56 }
57
58 pub fn convert_ipv4_mapped(&mut self) -> bool {
65 if let IpAddr::V6(v6) = self.address {
67 if let Some(v4) = v6.to_ipv4_mapped() {
68 self.address = v4.into();
69 return true
70 }
71 }
72 false
73 }
74
75 pub fn into_ipv4_mapped(mut self) -> Self {
77 self.convert_ipv4_mapped();
78 self
79 }
80
81 pub const fn with_tcp_port(mut self, port: u16) -> Self {
83 self.tcp_port = port;
84 self
85 }
86
87 pub const fn with_udp_port(mut self, port: u16) -> Self {
89 self.udp_port = port;
90 self
91 }
92
93 pub const fn new(addr: SocketAddr, id: PeerId) -> Self {
95 Self { address: addr.ip(), tcp_port: addr.port(), udp_port: addr.port(), id }
96 }
97
98 pub fn new_with_ports(
100 ip_addr: IpAddr,
101 tcp_port: u16,
102 udp_port: Option<u16>,
103 id: PeerId,
104 ) -> Self {
105 let udp_port = udp_port.unwrap_or(tcp_port);
106 Self { address: ip_addr, tcp_port, udp_port, id }
107 }
108
109 #[must_use]
111 pub const fn tcp_addr(&self) -> SocketAddr {
112 SocketAddr::new(self.address, self.tcp_port)
113 }
114
115 #[must_use]
117 pub const fn udp_addr(&self) -> SocketAddr {
118 SocketAddr::new(self.address, self.udp_port)
119 }
120}
121
122impl fmt::Display for NodeRecord {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 f.write_str("enode://")?;
125 alloy_primitives::hex::encode(self.id.as_slice()).fmt(f)?;
126 f.write_char('@')?;
127 match self.address {
128 IpAddr::V4(ip) => {
129 ip.fmt(f)?;
130 }
131 IpAddr::V6(ip) => {
132 f.write_char('[')?;
134 ip.fmt(f)?;
135 f.write_char(']')?;
136 }
137 }
138 f.write_char(':')?;
139 self.tcp_port.fmt(f)?;
140 if self.tcp_port != self.udp_port {
141 f.write_str("?discport=")?;
142 self.udp_port.fmt(f)?;
143 }
144
145 Ok(())
146 }
147}
148
149#[derive(Debug, thiserror::Error)]
151pub enum NodeRecordParseError {
152 #[error("Failed to parse url: {0}")]
154 InvalidUrl(String),
155 #[error("Failed to parse id")]
157 InvalidId(String),
158 #[error("Failed to discport query: {0}")]
160 Discport(ParseIntError),
161}
162
163impl FromStr for NodeRecord {
164 type Err = NodeRecordParseError;
165
166 fn from_str(s: &str) -> Result<Self, Self::Err> {
167 use url::{Host, Url};
168
169 let url = Url::parse(s).map_err(|e| NodeRecordParseError::InvalidUrl(e.to_string()))?;
170
171 let address = match url.host() {
172 Some(Host::Ipv4(ip)) => IpAddr::V4(ip),
173 Some(Host::Ipv6(ip)) => IpAddr::V6(ip),
174 Some(Host::Domain(ip)) => IpAddr::V4(
175 Ipv4Addr::from_str(ip)
176 .map_err(|e| NodeRecordParseError::InvalidUrl(e.to_string()))?,
177 ),
178 _ => return Err(NodeRecordParseError::InvalidUrl(format!("invalid host: {url:?}"))),
179 };
180 let port = url
181 .port()
182 .ok_or_else(|| NodeRecordParseError::InvalidUrl("no port specified".to_string()))?;
183
184 let udp_port = if let Some(discovery_port) = url
185 .query_pairs()
186 .find_map(|(maybe_disc, port)| (maybe_disc.as_ref() == "discport").then_some(port))
187 {
188 discovery_port.parse::<u16>().map_err(NodeRecordParseError::Discport)?
189 } else {
190 port
191 };
192
193 let id = url
194 .username()
195 .parse::<PeerId>()
196 .map_err(|e| NodeRecordParseError::InvalidId(e.to_string()))?;
197
198 Ok(Self { address, id, tcp_port: port, udp_port })
199 }
200}
201
202#[cfg(feature = "secp256k1")]
203impl TryFrom<Enr<secp256k1::SecretKey>> for NodeRecord {
204 type Error = NodeRecordParseError;
205
206 fn try_from(enr: Enr<secp256k1::SecretKey>) -> Result<Self, Self::Error> {
207 (&enr).try_into()
208 }
209}
210
211#[cfg(feature = "secp256k1")]
212impl TryFrom<&Enr<secp256k1::SecretKey>> for NodeRecord {
213 type Error = NodeRecordParseError;
214
215 fn try_from(enr: &Enr<secp256k1::SecretKey>) -> Result<Self, Self::Error> {
216 let Some(address) = enr.ip4().map(IpAddr::from).or_else(|| enr.ip6().map(IpAddr::from))
217 else {
218 return Err(NodeRecordParseError::InvalidUrl("ip missing".to_string()))
219 };
220
221 let Some(udp_port) = enr.udp4().or_else(|| enr.udp6()) else {
222 return Err(NodeRecordParseError::InvalidUrl("udp port missing".to_string()))
223 };
224
225 let Some(tcp_port) = enr.tcp4().or_else(|| enr.tcp6()) else {
226 return Err(NodeRecordParseError::InvalidUrl("tcp port missing".to_string()))
227 };
228
229 let id = crate::pk2id(&enr.public_key());
230
231 Ok(Self { address, tcp_port, udp_port, id }.into_ipv4_mapped())
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use alloy_rlp::Decodable;
239 use rand::{thread_rng, Rng, RngCore};
240 use std::net::Ipv6Addr;
241
242 #[test]
243 fn test_mapped_ipv6() {
244 let mut rng = thread_rng();
245
246 let v4: Ipv4Addr = "0.0.0.0".parse().unwrap();
247 let v6 = v4.to_ipv6_mapped();
248
249 let record = NodeRecord {
250 address: v6.into(),
251 tcp_port: rng.gen(),
252 udp_port: rng.gen(),
253 id: rng.gen(),
254 };
255
256 assert!(record.clone().convert_ipv4_mapped());
257 assert_eq!(record.into_ipv4_mapped().address, IpAddr::from(v4));
258 }
259
260 #[test]
261 fn test_mapped_ipv4() {
262 let mut rng = thread_rng();
263 let v4: Ipv4Addr = "0.0.0.0".parse().unwrap();
264
265 let record = NodeRecord {
266 address: v4.into(),
267 tcp_port: rng.gen(),
268 udp_port: rng.gen(),
269 id: rng.gen(),
270 };
271
272 assert!(!record.clone().convert_ipv4_mapped());
273 assert_eq!(record.into_ipv4_mapped().address, IpAddr::from(v4));
274 }
275
276 #[test]
277 fn test_noderecord_codec_ipv4() {
278 let mut rng = thread_rng();
279 for _ in 0..100 {
280 let mut ip = [0u8; 4];
281 rng.fill_bytes(&mut ip);
282 let record = NodeRecord {
283 address: IpAddr::V4(ip.into()),
284 tcp_port: rng.gen(),
285 udp_port: rng.gen(),
286 id: rng.gen(),
287 };
288
289 let decoded = NodeRecord::decode(&mut alloy_rlp::encode(record).as_slice()).unwrap();
290 assert_eq!(record, decoded);
291 }
292 }
293
294 #[test]
295 fn test_noderecord_codec_ipv6() {
296 let mut rng = thread_rng();
297 for _ in 0..100 {
298 let mut ip = [0u8; 16];
299 rng.fill_bytes(&mut ip);
300 let record = NodeRecord {
301 address: IpAddr::V6(ip.into()),
302 tcp_port: rng.gen(),
303 udp_port: rng.gen(),
304 id: rng.gen(),
305 };
306
307 let decoded = NodeRecord::decode(&mut alloy_rlp::encode(record).as_slice()).unwrap();
308 assert_eq!(record, decoded);
309 }
310 }
311
312 #[test]
313 fn test_url_parse() {
314 let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301";
315 let node: NodeRecord = url.parse().unwrap();
316 assert_eq!(node, NodeRecord {
317 address: IpAddr::V4([10,3,58,6].into()),
318 tcp_port: 30303,
319 udp_port: 30301,
320 id: "6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0".parse().unwrap(),
321 })
322 }
323
324 #[test]
325 fn test_node_display() {
326 let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303";
327 let node: NodeRecord = url.parse().unwrap();
328 assert_eq!(url, &format!("{node}"));
329 }
330
331 #[test]
332 fn test_node_display_discport() {
333 let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301";
334 let node: NodeRecord = url.parse().unwrap();
335 assert_eq!(url, &format!("{node}"));
336 }
337
338 #[test]
339 fn test_node_serialize() {
340 let cases = vec![
341 (
343 NodeRecord {
344 address: IpAddr::V4([10, 3, 58, 6].into()),
345 tcp_port: 30303u16,
346 udp_port: 30301u16,
347 id: PeerId::from_str("6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0").unwrap(),
348 },
349 "\"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301\""
350 ),
351 (
353 NodeRecord {
354 address: Ipv6Addr::new(0x2001, 0xdb8, 0x3c4d, 0x15, 0x0, 0x0, 0xabcd, 0xef12).into(),
355 tcp_port: 52150u16,
356 udp_port: 52151u16,
357 id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(),
358 },
359 "\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150?discport=52151\"",
360 )
361 ];
362
363 for (node, expected) in cases {
364 let ser = serde_json::to_string::<NodeRecord>(&node).expect("couldn't serialize");
365 assert_eq!(ser, expected);
366 }
367 }
368
369 #[test]
370 fn test_node_deserialize() {
371 let cases = vec![
372 (
374 "\"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301\"",
375 NodeRecord {
376 address: IpAddr::V4([10, 3, 58, 6].into()),
377 tcp_port: 30303u16,
378 udp_port: 30301u16,
379 id: PeerId::from_str("6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0").unwrap(),
380 }
381 ),
382 (
384 "\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150?discport=52151\"",
385 NodeRecord {
386 address: Ipv6Addr::new(0x2001, 0xdb8, 0x3c4d, 0x15, 0x0, 0x0, 0xabcd, 0xef12).into(),
387 tcp_port: 52150u16,
388 udp_port: 52151u16,
389 id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(),
390 }
391 ),
392 ];
393
394 for (url, expected) in cases {
395 let node: NodeRecord = serde_json::from_str(url).expect("couldn't deserialize");
396 assert_eq!(node, expected);
397 }
398 }
399}