Skip to main content

nox_core/models/
handshake.rs

1//! Protocol handshake for version/capability exchange between nodes.
2
3use bitflags::bitflags;
4use serde::{Deserialize, Serialize};
5
6/// Current protocol version.
7/// v2: Removed `identity_key` (BJJ) from handshake -- unused dead code.
8/// v3: Added `eth_address` for on-chain registration verification (ISSUE-005).
9/// v4: Added `payload_version_min`/`payload_version_max` for wire-format versioning.
10/// v5: Added `identity_sig` -- secp256k1 signature proving ownership of `eth_address`.
11pub const PROTOCOL_VERSION: u32 = 5;
12
13/// Minimum protocol version this node will accept from peers.
14/// v5 is mandatory: all peers must provide `identity_sig` proving ETH key ownership.
15pub const MIN_SUPPORTED_VERSION: u32 = 5;
16
17bitflags! {
18    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19    pub struct Capabilities: u32 {
20        const RELAY     = 0b0000_0001;
21        const EXIT_NODE = 0b0000_0010;
22        const STORAGE   = 0b0000_0100;
23        const ALL = Self::RELAY.bits() | Self::EXIT_NODE.bits() | Self::STORAGE.bits();
24    }
25}
26
27impl Default for Capabilities {
28    fn default() -> Self {
29        Self::RELAY
30    }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct Handshake {
35    pub version: u32,
36    pub capabilities: Capabilities,
37    /// X25519 routing public key (hex-encoded), used for Sphinx packet encryption.
38    pub routing_key: String,
39    /// v3+: Ethereum address for on-chain `NoxRegistry` verification.
40    #[serde(default)]
41    pub eth_address: Option<String>,
42    #[serde(default = "default_payload_version")]
43    pub payload_version_min: u8,
44    #[serde(default = "default_payload_version")]
45    pub payload_version_max: u8,
46    /// v5+: secp256k1 signature proving ownership of `eth_address`.
47    /// 65 bytes [r(32) || s(32) || v(1)], hex-encoded.
48    #[serde(default)]
49    pub identity_sig: Option<String>,
50}
51
52fn default_payload_version() -> u8 {
53    1
54}
55
56impl Handshake {
57    #[must_use]
58    pub fn new(routing_key: String, capabilities: Capabilities, eth_address: String) -> Self {
59        use crate::models::payloads::PAYLOAD_VERSION;
60        Self {
61            version: PROTOCOL_VERSION,
62            capabilities,
63            routing_key,
64            eth_address: Some(eth_address),
65            payload_version_min: PAYLOAD_VERSION,
66            payload_version_max: PAYLOAD_VERSION,
67            identity_sig: None,
68        }
69    }
70
71    #[must_use]
72    pub fn with_identity_sig(mut self, sig_hex: String) -> Self {
73        self.identity_sig = Some(sig_hex);
74        self
75    }
76
77    #[must_use]
78    pub fn is_compatible(&self) -> bool {
79        self.version >= MIN_SUPPORTED_VERSION && self.version <= PROTOCOL_VERSION
80    }
81
82    #[must_use]
83    pub fn has_capability(&self, cap: Capabilities) -> bool {
84        self.capabilities.contains(cap)
85    }
86}
87
88#[derive(Debug, Clone)]
89pub struct PeerInfo {
90    pub connected_at: std::time::Instant,
91    pub capabilities: Capabilities,
92    pub routing_key: String,
93    pub eth_address: Option<String>,
94    pub registry_verified: bool,
95}
96
97impl PeerInfo {
98    #[must_use]
99    pub fn from_handshake(handshake: &Handshake, registry_verified: bool) -> Self {
100        Self {
101            connected_at: std::time::Instant::now(),
102            capabilities: handshake.capabilities,
103            routing_key: handshake.routing_key.clone(),
104            eth_address: handshake.eth_address.clone(),
105            registry_verified,
106        }
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_handshake_creation() {
116        let hs = Handshake::new(
117            "routing_key_hex".to_string(),
118            Capabilities::RELAY | Capabilities::EXIT_NODE,
119            "0x1234567890abcdef1234567890abcdef12345678".to_string(),
120        );
121
122        assert_eq!(hs.version, PROTOCOL_VERSION);
123        assert!(hs.has_capability(Capabilities::RELAY));
124        assert!(hs.has_capability(Capabilities::EXIT_NODE));
125        assert!(!hs.has_capability(Capabilities::STORAGE));
126        assert_eq!(
127            hs.eth_address.as_deref(),
128            Some("0x1234567890abcdef1234567890abcdef12345678")
129        );
130    }
131
132    #[test]
133    fn test_version_compatibility() {
134        let hs = Handshake::new("key".to_string(), Capabilities::RELAY, "0xaddr".to_string());
135        assert!(hs.is_compatible());
136
137        // Versions below v5 should be incompatible (MIN_SUPPORTED_VERSION = 5)
138        let mut old_hs = hs.clone();
139        old_hs.version = 4;
140        assert!(!old_hs.is_compatible());
141
142        // Current version should be compatible
143        let mut current_hs = hs.clone();
144        current_hs.version = PROTOCOL_VERSION;
145        assert!(current_hs.is_compatible());
146
147        // Version above current should be incompatible
148        let mut future_hs = hs.clone();
149        future_hs.version = PROTOCOL_VERSION + 1;
150        assert!(!future_hs.is_compatible());
151    }
152
153    #[test]
154    fn test_capabilities_bitflags() {
155        let caps = Capabilities::RELAY | Capabilities::EXIT_NODE;
156        assert!(caps.contains(Capabilities::RELAY));
157        assert!(caps.contains(Capabilities::EXIT_NODE));
158        assert!(!caps.contains(Capabilities::STORAGE));
159    }
160
161    #[test]
162    fn test_serialization() {
163        let hs = Handshake::new(
164            "def456".to_string(),
165            Capabilities::ALL,
166            "0xabcdef".to_string(),
167        );
168
169        let json = serde_json::to_string(&hs).unwrap();
170        let parsed: Handshake = serde_json::from_str(&json).unwrap();
171
172        assert_eq!(hs, parsed);
173    }
174
175    #[test]
176    fn test_legacy_deserialization() {
177        // Old handshake formats still deserialize (missing fields default).
178        // Note: v2/v3/v4 peers would be REJECTED by is_compatible() since MIN_SUPPORTED_VERSION=5,
179        // but deserialization itself must not fail.
180        let json = r#"{"version":2,"capabilities":"RELAY","routing_key":"abc123"}"#;
181        let parsed: Handshake = serde_json::from_str(json).unwrap();
182        assert_eq!(parsed.version, 2);
183        assert!(parsed.eth_address.is_none());
184        assert!(parsed.identity_sig.is_none());
185        assert!(!parsed.is_compatible()); // v2 < MIN_SUPPORTED_VERSION(5)
186
187        let json =
188            r#"{"version":3,"capabilities":"RELAY","routing_key":"abc123","eth_address":"0xabc"}"#;
189        let parsed: Handshake = serde_json::from_str(json).unwrap();
190        assert_eq!(parsed.version, 3);
191        assert_eq!(parsed.eth_address.as_deref(), Some("0xabc"));
192        assert!(parsed.identity_sig.is_none());
193        assert!(!parsed.is_compatible()); // v3 < MIN_SUPPORTED_VERSION(5)
194    }
195
196    #[test]
197    fn test_v5_handshake_has_identity_sig() {
198        let hs = Handshake::new("key".to_string(), Capabilities::RELAY, "0xaddr".to_string())
199            .with_identity_sig("deadbeef".to_string());
200        assert_eq!(hs.version, PROTOCOL_VERSION);
201        assert_eq!(hs.identity_sig.as_deref(), Some("deadbeef"));
202        assert!(hs.is_compatible());
203    }
204
205    #[test]
206    fn test_peer_info_from_handshake() {
207        let hs = Handshake::new(
208            "routing_key".to_string(),
209            Capabilities::RELAY,
210            "0xaddr".to_string(),
211        );
212        let info = PeerInfo::from_handshake(&hs, true);
213        assert!(info.registry_verified);
214        assert_eq!(info.eth_address.as_deref(), Some("0xaddr"));
215
216        let info_unverified = PeerInfo::from_handshake(&hs, false);
217        assert!(!info_unverified.registry_verified);
218    }
219}