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;
25use crate::payment::{PaymentClaim, PaymentClaimError, SignatureVerifier};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
30#[serde(rename_all = "lowercase")]
31pub enum AddressKind {
32 Direct,
34 Mapped,
36 Reflexive,
38 Relay,
40 #[serde(other)]
43 #[default]
44 Unknown,
45}
46
47impl AddressKind {
48 #[must_use]
50 pub fn as_str(self) -> &'static str {
51 match self {
52 AddressKind::Direct => "direct",
53 AddressKind::Mapped => "mapped",
54 AddressKind::Reflexive => "reflexive",
55 AddressKind::Relay => "relay",
56 AddressKind::Unknown => "unknown",
57 }
58 }
59
60 #[must_use]
62 pub fn is_registered(self) -> bool {
63 !matches!(self, AddressKind::Unknown)
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
71#[serde(rename_all = "lowercase")]
72pub enum Provenance {
73 Direct,
75 Relay,
77 Introducer,
79 #[serde(other)]
82 #[default]
83 Unknown,
84}
85
86impl Provenance {
87 #[must_use]
89 pub fn as_str(self) -> &'static str {
90 match self {
91 Provenance::Direct => "direct",
92 Provenance::Relay => "relay",
93 Provenance::Introducer => "introducer",
94 Provenance::Unknown => "unknown",
95 }
96 }
97
98 #[must_use]
100 pub fn is_registered(self) -> bool {
101 !matches!(self, Provenance::Unknown)
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108pub struct Address {
109 #[serde(default)]
111 pub host: String,
112 #[serde(default)]
114 pub port: u16,
115 #[serde(default)]
117 pub kind: AddressKind,
118}
119
120impl Address {
121 #[must_use]
123 pub fn direct(host: impl Into<String>, port: u16) -> Self {
124 Address {
125 host: host.into(),
126 port,
127 kind: AddressKind::Direct,
128 }
129 }
130
131 #[must_use]
133 pub fn new(host: impl Into<String>, port: u16, kind: AddressKind) -> Self {
134 Address {
135 host: host.into(),
136 port,
137 kind,
138 }
139 }
140
141 #[must_use]
144 pub fn is_valid(&self) -> bool {
145 !self.host.is_empty() && self.port != 0 && self.kind.is_registered()
146 }
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152pub struct PeerEntry {
153 #[serde(default)]
155 pub peer_id: String,
156 #[serde(default)]
159 pub addresses: Vec<Address>,
160 #[serde(default)]
162 pub network_id: String,
163 #[serde(default)]
165 pub last_seen: u64,
166 #[serde(default)]
168 pub via: Provenance,
169 #[serde(default)]
171 pub flags: Vec<String>,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub payment: Option<PaymentClaim>,
179}
180
181impl PeerEntry {
182 #[must_use]
185 pub fn new(
186 peer_id: impl Into<String>,
187 network_id: impl Into<String>,
188 last_seen: u64,
189 via: Provenance,
190 ) -> Self {
191 PeerEntry {
192 peer_id: peer_id.into(),
193 addresses: Vec::new(),
194 network_id: network_id.into(),
195 last_seen,
196 via,
197 flags: Vec::new(),
198 payment: None,
199 }
200 }
201
202 #[must_use]
204 pub fn with_address(mut self, addr: Address) -> Self {
205 self.addresses.push(addr);
206 self
207 }
208
209 #[must_use]
211 pub fn with_flag(mut self, flag: impl Into<String>) -> Self {
212 self.flags.push(flag.into());
213 self
214 }
215
216 #[must_use]
218 pub fn with_payment(mut self, claim: PaymentClaim) -> Self {
219 self.payment = Some(claim);
220 self
221 }
222
223 pub fn verified_payment_address(
236 &self,
237 verifier: &impl SignatureVerifier,
238 ) -> Result<&str, PaymentClaimError> {
239 self.payment
240 .as_ref()
241 .ok_or(PaymentClaimError::NotPresent)?
242 .verify(&self.peer_id, &self.network_id, verifier)
243 }
244
245 pub fn validate(&self, ctx: &ValidateCtx<'_>) -> Result<(), EntrySkip> {
248 if !is_hex64(&self.peer_id) {
249 return Err(EntrySkip::BadPeerId);
250 }
251 if self.peer_id == ctx.receiver_peer_id || self.peer_id == ctx.sender_peer_id {
252 return Err(EntrySkip::SelfOrPartner);
253 }
254 if self.addresses.len() > PEX_MAX_ADDRESSES {
255 return Err(EntrySkip::TooManyAddresses);
256 }
257 if self.addresses.iter().any(|a| !a.is_valid()) {
258 return Err(EntrySkip::BadAddress);
259 }
260 if self.flags.len() > PEX_MAX_FLAGS || self.flags.iter().any(|f| f.len() > PEX_MAX_FLAG_LEN)
261 {
262 return Err(EntrySkip::TooManyFlags);
263 }
264 if self.network_id != ctx.network_id {
265 return Err(EntrySkip::NetworkMismatch);
266 }
267 if !self.via.is_registered() {
268 return Err(EntrySkip::BadVia);
269 }
270 if self.payment.as_ref().is_some_and(|p| !p.within_caps()) {
273 return Err(EntrySkip::OversizePayment);
274 }
275 if self.last_seen < ctx.now_secs && ctx.now_secs - self.last_seen > PEX_MAX_ENTRY_AGE {
278 return Err(EntrySkip::TooOld);
279 }
280 Ok(())
281 }
282
283 #[must_use]
286 pub fn clamped(&self, now_secs: u64) -> PeerEntry {
287 let mut e = self.clone();
288 if e.last_seen > now_secs {
289 e.last_seen = now_secs;
290 }
291 e
292 }
293
294 #[must_use]
303 pub fn fingerprint(&self) -> String {
304 let mut addrs: Vec<String> = self
305 .addresses
306 .iter()
307 .map(|a| format!("{}|{}|{}", a.host, a.port, a.kind.as_str()))
308 .collect();
309 addrs.sort();
310 let mut flags = self.flags.clone();
311 flags.sort();
312 let payment = self
313 .payment
314 .as_ref()
315 .map(|p| p.wire_parts().join("|"))
316 .unwrap_or_default();
317 format!("{}#{}#{}", addrs.join(","), flags.join(","), payment)
318 }
319
320 #[must_use]
333 pub fn fingerprint_hash(&self) -> u64 {
334 use std::hash::{Hash, Hasher};
335
336 let mut addrs: Vec<&Address> = self.addresses.iter().collect();
337 addrs.sort_by(|a, b| {
338 (a.host.as_str(), a.port, a.kind.as_str()).cmp(&(
339 b.host.as_str(),
340 b.port,
341 b.kind.as_str(),
342 ))
343 });
344 let mut flags: Vec<&str> = self.flags.iter().map(String::as_str).collect();
345 flags.sort_unstable();
346
347 let mut hasher = std::collections::hash_map::DefaultHasher::new();
351 addrs.len().hash(&mut hasher);
352 for a in &addrs {
353 a.host.hash(&mut hasher);
354 a.port.hash(&mut hasher);
355 a.kind.as_str().hash(&mut hasher);
356 }
357 flags.len().hash(&mut hasher);
358 for f in &flags {
359 f.hash(&mut hasher);
360 }
361 match &self.payment {
363 Some(p) => p.wire_parts().hash(&mut hasher),
364 None => 0u8.hash(&mut hasher),
365 }
366 hasher.finish()
367 }
368}
369
370#[derive(Debug, Clone, Copy)]
372pub struct ValidateCtx<'a> {
373 pub receiver_peer_id: &'a str,
375 pub sender_peer_id: &'a str,
377 pub network_id: &'a str,
379 pub now_secs: u64,
381}
382
383#[must_use]
385pub fn is_hex64(s: &str) -> bool {
386 s.len() == 64
387 && s.bytes()
388 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 fn hex(b: u8) -> String {
396 format!("{b:02x}").repeat(32)
397 }
398
399 fn ctx<'a>(recv: &'a str, send: &'a str, net: &'a str, now: u64) -> ValidateCtx<'a> {
400 ValidateCtx {
401 receiver_peer_id: recv,
402 sender_peer_id: send,
403 network_id: net,
404 now_secs: now,
405 }
406 }
407
408 #[test]
409 fn hex64_recognizer() {
410 assert!(is_hex64(&"a".repeat(64)));
411 assert!(is_hex64(&hex(0xab)));
412 assert!(!is_hex64(&"a".repeat(63)));
413 assert!(!is_hex64(&"A".repeat(64))); assert!(!is_hex64(&"g".repeat(64))); }
416
417 #[test]
418 fn kind_and_via_tokens_are_frozen_lowercase() {
419 assert_eq!(
420 serde_json::to_string(&AddressKind::Direct).unwrap(),
421 "\"direct\""
422 );
423 assert_eq!(
424 serde_json::to_string(&AddressKind::Relay).unwrap(),
425 "\"relay\""
426 );
427 assert_eq!(
428 serde_json::to_string(&Provenance::Introducer).unwrap(),
429 "\"introducer\""
430 );
431 }
432
433 #[test]
434 fn unknown_kind_and_via_decode_to_catch_all() {
435 let a: Address = serde_json::from_str(r#"{"host":"h","port":1,"kind":"quantum"}"#).unwrap();
436 assert_eq!(a.kind, AddressKind::Unknown);
437 let e: PeerEntry = serde_json::from_str(
438 r#"{"peer_id":"x","addresses":[],"network_id":"n","last_seen":1,"via":"teleport"}"#,
439 )
440 .unwrap();
441 assert_eq!(e.via, Provenance::Unknown);
442 }
443
444 #[test]
445 fn valid_entry_passes() {
446 let e = PeerEntry::new(hex(0x07), "mainnet", 1000, Provenance::Direct)
447 .with_address(Address::direct("203.0.113.7", 9444))
448 .with_flag("storage");
449 assert!(e
450 .validate(&ctx(&hex(0x01), &hex(0x02), "mainnet", 1000))
451 .is_ok());
452 }
453
454 #[test]
455 fn skip_reasons_match_spec() {
456 let recv = hex(0x01);
457 let send = hex(0x02);
458 let e = PeerEntry::new("nothex", "mainnet", 10, Provenance::Direct);
460 assert_eq!(
461 e.validate(&ctx(&recv, &send, "mainnet", 10)),
462 Err(EntrySkip::BadPeerId)
463 );
464 let e = PeerEntry::new(recv.clone(), "mainnet", 10, Provenance::Direct);
466 assert_eq!(
467 e.validate(&ctx(&recv, &send, "mainnet", 10)),
468 Err(EntrySkip::SelfOrPartner)
469 );
470 let e = PeerEntry::new(send.clone(), "mainnet", 10, Provenance::Direct);
471 assert_eq!(
472 e.validate(&ctx(&recv, &send, "mainnet", 10)),
473 Err(EntrySkip::SelfOrPartner)
474 );
475 let e = PeerEntry::new(hex(0x07), "mainnet", 10, Provenance::Direct)
477 .with_address(Address::new("h", 0, AddressKind::Direct));
478 assert_eq!(
479 e.validate(&ctx(&recv, &send, "mainnet", 10)),
480 Err(EntrySkip::BadAddress)
481 );
482 let e = PeerEntry::new(hex(0x07), "mainnet", 10, Provenance::Direct)
484 .with_address(Address::new("h", 1, AddressKind::Unknown));
485 assert_eq!(
486 e.validate(&ctx(&recv, &send, "mainnet", 10)),
487 Err(EntrySkip::BadAddress)
488 );
489 let e = PeerEntry::new(hex(0x07), "testnet", 10, Provenance::Direct);
491 assert_eq!(
492 e.validate(&ctx(&recv, &send, "mainnet", 10)),
493 Err(EntrySkip::NetworkMismatch)
494 );
495 let e = PeerEntry::new(hex(0x07), "mainnet", 10, Provenance::Unknown);
497 assert_eq!(
498 e.validate(&ctx(&recv, &send, "mainnet", 10)),
499 Err(EntrySkip::BadVia)
500 );
501 let e = PeerEntry::new(hex(0x07), "mainnet", 10, Provenance::Direct);
503 assert_eq!(
504 e.validate(&ctx(&recv, &send, "mainnet", 2000)),
505 Err(EntrySkip::TooOld)
506 );
507 }
508
509 #[test]
510 fn too_many_addresses_and_flags() {
511 let recv = hex(0x01);
512 let send = hex(0x02);
513 let mut e = PeerEntry::new(hex(0x07), "mainnet", 10, Provenance::Direct);
514 for i in 0..9 {
515 e = e.with_address(Address::direct("h", 1000 + i));
516 }
517 assert_eq!(
518 e.validate(&ctx(&recv, &send, "mainnet", 10)),
519 Err(EntrySkip::TooManyAddresses)
520 );
521
522 let mut e = PeerEntry::new(hex(0x07), "mainnet", 10, Provenance::Direct);
523 for i in 0..9 {
524 e = e.with_flag(format!("f{i}"));
525 }
526 assert_eq!(
527 e.validate(&ctx(&recv, &send, "mainnet", 10)),
528 Err(EntrySkip::TooManyFlags)
529 );
530 }
531
532 #[test]
533 fn future_last_seen_is_clamped_not_skipped() {
534 let recv = hex(0x01);
535 let send = hex(0x02);
536 let e = PeerEntry::new(hex(0x07), "mainnet", 5000, Provenance::Direct);
537 assert!(e.validate(&ctx(&recv, &send, "mainnet", 1000)).is_ok());
539 assert_eq!(e.clamped(1000).last_seen, 1000);
540 assert_eq!(e.clamped(6000).last_seen, 5000);
541 }
542
543 #[test]
544 fn fingerprint_ignores_last_seen_but_tracks_addresses_and_flags() {
545 let a = PeerEntry::new(hex(0x07), "mainnet", 100, Provenance::Direct)
546 .with_address(Address::direct("h", 1))
547 .with_flag("storage");
548 let a2 = PeerEntry::new(hex(0x07), "mainnet", 999, Provenance::Direct)
549 .with_address(Address::direct("h", 1))
550 .with_flag("storage");
551 assert_eq!(
552 a.fingerprint(),
553 a2.fingerprint(),
554 "last_seen must not affect fingerprint"
555 );
556 let b = a2.clone().with_flag("holepunch");
557 assert_ne!(
558 a.fingerprint(),
559 b.fingerprint(),
560 "a flag change must change fingerprint"
561 );
562 }
563
564 #[test]
569 fn fingerprint_hash_matches_fingerprint_equality_semantics() {
570 let a = PeerEntry::new(hex(0x07), "mainnet", 100, Provenance::Direct)
571 .with_address(Address::direct("h", 1))
572 .with_flag("storage");
573 let a2 = PeerEntry::new(hex(0x07), "mainnet", 999, Provenance::Direct)
574 .with_address(Address::direct("h", 1))
575 .with_flag("storage");
576 assert_eq!(
577 a.fingerprint_hash(),
578 a2.fingerprint_hash(),
579 "last_seen must not affect fingerprint_hash"
580 );
581 assert_eq!(
582 a.fingerprint() == a2.fingerprint(),
583 a.fingerprint_hash() == a2.fingerprint_hash(),
584 "fingerprint_hash must agree with fingerprint on equality"
585 );
586
587 let b = a2.clone().with_flag("holepunch");
588 assert_ne!(
589 a.fingerprint_hash(),
590 b.fingerprint_hash(),
591 "a flag change must change fingerprint_hash"
592 );
593 assert_eq!(
594 a.fingerprint() == b.fingerprint(),
595 a.fingerprint_hash() == b.fingerprint_hash(),
596 "fingerprint_hash must agree with fingerprint on inequality"
597 );
598
599 let c = PeerEntry::new(hex(0x08), "mainnet", 1, Provenance::Direct)
602 .with_address(Address::direct("h1", 1))
603 .with_address(Address::direct("h2", 2))
604 .with_flag("storage")
605 .with_flag("holepunch");
606 let d = PeerEntry::new(hex(0x08), "mainnet", 2, Provenance::Direct)
607 .with_address(Address::direct("h2", 2))
608 .with_address(Address::direct("h1", 1))
609 .with_flag("holepunch")
610 .with_flag("storage");
611 assert_eq!(
612 c.fingerprint_hash(),
613 d.fingerprint_hash(),
614 "address/flag insertion order must not affect fingerprint_hash"
615 );
616 assert_eq!(c.fingerprint(), d.fingerprint());
617 }
618
619 #[test]
620 fn entry_round_trips_through_json() {
621 let e = PeerEntry::new(hex(0x07), "mainnet", 1_719_763_200, Provenance::Direct)
622 .with_address(Address::direct("203.0.113.7", 9444))
623 .with_flag("storage")
624 .with_flag("holepunch");
625 let json = serde_json::to_string(&e).unwrap();
626 assert!(json.contains("\"peer_id\":"));
627 assert!(json.contains("\"via\":\"direct\""));
628 assert!(json.contains("\"kind\":\"direct\""));
629 let back: PeerEntry = serde_json::from_str(&json).unwrap();
630 assert_eq!(e, back);
631 }
632}