1use serde::{Deserialize, Serialize};
22
23use crate::caps::{PEX_MAX_ADDRESSES, PEX_MAX_ENTRY_AGE, PEX_MAX_FLAGS, PEX_MAX_FLAG_LEN};
24use crate::error::EntrySkip;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30pub enum AddressKind {
31 Direct,
33 Mapped,
35 Reflexive,
37 Relay,
39 #[serde(other)]
42 #[default]
43 Unknown,
44}
45
46impl AddressKind {
47 #[must_use]
49 pub fn as_str(self) -> &'static str {
50 match self {
51 AddressKind::Direct => "direct",
52 AddressKind::Mapped => "mapped",
53 AddressKind::Reflexive => "reflexive",
54 AddressKind::Relay => "relay",
55 AddressKind::Unknown => "unknown",
56 }
57 }
58
59 #[must_use]
61 pub fn is_registered(self) -> bool {
62 !matches!(self, AddressKind::Unknown)
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
70#[serde(rename_all = "lowercase")]
71pub enum Provenance {
72 Direct,
74 Relay,
76 Introducer,
78 #[serde(other)]
81 #[default]
82 Unknown,
83}
84
85impl Provenance {
86 #[must_use]
88 pub fn as_str(self) -> &'static str {
89 match self {
90 Provenance::Direct => "direct",
91 Provenance::Relay => "relay",
92 Provenance::Introducer => "introducer",
93 Provenance::Unknown => "unknown",
94 }
95 }
96
97 #[must_use]
99 pub fn is_registered(self) -> bool {
100 !matches!(self, Provenance::Unknown)
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107pub struct Address {
108 #[serde(default)]
110 pub host: String,
111 #[serde(default)]
113 pub port: u16,
114 #[serde(default)]
116 pub kind: AddressKind,
117}
118
119impl Address {
120 #[must_use]
122 pub fn direct(host: impl Into<String>, port: u16) -> Self {
123 Address {
124 host: host.into(),
125 port,
126 kind: AddressKind::Direct,
127 }
128 }
129
130 #[must_use]
132 pub fn new(host: impl Into<String>, port: u16, kind: AddressKind) -> Self {
133 Address {
134 host: host.into(),
135 port,
136 kind,
137 }
138 }
139
140 #[must_use]
143 pub fn is_valid(&self) -> bool {
144 !self.host.is_empty() && self.port != 0 && self.kind.is_registered()
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
151pub struct PeerEntry {
152 #[serde(default)]
154 pub peer_id: String,
155 #[serde(default)]
158 pub addresses: Vec<Address>,
159 #[serde(default)]
161 pub network_id: String,
162 #[serde(default)]
164 pub last_seen: u64,
165 #[serde(default)]
167 pub via: Provenance,
168 #[serde(default)]
170 pub flags: Vec<String>,
171}
172
173impl PeerEntry {
174 #[must_use]
177 pub fn new(
178 peer_id: impl Into<String>,
179 network_id: impl Into<String>,
180 last_seen: u64,
181 via: Provenance,
182 ) -> Self {
183 PeerEntry {
184 peer_id: peer_id.into(),
185 addresses: Vec::new(),
186 network_id: network_id.into(),
187 last_seen,
188 via,
189 flags: Vec::new(),
190 }
191 }
192
193 #[must_use]
195 pub fn with_address(mut self, addr: Address) -> Self {
196 self.addresses.push(addr);
197 self
198 }
199
200 #[must_use]
202 pub fn with_flag(mut self, flag: impl Into<String>) -> Self {
203 self.flags.push(flag.into());
204 self
205 }
206
207 pub fn validate(&self, ctx: &ValidateCtx<'_>) -> Result<(), EntrySkip> {
210 if !is_hex64(&self.peer_id) {
211 return Err(EntrySkip::BadPeerId);
212 }
213 if self.peer_id == ctx.receiver_peer_id || self.peer_id == ctx.sender_peer_id {
214 return Err(EntrySkip::SelfOrPartner);
215 }
216 if self.addresses.len() > PEX_MAX_ADDRESSES {
217 return Err(EntrySkip::TooManyAddresses);
218 }
219 if self.addresses.iter().any(|a| !a.is_valid()) {
220 return Err(EntrySkip::BadAddress);
221 }
222 if self.flags.len() > PEX_MAX_FLAGS || self.flags.iter().any(|f| f.len() > PEX_MAX_FLAG_LEN)
223 {
224 return Err(EntrySkip::TooManyFlags);
225 }
226 if self.network_id != ctx.network_id {
227 return Err(EntrySkip::NetworkMismatch);
228 }
229 if !self.via.is_registered() {
230 return Err(EntrySkip::BadVia);
231 }
232 if self.last_seen < ctx.now_secs && ctx.now_secs - self.last_seen > PEX_MAX_ENTRY_AGE {
235 return Err(EntrySkip::TooOld);
236 }
237 Ok(())
238 }
239
240 #[must_use]
243 pub fn clamped(&self, now_secs: u64) -> PeerEntry {
244 let mut e = self.clone();
245 if e.last_seen > now_secs {
246 e.last_seen = now_secs;
247 }
248 e
249 }
250
251 #[must_use]
260 pub fn fingerprint(&self) -> String {
261 let mut addrs: Vec<String> = self
262 .addresses
263 .iter()
264 .map(|a| format!("{}|{}|{}", a.host, a.port, a.kind.as_str()))
265 .collect();
266 addrs.sort();
267 let mut flags = self.flags.clone();
268 flags.sort();
269 format!("{}#{}", addrs.join(","), flags.join(","))
270 }
271
272 #[must_use]
285 pub fn fingerprint_hash(&self) -> u64 {
286 use std::hash::{Hash, Hasher};
287
288 let mut addrs: Vec<&Address> = self.addresses.iter().collect();
289 addrs.sort_by(|a, b| {
290 (a.host.as_str(), a.port, a.kind.as_str()).cmp(&(
291 b.host.as_str(),
292 b.port,
293 b.kind.as_str(),
294 ))
295 });
296 let mut flags: Vec<&str> = self.flags.iter().map(String::as_str).collect();
297 flags.sort_unstable();
298
299 let mut hasher = std::collections::hash_map::DefaultHasher::new();
303 addrs.len().hash(&mut hasher);
304 for a in &addrs {
305 a.host.hash(&mut hasher);
306 a.port.hash(&mut hasher);
307 a.kind.as_str().hash(&mut hasher);
308 }
309 flags.len().hash(&mut hasher);
310 for f in &flags {
311 f.hash(&mut hasher);
312 }
313 hasher.finish()
314 }
315}
316
317#[derive(Debug, Clone, Copy)]
319pub struct ValidateCtx<'a> {
320 pub receiver_peer_id: &'a str,
322 pub sender_peer_id: &'a str,
324 pub network_id: &'a str,
326 pub now_secs: u64,
328}
329
330#[must_use]
332pub fn is_hex64(s: &str) -> bool {
333 s.len() == 64
334 && s.bytes()
335 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 fn hex(b: u8) -> String {
343 format!("{b:02x}").repeat(32)
344 }
345
346 fn ctx<'a>(recv: &'a str, send: &'a str, net: &'a str, now: u64) -> ValidateCtx<'a> {
347 ValidateCtx {
348 receiver_peer_id: recv,
349 sender_peer_id: send,
350 network_id: net,
351 now_secs: now,
352 }
353 }
354
355 #[test]
356 fn hex64_recognizer() {
357 assert!(is_hex64(&"a".repeat(64)));
358 assert!(is_hex64(&hex(0xab)));
359 assert!(!is_hex64(&"a".repeat(63)));
360 assert!(!is_hex64(&"A".repeat(64))); assert!(!is_hex64(&"g".repeat(64))); }
363
364 #[test]
365 fn kind_and_via_tokens_are_frozen_lowercase() {
366 assert_eq!(
367 serde_json::to_string(&AddressKind::Direct).unwrap(),
368 "\"direct\""
369 );
370 assert_eq!(
371 serde_json::to_string(&AddressKind::Relay).unwrap(),
372 "\"relay\""
373 );
374 assert_eq!(
375 serde_json::to_string(&Provenance::Introducer).unwrap(),
376 "\"introducer\""
377 );
378 }
379
380 #[test]
381 fn unknown_kind_and_via_decode_to_catch_all() {
382 let a: Address = serde_json::from_str(r#"{"host":"h","port":1,"kind":"quantum"}"#).unwrap();
383 assert_eq!(a.kind, AddressKind::Unknown);
384 let e: PeerEntry = serde_json::from_str(
385 r#"{"peer_id":"x","addresses":[],"network_id":"n","last_seen":1,"via":"teleport"}"#,
386 )
387 .unwrap();
388 assert_eq!(e.via, Provenance::Unknown);
389 }
390
391 #[test]
392 fn valid_entry_passes() {
393 let e = PeerEntry::new(hex(0x07), "mainnet", 1000, Provenance::Direct)
394 .with_address(Address::direct("203.0.113.7", 9444))
395 .with_flag("storage");
396 assert!(e
397 .validate(&ctx(&hex(0x01), &hex(0x02), "mainnet", 1000))
398 .is_ok());
399 }
400
401 #[test]
402 fn skip_reasons_match_spec() {
403 let recv = hex(0x01);
404 let send = hex(0x02);
405 let e = PeerEntry::new("nothex", "mainnet", 10, Provenance::Direct);
407 assert_eq!(
408 e.validate(&ctx(&recv, &send, "mainnet", 10)),
409 Err(EntrySkip::BadPeerId)
410 );
411 let e = PeerEntry::new(recv.clone(), "mainnet", 10, Provenance::Direct);
413 assert_eq!(
414 e.validate(&ctx(&recv, &send, "mainnet", 10)),
415 Err(EntrySkip::SelfOrPartner)
416 );
417 let e = PeerEntry::new(send.clone(), "mainnet", 10, Provenance::Direct);
418 assert_eq!(
419 e.validate(&ctx(&recv, &send, "mainnet", 10)),
420 Err(EntrySkip::SelfOrPartner)
421 );
422 let e = PeerEntry::new(hex(0x07), "mainnet", 10, Provenance::Direct)
424 .with_address(Address::new("h", 0, AddressKind::Direct));
425 assert_eq!(
426 e.validate(&ctx(&recv, &send, "mainnet", 10)),
427 Err(EntrySkip::BadAddress)
428 );
429 let e = PeerEntry::new(hex(0x07), "mainnet", 10, Provenance::Direct)
431 .with_address(Address::new("h", 1, AddressKind::Unknown));
432 assert_eq!(
433 e.validate(&ctx(&recv, &send, "mainnet", 10)),
434 Err(EntrySkip::BadAddress)
435 );
436 let e = PeerEntry::new(hex(0x07), "testnet", 10, Provenance::Direct);
438 assert_eq!(
439 e.validate(&ctx(&recv, &send, "mainnet", 10)),
440 Err(EntrySkip::NetworkMismatch)
441 );
442 let e = PeerEntry::new(hex(0x07), "mainnet", 10, Provenance::Unknown);
444 assert_eq!(
445 e.validate(&ctx(&recv, &send, "mainnet", 10)),
446 Err(EntrySkip::BadVia)
447 );
448 let e = PeerEntry::new(hex(0x07), "mainnet", 10, Provenance::Direct);
450 assert_eq!(
451 e.validate(&ctx(&recv, &send, "mainnet", 2000)),
452 Err(EntrySkip::TooOld)
453 );
454 }
455
456 #[test]
457 fn too_many_addresses_and_flags() {
458 let recv = hex(0x01);
459 let send = hex(0x02);
460 let mut e = PeerEntry::new(hex(0x07), "mainnet", 10, Provenance::Direct);
461 for i in 0..9 {
462 e = e.with_address(Address::direct("h", 1000 + i));
463 }
464 assert_eq!(
465 e.validate(&ctx(&recv, &send, "mainnet", 10)),
466 Err(EntrySkip::TooManyAddresses)
467 );
468
469 let mut e = PeerEntry::new(hex(0x07), "mainnet", 10, Provenance::Direct);
470 for i in 0..9 {
471 e = e.with_flag(format!("f{i}"));
472 }
473 assert_eq!(
474 e.validate(&ctx(&recv, &send, "mainnet", 10)),
475 Err(EntrySkip::TooManyFlags)
476 );
477 }
478
479 #[test]
480 fn future_last_seen_is_clamped_not_skipped() {
481 let recv = hex(0x01);
482 let send = hex(0x02);
483 let e = PeerEntry::new(hex(0x07), "mainnet", 5000, Provenance::Direct);
484 assert!(e.validate(&ctx(&recv, &send, "mainnet", 1000)).is_ok());
486 assert_eq!(e.clamped(1000).last_seen, 1000);
487 assert_eq!(e.clamped(6000).last_seen, 5000);
488 }
489
490 #[test]
491 fn fingerprint_ignores_last_seen_but_tracks_addresses_and_flags() {
492 let a = PeerEntry::new(hex(0x07), "mainnet", 100, Provenance::Direct)
493 .with_address(Address::direct("h", 1))
494 .with_flag("storage");
495 let a2 = PeerEntry::new(hex(0x07), "mainnet", 999, Provenance::Direct)
496 .with_address(Address::direct("h", 1))
497 .with_flag("storage");
498 assert_eq!(
499 a.fingerprint(),
500 a2.fingerprint(),
501 "last_seen must not affect fingerprint"
502 );
503 let b = a2.clone().with_flag("holepunch");
504 assert_ne!(
505 a.fingerprint(),
506 b.fingerprint(),
507 "a flag change must change fingerprint"
508 );
509 }
510
511 #[test]
516 fn fingerprint_hash_matches_fingerprint_equality_semantics() {
517 let a = PeerEntry::new(hex(0x07), "mainnet", 100, Provenance::Direct)
518 .with_address(Address::direct("h", 1))
519 .with_flag("storage");
520 let a2 = PeerEntry::new(hex(0x07), "mainnet", 999, Provenance::Direct)
521 .with_address(Address::direct("h", 1))
522 .with_flag("storage");
523 assert_eq!(
524 a.fingerprint_hash(),
525 a2.fingerprint_hash(),
526 "last_seen must not affect fingerprint_hash"
527 );
528 assert_eq!(
529 a.fingerprint() == a2.fingerprint(),
530 a.fingerprint_hash() == a2.fingerprint_hash(),
531 "fingerprint_hash must agree with fingerprint on equality"
532 );
533
534 let b = a2.clone().with_flag("holepunch");
535 assert_ne!(
536 a.fingerprint_hash(),
537 b.fingerprint_hash(),
538 "a flag change must change fingerprint_hash"
539 );
540 assert_eq!(
541 a.fingerprint() == b.fingerprint(),
542 a.fingerprint_hash() == b.fingerprint_hash(),
543 "fingerprint_hash must agree with fingerprint on inequality"
544 );
545
546 let c = PeerEntry::new(hex(0x08), "mainnet", 1, Provenance::Direct)
549 .with_address(Address::direct("h1", 1))
550 .with_address(Address::direct("h2", 2))
551 .with_flag("storage")
552 .with_flag("holepunch");
553 let d = PeerEntry::new(hex(0x08), "mainnet", 2, Provenance::Direct)
554 .with_address(Address::direct("h2", 2))
555 .with_address(Address::direct("h1", 1))
556 .with_flag("holepunch")
557 .with_flag("storage");
558 assert_eq!(
559 c.fingerprint_hash(),
560 d.fingerprint_hash(),
561 "address/flag insertion order must not affect fingerprint_hash"
562 );
563 assert_eq!(c.fingerprint(), d.fingerprint());
564 }
565
566 #[test]
567 fn entry_round_trips_through_json() {
568 let e = PeerEntry::new(hex(0x07), "mainnet", 1_719_763_200, Provenance::Direct)
569 .with_address(Address::direct("203.0.113.7", 9444))
570 .with_flag("storage")
571 .with_flag("holepunch");
572 let json = serde_json::to_string(&e).unwrap();
573 assert!(json.contains("\"peer_id\":"));
574 assert!(json.contains("\"via\":\"direct\""));
575 assert!(json.contains("\"kind\":\"direct\""));
576 let back: PeerEntry = serde_json::from_str(&json).unwrap();
577 assert_eq!(e, back);
578 }
579}