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;
20use url::{Host, Url};
21
22#[derive(
26 Clone,
27 Copy,
28 Debug,
29 Eq,
30 PartialEq,
31 Hash,
32 SerializeDisplay,
33 DeserializeFromStr,
34 RlpEncodable,
35 RlpDecodable,
36)]
37pub struct NodeRecord {
38 pub address: IpAddr,
40 pub udp_port: u16,
42 pub tcp_port: u16,
44 pub id: PeerId,
46}
47
48impl NodeRecord {
49 #[cfg(feature = "secp256k1")]
53 pub fn from_secret_key(addr: SocketAddr, sk: &secp256k1::SecretKey) -> Self {
54 let pk = secp256k1::PublicKey::from_secret_key(sk);
55 let id = PeerId::from_slice(&pk.serialize_uncompressed()[1..]);
56 Self::new(addr, id)
57 }
58
59 pub fn convert_ipv4_mapped(&mut self) -> bool {
66 if let IpAddr::V6(v6) = self.address &&
68 let Some(v4) = v6.to_ipv4_mapped()
69 {
70 self.address = v4.into();
71 return true
72 }
73 false
74 }
75
76 pub fn into_ipv4_mapped(mut self) -> Self {
78 self.convert_ipv4_mapped();
79 self
80 }
81
82 pub const fn with_tcp_port(mut self, port: u16) -> Self {
84 self.tcp_port = port;
85 self
86 }
87
88 pub const fn with_udp_port(mut self, port: u16) -> Self {
90 self.udp_port = port;
91 self
92 }
93
94 pub const fn new(addr: SocketAddr, id: PeerId) -> Self {
96 Self { address: addr.ip(), tcp_port: addr.port(), udp_port: addr.port(), id }
97 }
98
99 pub fn new_with_ports(
101 ip_addr: IpAddr,
102 tcp_port: u16,
103 udp_port: Option<u16>,
104 id: PeerId,
105 ) -> Self {
106 let udp_port = udp_port.unwrap_or(tcp_port);
107 Self { address: ip_addr, tcp_port, udp_port, id }
108 }
109
110 #[must_use]
112 pub const fn tcp_addr(&self) -> SocketAddr {
113 SocketAddr::new(self.address, self.tcp_port)
114 }
115
116 #[must_use]
118 pub const fn udp_addr(&self) -> SocketAddr {
119 SocketAddr::new(self.address, self.udp_port)
120 }
121
122 #[must_use]
126 pub const fn has_rlpx_endpoint(&self) -> bool {
127 self.tcp_port != 0
128 }
129}
130
131impl fmt::Display for NodeRecord {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 f.write_str("enode://")?;
134 alloy_primitives::hex::encode(self.id.as_slice()).fmt(f)?;
135 f.write_char('@')?;
136 match self.address {
137 IpAddr::V4(ip) => {
138 ip.fmt(f)?;
139 }
140 IpAddr::V6(ip) => {
141 f.write_char('[')?;
143 ip.fmt(f)?;
144 f.write_char(']')?;
145 }
146 }
147 f.write_char(':')?;
148 self.tcp_port.fmt(f)?;
149 if self.tcp_port != self.udp_port {
150 f.write_str("?discport=")?;
151 self.udp_port.fmt(f)?;
152 }
153
154 Ok(())
155 }
156}
157
158#[derive(Debug, thiserror::Error)]
160pub enum NodeRecordParseError {
161 #[error("Failed to parse url: {0}")]
163 InvalidUrl(String),
164 #[error("Failed to parse id")]
166 InvalidId(String),
167 #[error("Failed to discport query: {0}")]
169 Discport(ParseIntError),
170}
171
172impl FromStr for NodeRecord {
173 type Err = NodeRecordParseError;
174
175 fn from_str(s: &str) -> Result<Self, Self::Err> {
176 let url = Url::parse(s).map_err(|e| NodeRecordParseError::InvalidUrl(e.to_string()))?;
177
178 let address = match url.host() {
179 Some(Host::Ipv4(ip)) => IpAddr::V4(ip),
180 Some(Host::Ipv6(ip)) => IpAddr::V6(ip),
181 Some(Host::Domain(ip)) => IpAddr::V4(
182 Ipv4Addr::from_str(ip)
183 .map_err(|e| NodeRecordParseError::InvalidUrl(e.to_string()))?,
184 ),
185 _ => return Err(NodeRecordParseError::InvalidUrl(format!("invalid host: {url:?}"))),
186 };
187 let port = url
188 .port()
189 .ok_or_else(|| NodeRecordParseError::InvalidUrl("no port specified".to_string()))?;
190
191 let udp_port = if let Some(discovery_port) = url
192 .query_pairs()
193 .find_map(|(maybe_disc, port)| (maybe_disc.as_ref() == "discport").then_some(port))
194 {
195 discovery_port.parse::<u16>().map_err(NodeRecordParseError::Discport)?
196 } else {
197 port
198 };
199
200 let id = url
201 .username()
202 .parse::<PeerId>()
203 .map_err(|e| NodeRecordParseError::InvalidId(e.to_string()))?;
204
205 Ok(Self { address, id, tcp_port: port, udp_port })
206 }
207}
208
209#[cfg(feature = "secp256k1")]
210impl TryFrom<Enr<secp256k1::SecretKey>> for NodeRecord {
211 type Error = NodeRecordParseError;
212
213 fn try_from(enr: Enr<secp256k1::SecretKey>) -> Result<Self, Self::Error> {
214 (&enr).try_into()
215 }
216}
217
218#[cfg(feature = "secp256k1")]
219impl TryFrom<&Enr<secp256k1::SecretKey>> for NodeRecord {
220 type Error = NodeRecordParseError;
221
222 fn try_from(enr: &Enr<secp256k1::SecretKey>) -> Result<Self, Self::Error> {
223 let endpoint = enr
224 .ip4()
225 .zip(enr.udp4())
226 .map(|(ip, udp)| (IpAddr::from(ip), udp, enr.tcp4().unwrap_or(0)));
227 let (address, udp_port, tcp_port) = endpoint
230 .or_else(|| {
231 enr.ip6().zip(enr.udp6().or_else(|| enr.udp4())).map(|(ip, udp)| {
232 (IpAddr::from(ip), udp, enr.tcp6().or_else(|| enr.tcp4()).unwrap_or(0))
233 })
234 })
235 .ok_or_else(|| {
236 NodeRecordParseError::InvalidUrl("ip or matching udp port missing".to_string())
237 })?;
238
239 let id = crate::pk2id(&enr.public_key());
240 Ok(Self { address, udp_port, tcp_port, id }.into_ipv4_mapped())
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use alloy_primitives::b512;
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: b512!("0xfc8a2ff614e848c0af4c99372a81b8655edb8e11b617cffd0aab1a0691bcca66ca533626a528ee567f05f70c8cb529bda2c0a864cc0aec638a367fd2bb8e49fb"),
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: b512!("6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0"),
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: b512!("6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0"),
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: b512!("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
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: b512!("6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0"),
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: b512!("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
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_secret_bytes([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_secret_bytes([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}