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_primitives::b512;
249 use alloy_rlp::Decodable;
250 use rand::{rng, Rng, RngCore};
251 use std::net::Ipv6Addr;
252
253 #[test]
254 fn test_mapped_ipv6() {
255 let mut rng = rng();
256
257 let v4: Ipv4Addr = "0.0.0.0".parse().unwrap();
258 let v6 = v4.to_ipv6_mapped();
259
260 let record = NodeRecord {
261 address: v6.into(),
262 tcp_port: rng.random(),
263 udp_port: rng.random(),
264 id: rng.random(),
265 };
266
267 assert!(record.clone().convert_ipv4_mapped());
268 assert_eq!(record.into_ipv4_mapped().address, IpAddr::from(v4));
269 }
270
271 #[test]
272 fn test_mapped_ipv4() {
273 let mut rng = rng();
274 let v4: Ipv4Addr = "0.0.0.0".parse().unwrap();
275
276 let record = NodeRecord {
277 address: v4.into(),
278 tcp_port: rng.random(),
279 udp_port: rng.random(),
280 id: rng.random(),
281 };
282
283 assert!(!record.clone().convert_ipv4_mapped());
284 assert_eq!(record.into_ipv4_mapped().address, IpAddr::from(v4));
285 }
286
287 #[test]
288 fn test_noderecord_codec_ipv4() {
289 let mut rng = rng();
290 for _ in 0..100 {
291 let mut ip = [0u8; 4];
292 rng.fill_bytes(&mut ip);
293 let record = NodeRecord {
294 address: IpAddr::V4(ip.into()),
295 tcp_port: rng.random(),
296 udp_port: rng.random(),
297 id: rng.random(),
298 };
299
300 let decoded = NodeRecord::decode(&mut alloy_rlp::encode(record).as_slice()).unwrap();
301 assert_eq!(record, decoded);
302 }
303 }
304
305 #[test]
306 fn test_noderecord_codec_ipv6() {
307 let mut rng = rng();
308 for _ in 0..100 {
309 let mut ip = [0u8; 16];
310 rng.fill_bytes(&mut ip);
311 let record = NodeRecord {
312 address: IpAddr::V6(ip.into()),
313 tcp_port: rng.random(),
314 udp_port: rng.random(),
315 id: rng.random(),
316 };
317
318 let decoded = NodeRecord::decode(&mut alloy_rlp::encode(record).as_slice()).unwrap();
319 assert_eq!(record, decoded);
320 }
321 }
322
323 #[test]
324 fn test_node_record() {
325 let url = "enode://fc8a2ff614e848c0af4c99372a81b8655edb8e11b617cffd0aab1a0691bcca66ca533626a528ee567f05f70c8cb529bda2c0a864cc0aec638a367fd2bb8e49fb@127.0.0.1:35481?discport=0";
326 let node: NodeRecord = url.parse().unwrap();
327 assert_eq!(node, NodeRecord {
328 address: IpAddr::V4([127,0,0, 1].into()),
329 tcp_port: 35481,
330 udp_port: 0,
331 id: b512!("0xfc8a2ff614e848c0af4c99372a81b8655edb8e11b617cffd0aab1a0691bcca66ca533626a528ee567f05f70c8cb529bda2c0a864cc0aec638a367fd2bb8e49fb"),
332 })
333 }
334
335 #[test]
336 fn test_url_parse() {
337 let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301";
338 let node: NodeRecord = url.parse().unwrap();
339 assert_eq!(node, NodeRecord {
340 address: IpAddr::V4([10,3,58,6].into()),
341 tcp_port: 30303,
342 udp_port: 30301,
343 id: b512!("6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0"),
344 })
345 }
346
347 #[test]
348 fn test_node_display() {
349 let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303";
350 let node: NodeRecord = url.parse().unwrap();
351 assert_eq!(url, &format!("{node}"));
352 }
353
354 #[test]
355 fn test_node_display_discport() {
356 let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301";
357 let node: NodeRecord = url.parse().unwrap();
358 assert_eq!(url, &format!("{node}"));
359 }
360
361 #[test]
362 fn test_node_serialize() {
363 let cases = vec![
364 (
366 NodeRecord {
367 address: IpAddr::V4([10, 3, 58, 6].into()),
368 tcp_port: 30303u16,
369 udp_port: 30301u16,
370 id: b512!("6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0"),
371 },
372 "\"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301\""
373 ),
374 (
376 NodeRecord {
377 address: Ipv6Addr::new(0x2001, 0xdb8, 0x3c4d, 0x15, 0x0, 0x0, 0xabcd, 0xef12).into(),
378 tcp_port: 52150u16,
379 udp_port: 52151u16,
380 id: b512!("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
381 },
382 "\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150?discport=52151\"",
383 )
384 ];
385
386 for (node, expected) in cases {
387 let ser = serde_json::to_string::<NodeRecord>(&node).expect("couldn't serialize");
388 assert_eq!(ser, expected);
389 }
390 }
391
392 #[test]
393 fn test_node_deserialize() {
394 let cases = vec![
395 (
397 "\"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301\"",
398 NodeRecord {
399 address: IpAddr::V4([10, 3, 58, 6].into()),
400 tcp_port: 30303u16,
401 udp_port: 30301u16,
402 id: b512!("6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0"),
403 }
404 ),
405 (
407 "\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150?discport=52151\"",
408 NodeRecord {
409 address: Ipv6Addr::new(0x2001, 0xdb8, 0x3c4d, 0x15, 0x0, 0x0, 0xabcd, 0xef12).into(),
410 tcp_port: 52150u16,
411 udp_port: 52151u16,
412 id: b512!("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
413 }
414 ),
415 ];
416
417 for (url, expected) in cases {
418 let node: NodeRecord = serde_json::from_str(url).expect("couldn't deserialize");
419 assert_eq!(node, expected);
420 }
421 }
422
423 #[test]
424 #[cfg(feature = "secp256k1")]
425 fn tcp_less_enr_converts_with_port_zero() {
426 let sk = secp256k1::SecretKey::from_byte_array(&[1u8; 32]).unwrap();
428 let enr = Enr::builder().ip4("1.2.3.4".parse().unwrap()).udp4(30301).build(&sk).unwrap();
429
430 let record = NodeRecord::try_from(&enr).unwrap();
431
432 assert_eq!(record.tcp_port, 0);
433 assert_eq!(record.udp_port, 30301);
434 }
435
436 #[test]
437 #[cfg(feature = "secp256k1")]
438 fn enr_endpoint_keeps_ip_and_ports_together() {
439 let sk = secp256k1::SecretKey::from_byte_array(&[1u8; 32]).unwrap();
440 let mut builder = Enr::builder();
441 builder.ip4("10.0.0.1".parse().unwrap()).ip6("::1".parse().unwrap());
442 builder.udp6(30401).tcp6(30403);
443 let enr = builder.build(&sk).unwrap();
444 let record = NodeRecord::try_from(&enr).unwrap();
445
446 assert_eq!(record.address, "::1".parse::<IpAddr>().unwrap());
447 assert_eq!(record.udp_port, 30401);
448 assert_eq!(record.tcp_port, 30403);
449
450 let mut builder = Enr::builder();
453 builder.ip6("::1".parse().unwrap()).udp6(30401).tcp4(30303);
454 let enr = builder.build(&sk).unwrap();
455 let record = NodeRecord::try_from(&enr).unwrap();
456
457 assert_eq!(record.address, "::1".parse::<IpAddr>().unwrap());
458 assert_eq!(record.udp_port, 30401);
459 assert_eq!(record.tcp_port, 30303);
460
461 let mut builder = Enr::builder();
463 builder.ip4("10.0.0.1".parse().unwrap()).udp4(30301).tcp4(30303);
464 builder.ip6("::1".parse().unwrap()).udp6(30401).tcp6(30403);
465 let enr = builder.build(&sk).unwrap();
466 let record = NodeRecord::try_from(&enr).unwrap();
467
468 assert_eq!(record.address, "10.0.0.1".parse::<IpAddr>().unwrap());
469 assert_eq!(record.udp_port, 30301);
470 assert_eq!(record.tcp_port, 30303);
471
472 let enr = Enr::builder().ip4("10.0.0.1".parse().unwrap()).tcp4(30303).build(&sk).unwrap();
474 assert!(NodeRecord::try_from(&enr).is_err());
475 }
476}