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 let Some(v4) = v6.to_ipv4_mapped()
68 {
69 self.address = v4.into();
70 return true
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 #[must_use]
125 pub const fn has_rlpx_endpoint(&self) -> bool {
126 self.tcp_port != 0
127 }
128}
129
130impl fmt::Display for NodeRecord {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 f.write_str("enode://")?;
133 alloy_primitives::hex::encode(self.id.as_slice()).fmt(f)?;
134 f.write_char('@')?;
135 match self.address {
136 IpAddr::V4(ip) => {
137 ip.fmt(f)?;
138 }
139 IpAddr::V6(ip) => {
140 f.write_char('[')?;
142 ip.fmt(f)?;
143 f.write_char(']')?;
144 }
145 }
146 f.write_char(':')?;
147 self.tcp_port.fmt(f)?;
148 if self.tcp_port != self.udp_port {
149 f.write_str("?discport=")?;
150 self.udp_port.fmt(f)?;
151 }
152
153 Ok(())
154 }
155}
156
157#[derive(Debug, thiserror::Error)]
159pub enum NodeRecordParseError {
160 #[error("Failed to parse url: {0}")]
162 InvalidUrl(String),
163 #[error("Failed to parse id")]
165 InvalidId(String),
166 #[error("Failed to discport query: {0}")]
168 Discport(ParseIntError),
169}
170
171impl FromStr for NodeRecord {
172 type Err = NodeRecordParseError;
173
174 fn from_str(s: &str) -> Result<Self, Self::Err> {
175 use url::{Host, Url};
176
177 let url = Url::parse(s).map_err(|e| NodeRecordParseError::InvalidUrl(e.to_string()))?;
178
179 let address = match url.host() {
180 Some(Host::Ipv4(ip)) => IpAddr::V4(ip),
181 Some(Host::Ipv6(ip)) => IpAddr::V6(ip),
182 Some(Host::Domain(ip)) => IpAddr::V4(
183 Ipv4Addr::from_str(ip)
184 .map_err(|e| NodeRecordParseError::InvalidUrl(e.to_string()))?,
185 ),
186 _ => return Err(NodeRecordParseError::InvalidUrl(format!("invalid host: {url:?}"))),
187 };
188 let port = url
189 .port()
190 .ok_or_else(|| NodeRecordParseError::InvalidUrl("no port specified".to_string()))?;
191
192 let udp_port = if let Some(discovery_port) = url
193 .query_pairs()
194 .find_map(|(maybe_disc, port)| (maybe_disc.as_ref() == "discport").then_some(port))
195 {
196 discovery_port.parse::<u16>().map_err(NodeRecordParseError::Discport)?
197 } else {
198 port
199 };
200
201 let id = url
202 .username()
203 .parse::<PeerId>()
204 .map_err(|e| NodeRecordParseError::InvalidId(e.to_string()))?;
205
206 Ok(Self { address, id, tcp_port: port, udp_port })
207 }
208}
209
210#[cfg(feature = "secp256k1")]
211impl TryFrom<Enr<secp256k1::SecretKey>> for NodeRecord {
212 type Error = NodeRecordParseError;
213
214 fn try_from(enr: Enr<secp256k1::SecretKey>) -> Result<Self, Self::Error> {
215 (&enr).try_into()
216 }
217}
218
219#[cfg(feature = "secp256k1")]
220impl TryFrom<&Enr<secp256k1::SecretKey>> for NodeRecord {
221 type Error = NodeRecordParseError;
222
223 fn try_from(enr: &Enr<secp256k1::SecretKey>) -> Result<Self, Self::Error> {
224 let endpoint = enr
225 .ip4()
226 .zip(enr.udp4())
227 .map(|(ip, udp)| (IpAddr::from(ip), udp, enr.tcp4().unwrap_or(0)));
228 let (address, udp_port, tcp_port) = endpoint
231 .or_else(|| {
232 enr.ip6().zip(enr.udp6().or_else(|| enr.udp4())).map(|(ip, udp)| {
233 (IpAddr::from(ip), udp, enr.tcp6().or_else(|| enr.tcp4()).unwrap_or(0))
234 })
235 })
236 .ok_or_else(|| {
237 NodeRecordParseError::InvalidUrl("ip or matching udp port missing".to_string())
238 })?;
239
240 let id = crate::pk2id(&enr.public_key());
241 Ok(Self { address, udp_port, tcp_port, id }.into_ipv4_mapped())
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use alloy_rlp::Decodable;
249 use rand::{rng, Rng, RngCore};
250 use std::net::Ipv6Addr;
251
252 #[test]
253 fn test_mapped_ipv6() {
254 let mut rng = rng();
255
256 let v4: Ipv4Addr = "0.0.0.0".parse().unwrap();
257 let v6 = v4.to_ipv6_mapped();
258
259 let record = NodeRecord {
260 address: v6.into(),
261 tcp_port: rng.random(),
262 udp_port: rng.random(),
263 id: rng.random(),
264 };
265
266 assert!(record.clone().convert_ipv4_mapped());
267 assert_eq!(record.into_ipv4_mapped().address, IpAddr::from(v4));
268 }
269
270 #[test]
271 fn test_mapped_ipv4() {
272 let mut rng = rng();
273 let v4: Ipv4Addr = "0.0.0.0".parse().unwrap();
274
275 let record = NodeRecord {
276 address: v4.into(),
277 tcp_port: rng.random(),
278 udp_port: rng.random(),
279 id: rng.random(),
280 };
281
282 assert!(!record.clone().convert_ipv4_mapped());
283 assert_eq!(record.into_ipv4_mapped().address, IpAddr::from(v4));
284 }
285
286 #[test]
287 fn test_noderecord_codec_ipv4() {
288 let mut rng = rng();
289 for _ in 0..100 {
290 let mut ip = [0u8; 4];
291 rng.fill_bytes(&mut ip);
292 let record = NodeRecord {
293 address: IpAddr::V4(ip.into()),
294 tcp_port: rng.random(),
295 udp_port: rng.random(),
296 id: rng.random(),
297 };
298
299 let decoded = NodeRecord::decode(&mut alloy_rlp::encode(record).as_slice()).unwrap();
300 assert_eq!(record, decoded);
301 }
302 }
303
304 #[test]
305 fn test_noderecord_codec_ipv6() {
306 let mut rng = rng();
307 for _ in 0..100 {
308 let mut ip = [0u8; 16];
309 rng.fill_bytes(&mut ip);
310 let record = NodeRecord {
311 address: IpAddr::V6(ip.into()),
312 tcp_port: rng.random(),
313 udp_port: rng.random(),
314 id: rng.random(),
315 };
316
317 let decoded = NodeRecord::decode(&mut alloy_rlp::encode(record).as_slice()).unwrap();
318 assert_eq!(record, decoded);
319 }
320 }
321
322 #[test]
323 fn test_node_record() {
324 let url = "enode://fc8a2ff614e848c0af4c99372a81b8655edb8e11b617cffd0aab1a0691bcca66ca533626a528ee567f05f70c8cb529bda2c0a864cc0aec638a367fd2bb8e49fb@127.0.0.1:35481?discport=0";
325 let node: NodeRecord = url.parse().unwrap();
326 assert_eq!(node, NodeRecord {
327 address: IpAddr::V4([127,0,0, 1].into()),
328 tcp_port: 35481,
329 udp_port: 0,
330 id: "0xfc8a2ff614e848c0af4c99372a81b8655edb8e11b617cffd0aab1a0691bcca66ca533626a528ee567f05f70c8cb529bda2c0a864cc0aec638a367fd2bb8e49fb".parse().unwrap(),
331 })
332 }
333
334 #[test]
335 fn test_url_parse() {
336 let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301";
337 let node: NodeRecord = url.parse().unwrap();
338 assert_eq!(node, NodeRecord {
339 address: IpAddr::V4([10,3,58,6].into()),
340 tcp_port: 30303,
341 udp_port: 30301,
342 id: "6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0".parse().unwrap(),
343 })
344 }
345
346 #[test]
347 fn test_node_display() {
348 let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303";
349 let node: NodeRecord = url.parse().unwrap();
350 assert_eq!(url, &format!("{node}"));
351 }
352
353 #[test]
354 fn test_node_display_discport() {
355 let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301";
356 let node: NodeRecord = url.parse().unwrap();
357 assert_eq!(url, &format!("{node}"));
358 }
359
360 #[test]
361 fn test_node_serialize() {
362 let cases = vec![
363 (
365 NodeRecord {
366 address: IpAddr::V4([10, 3, 58, 6].into()),
367 tcp_port: 30303u16,
368 udp_port: 30301u16,
369 id: PeerId::from_str("6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0").unwrap(),
370 },
371 "\"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301\""
372 ),
373 (
375 NodeRecord {
376 address: Ipv6Addr::new(0x2001, 0xdb8, 0x3c4d, 0x15, 0x0, 0x0, 0xabcd, 0xef12).into(),
377 tcp_port: 52150u16,
378 udp_port: 52151u16,
379 id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(),
380 },
381 "\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150?discport=52151\"",
382 )
383 ];
384
385 for (node, expected) in cases {
386 let ser = serde_json::to_string::<NodeRecord>(&node).expect("couldn't serialize");
387 assert_eq!(ser, expected);
388 }
389 }
390
391 #[test]
392 fn test_node_deserialize() {
393 let cases = vec![
394 (
396 "\"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301\"",
397 NodeRecord {
398 address: IpAddr::V4([10, 3, 58, 6].into()),
399 tcp_port: 30303u16,
400 udp_port: 30301u16,
401 id: PeerId::from_str("6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0").unwrap(),
402 }
403 ),
404 (
406 "\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150?discport=52151\"",
407 NodeRecord {
408 address: Ipv6Addr::new(0x2001, 0xdb8, 0x3c4d, 0x15, 0x0, 0x0, 0xabcd, 0xef12).into(),
409 tcp_port: 52150u16,
410 udp_port: 52151u16,
411 id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(),
412 }
413 ),
414 ];
415
416 for (url, expected) in cases {
417 let node: NodeRecord = serde_json::from_str(url).expect("couldn't deserialize");
418 assert_eq!(node, expected);
419 }
420 }
421
422 #[test]
423 #[cfg(feature = "secp256k1")]
424 fn tcp_less_enr_converts_with_port_zero() {
425 let sk = secp256k1::SecretKey::from_byte_array(&[1u8; 32]).unwrap();
427 let enr = Enr::builder().ip4("1.2.3.4".parse().unwrap()).udp4(30301).build(&sk).unwrap();
428
429 let record = NodeRecord::try_from(&enr).unwrap();
430
431 assert_eq!(record.tcp_port, 0);
432 assert_eq!(record.udp_port, 30301);
433 }
434
435 #[test]
436 #[cfg(feature = "secp256k1")]
437 fn enr_endpoint_keeps_ip_and_ports_together() {
438 let sk = secp256k1::SecretKey::from_byte_array(&[1u8; 32]).unwrap();
439 let mut builder = Enr::builder();
440 builder.ip4("10.0.0.1".parse().unwrap()).ip6("::1".parse().unwrap());
441 builder.udp6(30401).tcp6(30403);
442 let enr = builder.build(&sk).unwrap();
443 let record = NodeRecord::try_from(&enr).unwrap();
444
445 assert_eq!(record.address, "::1".parse::<IpAddr>().unwrap());
446 assert_eq!(record.udp_port, 30401);
447 assert_eq!(record.tcp_port, 30403);
448
449 let mut builder = Enr::builder();
452 builder.ip6("::1".parse().unwrap()).udp6(30401).tcp4(30303);
453 let enr = builder.build(&sk).unwrap();
454 let record = NodeRecord::try_from(&enr).unwrap();
455
456 assert_eq!(record.address, "::1".parse::<IpAddr>().unwrap());
457 assert_eq!(record.udp_port, 30401);
458 assert_eq!(record.tcp_port, 30303);
459
460 let mut builder = Enr::builder();
462 builder.ip4("10.0.0.1".parse().unwrap()).udp4(30301).tcp4(30303);
463 builder.ip6("::1".parse().unwrap()).udp6(30401).tcp6(30403);
464 let enr = builder.build(&sk).unwrap();
465 let record = NodeRecord::try_from(&enr).unwrap();
466
467 assert_eq!(record.address, "10.0.0.1".parse::<IpAddr>().unwrap());
468 assert_eq!(record.udp_port, 30301);
469 assert_eq!(record.tcp_port, 30303);
470
471 let enr = Enr::builder().ip4("10.0.0.1".parse().unwrap()).tcp4(30303).build(&sk).unwrap();
473 assert!(NodeRecord::try_from(&enr).is_err());
474 }
475}