use serde::{Deserialize, Serialize};
use crate::caps::{PEX_MAX_ADDRESSES, PEX_MAX_ENTRY_AGE, PEX_MAX_FLAGS, PEX_MAX_FLAG_LEN};
use crate::error::EntrySkip;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AddressKind {
Direct,
Mapped,
Reflexive,
Relay,
#[serde(other)]
#[default]
Unknown,
}
impl AddressKind {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
AddressKind::Direct => "direct",
AddressKind::Mapped => "mapped",
AddressKind::Reflexive => "reflexive",
AddressKind::Relay => "relay",
AddressKind::Unknown => "unknown",
}
}
#[must_use]
pub fn is_registered(self) -> bool {
!matches!(self, AddressKind::Unknown)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Provenance {
Direct,
Relay,
Introducer,
#[serde(other)]
#[default]
Unknown,
}
impl Provenance {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Provenance::Direct => "direct",
Provenance::Relay => "relay",
Provenance::Introducer => "introducer",
Provenance::Unknown => "unknown",
}
}
#[must_use]
pub fn is_registered(self) -> bool {
!matches!(self, Provenance::Unknown)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Address {
#[serde(default)]
pub host: String,
#[serde(default)]
pub port: u16,
#[serde(default)]
pub kind: AddressKind,
}
impl Address {
#[must_use]
pub fn direct(host: impl Into<String>, port: u16) -> Self {
Address {
host: host.into(),
port,
kind: AddressKind::Direct,
}
}
#[must_use]
pub fn new(host: impl Into<String>, port: u16, kind: AddressKind) -> Self {
Address {
host: host.into(),
port,
kind,
}
}
#[must_use]
pub fn is_valid(&self) -> bool {
!self.host.is_empty() && self.port != 0 && self.kind.is_registered()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PeerEntry {
#[serde(default)]
pub peer_id: String,
#[serde(default)]
pub addresses: Vec<Address>,
#[serde(default)]
pub network_id: String,
#[serde(default)]
pub last_seen: u64,
#[serde(default)]
pub via: Provenance,
#[serde(default)]
pub flags: Vec<String>,
}
impl PeerEntry {
#[must_use]
pub fn new(
peer_id: impl Into<String>,
network_id: impl Into<String>,
last_seen: u64,
via: Provenance,
) -> Self {
PeerEntry {
peer_id: peer_id.into(),
addresses: Vec::new(),
network_id: network_id.into(),
last_seen,
via,
flags: Vec::new(),
}
}
#[must_use]
pub fn with_address(mut self, addr: Address) -> Self {
self.addresses.push(addr);
self
}
#[must_use]
pub fn with_flag(mut self, flag: impl Into<String>) -> Self {
self.flags.push(flag.into());
self
}
pub fn validate(&self, ctx: &ValidateCtx<'_>) -> Result<(), EntrySkip> {
if !is_hex64(&self.peer_id) {
return Err(EntrySkip::BadPeerId);
}
if self.peer_id == ctx.receiver_peer_id || self.peer_id == ctx.sender_peer_id {
return Err(EntrySkip::SelfOrPartner);
}
if self.addresses.len() > PEX_MAX_ADDRESSES {
return Err(EntrySkip::TooManyAddresses);
}
if self.addresses.iter().any(|a| !a.is_valid()) {
return Err(EntrySkip::BadAddress);
}
if self.flags.len() > PEX_MAX_FLAGS || self.flags.iter().any(|f| f.len() > PEX_MAX_FLAG_LEN)
{
return Err(EntrySkip::TooManyFlags);
}
if self.network_id != ctx.network_id {
return Err(EntrySkip::NetworkMismatch);
}
if !self.via.is_registered() {
return Err(EntrySkip::BadVia);
}
if self.last_seen < ctx.now_secs && ctx.now_secs - self.last_seen > PEX_MAX_ENTRY_AGE {
return Err(EntrySkip::TooOld);
}
Ok(())
}
#[must_use]
pub fn clamped(&self, now_secs: u64) -> PeerEntry {
let mut e = self.clone();
if e.last_seen > now_secs {
e.last_seen = now_secs;
}
e
}
#[must_use]
pub fn fingerprint(&self) -> String {
let mut addrs: Vec<String> = self
.addresses
.iter()
.map(|a| format!("{}|{}|{}", a.host, a.port, a.kind.as_str()))
.collect();
addrs.sort();
let mut flags = self.flags.clone();
flags.sort();
format!("{}#{}", addrs.join(","), flags.join(","))
}
#[must_use]
pub fn fingerprint_hash(&self) -> u64 {
use std::hash::{Hash, Hasher};
let mut addrs: Vec<&Address> = self.addresses.iter().collect();
addrs.sort_by(|a, b| {
(a.host.as_str(), a.port, a.kind.as_str()).cmp(&(
b.host.as_str(),
b.port,
b.kind.as_str(),
))
});
let mut flags: Vec<&str> = self.flags.iter().map(String::as_str).collect();
flags.sort_unstable();
let mut hasher = std::collections::hash_map::DefaultHasher::new();
addrs.len().hash(&mut hasher);
for a in &addrs {
a.host.hash(&mut hasher);
a.port.hash(&mut hasher);
a.kind.as_str().hash(&mut hasher);
}
flags.len().hash(&mut hasher);
for f in &flags {
f.hash(&mut hasher);
}
hasher.finish()
}
}
#[derive(Debug, Clone, Copy)]
pub struct ValidateCtx<'a> {
pub receiver_peer_id: &'a str,
pub sender_peer_id: &'a str,
pub network_id: &'a str,
pub now_secs: u64,
}
#[must_use]
pub fn is_hex64(s: &str) -> bool {
s.len() == 64
&& s.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
}
#[cfg(test)]
mod tests {
use super::*;
fn hex(b: u8) -> String {
format!("{b:02x}").repeat(32)
}
fn ctx<'a>(recv: &'a str, send: &'a str, net: &'a str, now: u64) -> ValidateCtx<'a> {
ValidateCtx {
receiver_peer_id: recv,
sender_peer_id: send,
network_id: net,
now_secs: now,
}
}
#[test]
fn hex64_recognizer() {
assert!(is_hex64(&"a".repeat(64)));
assert!(is_hex64(&hex(0xab)));
assert!(!is_hex64(&"a".repeat(63)));
assert!(!is_hex64(&"A".repeat(64))); assert!(!is_hex64(&"g".repeat(64))); }
#[test]
fn kind_and_via_tokens_are_frozen_lowercase() {
assert_eq!(
serde_json::to_string(&AddressKind::Direct).unwrap(),
"\"direct\""
);
assert_eq!(
serde_json::to_string(&AddressKind::Relay).unwrap(),
"\"relay\""
);
assert_eq!(
serde_json::to_string(&Provenance::Introducer).unwrap(),
"\"introducer\""
);
}
#[test]
fn unknown_kind_and_via_decode_to_catch_all() {
let a: Address = serde_json::from_str(r#"{"host":"h","port":1,"kind":"quantum"}"#).unwrap();
assert_eq!(a.kind, AddressKind::Unknown);
let e: PeerEntry = serde_json::from_str(
r#"{"peer_id":"x","addresses":[],"network_id":"n","last_seen":1,"via":"teleport"}"#,
)
.unwrap();
assert_eq!(e.via, Provenance::Unknown);
}
#[test]
fn valid_entry_passes() {
let e = PeerEntry::new(hex(0x07), "mainnet", 1000, Provenance::Direct)
.with_address(Address::direct("203.0.113.7", 9444))
.with_flag("storage");
assert!(e
.validate(&ctx(&hex(0x01), &hex(0x02), "mainnet", 1000))
.is_ok());
}
#[test]
fn skip_reasons_match_spec() {
let recv = hex(0x01);
let send = hex(0x02);
let e = PeerEntry::new("nothex", "mainnet", 10, Provenance::Direct);
assert_eq!(
e.validate(&ctx(&recv, &send, "mainnet", 10)),
Err(EntrySkip::BadPeerId)
);
let e = PeerEntry::new(recv.clone(), "mainnet", 10, Provenance::Direct);
assert_eq!(
e.validate(&ctx(&recv, &send, "mainnet", 10)),
Err(EntrySkip::SelfOrPartner)
);
let e = PeerEntry::new(send.clone(), "mainnet", 10, Provenance::Direct);
assert_eq!(
e.validate(&ctx(&recv, &send, "mainnet", 10)),
Err(EntrySkip::SelfOrPartner)
);
let e = PeerEntry::new(hex(0x07), "mainnet", 10, Provenance::Direct)
.with_address(Address::new("h", 0, AddressKind::Direct));
assert_eq!(
e.validate(&ctx(&recv, &send, "mainnet", 10)),
Err(EntrySkip::BadAddress)
);
let e = PeerEntry::new(hex(0x07), "mainnet", 10, Provenance::Direct)
.with_address(Address::new("h", 1, AddressKind::Unknown));
assert_eq!(
e.validate(&ctx(&recv, &send, "mainnet", 10)),
Err(EntrySkip::BadAddress)
);
let e = PeerEntry::new(hex(0x07), "testnet", 10, Provenance::Direct);
assert_eq!(
e.validate(&ctx(&recv, &send, "mainnet", 10)),
Err(EntrySkip::NetworkMismatch)
);
let e = PeerEntry::new(hex(0x07), "mainnet", 10, Provenance::Unknown);
assert_eq!(
e.validate(&ctx(&recv, &send, "mainnet", 10)),
Err(EntrySkip::BadVia)
);
let e = PeerEntry::new(hex(0x07), "mainnet", 10, Provenance::Direct);
assert_eq!(
e.validate(&ctx(&recv, &send, "mainnet", 2000)),
Err(EntrySkip::TooOld)
);
}
#[test]
fn too_many_addresses_and_flags() {
let recv = hex(0x01);
let send = hex(0x02);
let mut e = PeerEntry::new(hex(0x07), "mainnet", 10, Provenance::Direct);
for i in 0..9 {
e = e.with_address(Address::direct("h", 1000 + i));
}
assert_eq!(
e.validate(&ctx(&recv, &send, "mainnet", 10)),
Err(EntrySkip::TooManyAddresses)
);
let mut e = PeerEntry::new(hex(0x07), "mainnet", 10, Provenance::Direct);
for i in 0..9 {
e = e.with_flag(format!("f{i}"));
}
assert_eq!(
e.validate(&ctx(&recv, &send, "mainnet", 10)),
Err(EntrySkip::TooManyFlags)
);
}
#[test]
fn future_last_seen_is_clamped_not_skipped() {
let recv = hex(0x01);
let send = hex(0x02);
let e = PeerEntry::new(hex(0x07), "mainnet", 5000, Provenance::Direct);
assert!(e.validate(&ctx(&recv, &send, "mainnet", 1000)).is_ok());
assert_eq!(e.clamped(1000).last_seen, 1000);
assert_eq!(e.clamped(6000).last_seen, 5000);
}
#[test]
fn fingerprint_ignores_last_seen_but_tracks_addresses_and_flags() {
let a = PeerEntry::new(hex(0x07), "mainnet", 100, Provenance::Direct)
.with_address(Address::direct("h", 1))
.with_flag("storage");
let a2 = PeerEntry::new(hex(0x07), "mainnet", 999, Provenance::Direct)
.with_address(Address::direct("h", 1))
.with_flag("storage");
assert_eq!(
a.fingerprint(),
a2.fingerprint(),
"last_seen must not affect fingerprint"
);
let b = a2.clone().with_flag("holepunch");
assert_ne!(
a.fingerprint(),
b.fingerprint(),
"a flag change must change fingerprint"
);
}
#[test]
fn fingerprint_hash_matches_fingerprint_equality_semantics() {
let a = PeerEntry::new(hex(0x07), "mainnet", 100, Provenance::Direct)
.with_address(Address::direct("h", 1))
.with_flag("storage");
let a2 = PeerEntry::new(hex(0x07), "mainnet", 999, Provenance::Direct)
.with_address(Address::direct("h", 1))
.with_flag("storage");
assert_eq!(
a.fingerprint_hash(),
a2.fingerprint_hash(),
"last_seen must not affect fingerprint_hash"
);
assert_eq!(
a.fingerprint() == a2.fingerprint(),
a.fingerprint_hash() == a2.fingerprint_hash(),
"fingerprint_hash must agree with fingerprint on equality"
);
let b = a2.clone().with_flag("holepunch");
assert_ne!(
a.fingerprint_hash(),
b.fingerprint_hash(),
"a flag change must change fingerprint_hash"
);
assert_eq!(
a.fingerprint() == b.fingerprint(),
a.fingerprint_hash() == b.fingerprint_hash(),
"fingerprint_hash must agree with fingerprint on inequality"
);
let c = PeerEntry::new(hex(0x08), "mainnet", 1, Provenance::Direct)
.with_address(Address::direct("h1", 1))
.with_address(Address::direct("h2", 2))
.with_flag("storage")
.with_flag("holepunch");
let d = PeerEntry::new(hex(0x08), "mainnet", 2, Provenance::Direct)
.with_address(Address::direct("h2", 2))
.with_address(Address::direct("h1", 1))
.with_flag("holepunch")
.with_flag("storage");
assert_eq!(
c.fingerprint_hash(),
d.fingerprint_hash(),
"address/flag insertion order must not affect fingerprint_hash"
);
assert_eq!(c.fingerprint(), d.fingerprint());
}
#[test]
fn entry_round_trips_through_json() {
let e = PeerEntry::new(hex(0x07), "mainnet", 1_719_763_200, Provenance::Direct)
.with_address(Address::direct("203.0.113.7", 9444))
.with_flag("storage")
.with_flag("holepunch");
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"peer_id\":"));
assert!(json.contains("\"via\":\"direct\""));
assert!(json.contains("\"kind\":\"direct\""));
let back: PeerEntry = serde_json::from_str(&json).unwrap();
assert_eq!(e, back);
}
}