use bitflags::bitflags;
use serde::{Deserialize, Serialize};
pub const PROTOCOL_VERSION: u32 = 5;
pub const MIN_SUPPORTED_VERSION: u32 = 5;
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Capabilities: u32 {
const RELAY = 0b0000_0001;
const EXIT_NODE = 0b0000_0010;
const STORAGE = 0b0000_0100;
const ALL = Self::RELAY.bits() | Self::EXIT_NODE.bits() | Self::STORAGE.bits();
}
}
impl Default for Capabilities {
fn default() -> Self {
Self::RELAY
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Handshake {
pub version: u32,
pub capabilities: Capabilities,
pub routing_key: String,
#[serde(default)]
pub eth_address: Option<String>,
#[serde(default = "default_payload_version")]
pub payload_version_min: u8,
#[serde(default = "default_payload_version")]
pub payload_version_max: u8,
#[serde(default)]
pub identity_sig: Option<String>,
}
fn default_payload_version() -> u8 {
1
}
impl Handshake {
#[must_use]
pub fn new(routing_key: String, capabilities: Capabilities, eth_address: String) -> Self {
use crate::models::payloads::PAYLOAD_VERSION;
Self {
version: PROTOCOL_VERSION,
capabilities,
routing_key,
eth_address: Some(eth_address),
payload_version_min: PAYLOAD_VERSION,
payload_version_max: PAYLOAD_VERSION,
identity_sig: None,
}
}
#[must_use]
pub fn with_identity_sig(mut self, sig_hex: String) -> Self {
self.identity_sig = Some(sig_hex);
self
}
#[must_use]
pub fn is_compatible(&self) -> bool {
self.version >= MIN_SUPPORTED_VERSION && self.version <= PROTOCOL_VERSION
}
#[must_use]
pub fn has_capability(&self, cap: Capabilities) -> bool {
self.capabilities.contains(cap)
}
}
#[derive(Debug, Clone)]
pub struct PeerInfo {
pub connected_at: std::time::Instant,
pub capabilities: Capabilities,
pub routing_key: String,
pub eth_address: Option<String>,
pub registry_verified: bool,
}
impl PeerInfo {
#[must_use]
pub fn from_handshake(handshake: &Handshake, registry_verified: bool) -> Self {
Self {
connected_at: std::time::Instant::now(),
capabilities: handshake.capabilities,
routing_key: handshake.routing_key.clone(),
eth_address: handshake.eth_address.clone(),
registry_verified,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_handshake_creation() {
let hs = Handshake::new(
"routing_key_hex".to_string(),
Capabilities::RELAY | Capabilities::EXIT_NODE,
"0x1234567890abcdef1234567890abcdef12345678".to_string(),
);
assert_eq!(hs.version, PROTOCOL_VERSION);
assert!(hs.has_capability(Capabilities::RELAY));
assert!(hs.has_capability(Capabilities::EXIT_NODE));
assert!(!hs.has_capability(Capabilities::STORAGE));
assert_eq!(
hs.eth_address.as_deref(),
Some("0x1234567890abcdef1234567890abcdef12345678")
);
}
#[test]
fn test_version_compatibility() {
let hs = Handshake::new("key".to_string(), Capabilities::RELAY, "0xaddr".to_string());
assert!(hs.is_compatible());
let mut old_hs = hs.clone();
old_hs.version = 4;
assert!(!old_hs.is_compatible());
let mut current_hs = hs.clone();
current_hs.version = PROTOCOL_VERSION;
assert!(current_hs.is_compatible());
let mut future_hs = hs.clone();
future_hs.version = PROTOCOL_VERSION + 1;
assert!(!future_hs.is_compatible());
}
#[test]
fn test_capabilities_bitflags() {
let caps = Capabilities::RELAY | Capabilities::EXIT_NODE;
assert!(caps.contains(Capabilities::RELAY));
assert!(caps.contains(Capabilities::EXIT_NODE));
assert!(!caps.contains(Capabilities::STORAGE));
}
#[test]
fn test_serialization() {
let hs = Handshake::new(
"def456".to_string(),
Capabilities::ALL,
"0xabcdef".to_string(),
);
let json = serde_json::to_string(&hs).unwrap();
let parsed: Handshake = serde_json::from_str(&json).unwrap();
assert_eq!(hs, parsed);
}
#[test]
fn test_legacy_deserialization() {
let json = r#"{"version":2,"capabilities":"RELAY","routing_key":"abc123"}"#;
let parsed: Handshake = serde_json::from_str(json).unwrap();
assert_eq!(parsed.version, 2);
assert!(parsed.eth_address.is_none());
assert!(parsed.identity_sig.is_none());
assert!(!parsed.is_compatible());
let json =
r#"{"version":3,"capabilities":"RELAY","routing_key":"abc123","eth_address":"0xabc"}"#;
let parsed: Handshake = serde_json::from_str(json).unwrap();
assert_eq!(parsed.version, 3);
assert_eq!(parsed.eth_address.as_deref(), Some("0xabc"));
assert!(parsed.identity_sig.is_none());
assert!(!parsed.is_compatible()); }
#[test]
fn test_v5_handshake_has_identity_sig() {
let hs = Handshake::new("key".to_string(), Capabilities::RELAY, "0xaddr".to_string())
.with_identity_sig("deadbeef".to_string());
assert_eq!(hs.version, PROTOCOL_VERSION);
assert_eq!(hs.identity_sig.as_deref(), Some("deadbeef"));
assert!(hs.is_compatible());
}
#[test]
fn test_peer_info_from_handshake() {
let hs = Handshake::new(
"routing_key".to_string(),
Capabilities::RELAY,
"0xaddr".to_string(),
);
let info = PeerInfo::from_handshake(&hs, true);
assert!(info.registry_verified);
assert_eq!(info.eth_address.as_deref(), Some("0xaddr"));
let info_unverified = PeerInfo::from_handshake(&hs, false);
assert!(!info_unverified.registry_verified);
}
}