1use crate::models::{
5 AuthenticatorDetailsV1, AuxiliaryDetailsV1, BinaryBuildInformationOwned, DeclaredRolesV1,
6 DescribedNodeTypeV1, HostInformationV1, HostKeysV1, IpPacketRouterDetailsV1,
7 LewesProtocolDetailsV1, NetworkRequesterDetailsV1, NymNodeDataV1, NymNodeDescriptionV1,
8 OffsetDateTimeJsonSchemaWrapper, SphinxKeyV1, WebSocketsV1, WireguardDetailsV1,
9};
10use crate::nym_nodes::{
11 BasicEntryInformation, NodeRole, SemiSkimmedNodeV1, SemiSkimmedNodeV3, SkimmedNodeV1,
12};
13use nym_crypto::asymmetric::{ed25519, x25519};
14use nym_mixnet_contract_common::reward_params::Performance;
15use nym_mixnet_contract_common::NodeId;
16use nym_network_defaults::{DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT};
17use nym_noise_keys::VersionedNoiseKeyV1;
18use serde::{Deserialize, Serialize};
19use tracing::warn;
20use utoipa::ToSchema;
21
22pub type HostInformationV2 = HostInformationV1;
24pub type DeclaredRolesV2 = DeclaredRolesV1;
25pub type AuxiliaryDetailsV2 = AuxiliaryDetailsV1;
26pub type NetworkRequesterDetailsV2 = NetworkRequesterDetailsV1;
27pub type IpPacketRouterDetailsV2 = IpPacketRouterDetailsV1;
28pub type AuthenticatorDetailsV2 = AuthenticatorDetailsV1;
29pub type WireguardDetailsV2 = WireguardDetailsV1;
30pub type WebSocketsV2 = WebSocketsV1;
31pub type DescribedNodeTypeV2 = DescribedNodeTypeV1;
32pub type HostKeysV2 = HostKeysV1;
33pub type SphinxKeyV2 = SphinxKeyV1;
34pub type VersionedNoiseKeyV2 = VersionedNoiseKeyV1;
35
36#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
41pub struct NymNodeDescriptionV2 {
42 #[schema(value_type = u32)]
43 pub node_id: NodeId,
44 pub contract_node_type: DescribedNodeTypeV2,
45 pub description: NymNodeDataV2,
46}
47
48impl NymNodeDescriptionV2 {
49 pub fn version(&self) -> &str {
50 &self.description.build_information.build_version
51 }
52
53 pub fn entry_information(&self) -> BasicEntryInformation {
54 BasicEntryInformation {
55 hostname: self.description.host_information.hostname.clone(),
56 ws_port: self.description.mixnet_websockets.ws_port,
57 wss_port: self.description.mixnet_websockets.wss_port,
58 }
59 }
60
61 pub fn ed25519_identity_key(&self) -> ed25519::PublicKey {
62 self.description.host_information.keys.ed25519
63 }
64
65 pub fn current_sphinx_key(&self, current_rotation_id: u32) -> x25519::PublicKey {
66 let keys = &self.description.host_information.keys;
67
68 if keys.current_x25519_sphinx_key.rotation_id == u32::MAX {
69 return keys.current_x25519_sphinx_key.public_key;
71 }
72
73 if current_rotation_id == keys.current_x25519_sphinx_key.rotation_id {
74 return keys.current_x25519_sphinx_key.public_key;
76 }
77
78 if let Some(pre_announced) = &keys.pre_announced_x25519_sphinx_key {
79 if pre_announced.rotation_id == current_rotation_id {
80 return pre_announced.public_key;
81 }
82 }
83
84 warn!(
85 "unexpected key rotation {current_rotation_id} for node {}",
86 self.node_id
87 );
88 keys.current_x25519_sphinx_key.public_key
90 }
91
92 pub fn to_skimmed_node(
93 &self,
94 current_rotation_id: u32,
95 role: NodeRole,
96 performance: Performance,
97 ) -> SkimmedNodeV1 {
98 let keys = &self.description.host_information.keys;
99 let entry = if self.description.declared_role.entry {
100 Some(self.entry_information())
101 } else {
102 None
103 };
104
105 SkimmedNodeV1 {
106 node_id: self.node_id,
107 ed25519_identity_pubkey: keys.ed25519,
108 ip_addresses: self.description.host_information.ip_address.clone(),
109 mix_port: self.description.mix_port(),
110 x25519_sphinx_pubkey: self.current_sphinx_key(current_rotation_id),
111 role,
115 supported_roles: self.description.declared_role,
116 entry,
117 performance,
118 }
119 }
120
121 pub fn to_semi_skimmed_node(
122 &self,
123 current_rotation_id: u32,
124 role: NodeRole,
125 performance: Performance,
126 ) -> SemiSkimmedNodeV1 {
127 let skimmed_node = self.to_skimmed_node(current_rotation_id, role, performance);
128
129 SemiSkimmedNodeV1 {
130 basic: skimmed_node,
131 x25519_noise_versioned_key: self
132 .description
133 .host_information
134 .keys
135 .x25519_versioned_noise,
136 }
137 }
138
139 pub fn to_semi_skimmed_node_v3(
140 &self,
141 current_rotation_id: u32,
142 role: NodeRole,
143 performance: Performance,
144 ) -> SemiSkimmedNodeV3 {
145 let skimmed_node = self.to_skimmed_node(current_rotation_id, role, performance);
146
147 SemiSkimmedNodeV3 {
148 basic: skimmed_node,
149 noise_key: self
150 .description
151 .host_information
152 .keys
153 .x25519_versioned_noise,
154 build_version: self.description.build_information.build_version.clone(),
155 lp: self.description.lewes_protocol.clone(),
156 }
157 }
158}
159
160#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
165pub struct NymNodeDataV2 {
166 #[serde(default)]
167 pub last_polled: OffsetDateTimeJsonSchemaWrapper,
168
169 pub host_information: HostInformationV2,
170
171 #[serde(default)]
172 pub declared_role: DeclaredRolesV2,
173
174 #[serde(default)]
175 pub auxiliary_details: AuxiliaryDetailsV2,
176
177 pub build_information: BinaryBuildInformationOwned,
179
180 #[serde(default)]
181 pub network_requester: Option<NetworkRequesterDetailsV2>,
182
183 #[serde(default)]
184 pub ip_packet_router: Option<IpPacketRouterDetailsV2>,
185
186 #[serde(default)]
187 pub authenticator: Option<AuthenticatorDetailsV2>,
188
189 #[serde(default)]
190 pub wireguard: Option<WireguardDetailsV2>,
191
192 pub mixnet_websockets: WebSocketsV2,
194
195 #[serde(default)]
196 pub lewes_protocol: Option<LewesProtocolDetailsV1>,
197}
198
199impl NymNodeDataV2 {
200 pub fn mix_port(&self) -> u16 {
201 self.auxiliary_details
202 .announce_ports
203 .mix_port
204 .unwrap_or(DEFAULT_MIX_LISTENING_PORT)
205 }
206
207 pub fn verloc_port(&self) -> u16 {
208 self.auxiliary_details
209 .announce_ports
210 .verloc_port
211 .unwrap_or(DEFAULT_VERLOC_LISTENING_PORT)
212 }
213}
214
215impl From<NymNodeDataV2> for NymNodeDataV1 {
216 fn from(data: NymNodeDataV2) -> Self {
217 NymNodeDataV1 {
218 last_polled: data.last_polled,
219 host_information: data.host_information,
220 declared_role: data.declared_role,
221 auxiliary_details: data.auxiliary_details,
222 build_information: data.build_information,
223 network_requester: data.network_requester,
224 ip_packet_router: data.ip_packet_router,
225 authenticator: data.authenticator,
226 wireguard: data.wireguard,
227 mixnet_websockets: data.mixnet_websockets,
228 }
229 }
230}
231
232impl From<NymNodeDataV1> for NymNodeDataV2 {
233 fn from(data: NymNodeDataV1) -> Self {
234 NymNodeDataV2 {
235 last_polled: data.last_polled,
236 host_information: data.host_information,
237 declared_role: data.declared_role,
238 auxiliary_details: data.auxiliary_details,
239 build_information: data.build_information,
240 network_requester: data.network_requester,
241 ip_packet_router: data.ip_packet_router,
242 authenticator: data.authenticator,
243 wireguard: data.wireguard,
244 mixnet_websockets: data.mixnet_websockets,
245 lewes_protocol: Default::default(),
246 }
247 }
248}
249
250impl From<NymNodeDescriptionV2> for NymNodeDescriptionV1 {
251 fn from(value: NymNodeDescriptionV2) -> Self {
252 NymNodeDescriptionV1 {
253 node_id: value.node_id,
254 contract_node_type: value.contract_node_type,
255 description: value.description.into(),
256 }
257 }
258}
259
260impl From<NymNodeDescriptionV1> for NymNodeDescriptionV2 {
261 fn from(value: NymNodeDescriptionV1) -> Self {
262 NymNodeDescriptionV2 {
263 node_id: value.node_id,
264 contract_node_type: value.contract_node_type,
265 description: value.description.into(),
266 }
267 }
268}
269
270#[cfg(any(test, feature = "mock-fixtures"))]
271pub fn mock_nym_node_description(seed: u64) -> NymNodeDescriptionV2 {
272 use nym_node_requests::api::v1::lewes_protocol::models::{LPHashFunction, LPKEM};
273 use nym_test_utils::helpers::{u64_seeded_rng, RngCore};
274
275 let mut rng = u64_seeded_rng(seed);
276
277 let ed25519 = ed25519::KeyPair::new(&mut rng);
278
279 let x25519 = x25519::KeyPair::new(&mut rng);
281
282 let mut dummy_kems = std::collections::BTreeMap::new();
283 for kem in [LPKEM::McEliece, LPKEM::McEliece] {
284 let mut kem_digests = std::collections::BTreeMap::new();
285 for (i, sf) in [
286 LPHashFunction::Blake3,
287 LPHashFunction::Shake128,
288 LPHashFunction::Shake256,
289 LPHashFunction::Sha256,
290 ]
291 .iter()
292 .enumerate()
293 {
294 kem_digests.insert(*sf, hex::encode([((seed + i as u64) % 256) as u8; 32]));
295 }
296 dummy_kems.insert(kem, kem_digests);
297 }
298
299 let dummy_lp = nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol {
301 enabled: false,
302 control_port: 123,
303 data_port: 345,
304 x25519: (*x25519.public_key()).into(),
305 kem_keys: dummy_kems,
306 };
307 let dummy_signed_lp =
308 nym_node_requests::api::SignedLewesProtocol::new(dummy_lp, ed25519.private_key()).unwrap();
309
310 NymNodeDescriptionV2 {
311 node_id: rng.next_u32(),
312 contract_node_type: DescribedNodeTypeV1::NymNode,
313 description: NymNodeDataV2 {
314 last_polled: time::OffsetDateTime::from_unix_timestamp(1767225600)
315 .unwrap()
316 .into(),
317 host_information: HostInformationV2 {
318 ip_address: vec![
319 std::net::IpAddr::V4(std::net::Ipv4Addr::new(1, 2, 3, (seed % 255) as u8)),
320 ],
321 hostname: Some(format!("my-awesome-node-{seed}.com")),
322 keys: HostKeysV2 {
323 ed25519: *ed25519.public_key(),
324 x25519: *x25519.public_key(),
325 current_x25519_sphinx_key: SphinxKeyV2 {
326 rotation_id: 123,
327 public_key: *x25519.public_key(),
328 },
329 pre_announced_x25519_sphinx_key: None,
330 x25519_versioned_noise: Some(VersionedNoiseKeyV2 {
331 supported_version: nym_noise_keys::NoiseVersion::V1,
332 x25519_pubkey: *x25519.public_key(),
333 }),
334 },
335 },
336 declared_role: DeclaredRolesV2 {
337 mixnode: false,
338 entry: true,
339 exit_nr: true,
340 exit_ipr: true,
341 },
342 auxiliary_details: AuxiliaryDetailsV2 {
343 location: Some(celes::Country::switzerland()),
344 announce_ports: Default::default(),
345 accepted_operator_terms_and_conditions: true,
346 },
347 build_information: BinaryBuildInformationOwned {
348 binary_name: "dummy-node".to_string(),
349 build_timestamp: "2021-02-23T20:14:46.558472672+00:00".to_string(),
350 build_version: "0.1.0-9-g46f83e1".to_string(),
351 commit_sha: "46f83e112520533338245862d366f6a02cef07d4".to_string(),
352 commit_timestamp: "2021-02-23T08:08:02-05:00".to_string(),
353 commit_branch: "master".to_string(),
354 rustc_version: "1.52.0-nightly".to_string(),
355 rustc_channel: "nightly".to_string(),
356 cargo_profile: "release".to_string(),
357 cargo_triple: "wasm32-unknown-unknown".to_string(),
358 },
359 network_requester: Some(NetworkRequesterDetailsV2 {
360 address: "FhtkzizQg2JbZ19kGkRKXdjV2QnFbT5ww88ZAKaD4nkF.7Remi4UVYzn1yL3qYtEcQBGh6tzTYxMdYB4uqyHVc5Z4@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve".to_string(),
361 uses_exit_policy: true,
362 }),
363 ip_packet_router: Some(IpPacketRouterDetailsV2 {
364 address: "FhtkzizQg2JbZ19kGkRKXdjV2QnFbT5ww88ZAKaD4nkF.7Remi4UVYzn1yL3qYtEcQBGh6tzTYxMdYB4uqyHVc5Z4@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve".to_string(),
365 }),
366 authenticator: Some(AuthenticatorDetailsV2 {
367 address: "FhtkzizQg2JbZ19kGkRKXdjV2QnFbT5ww88ZAKaD4nkF.7Remi4UVYzn1yL3qYtEcQBGh6tzTYxMdYB4uqyHVc5Z4@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve".to_string(),
368 }),
369 wireguard: Some(WireguardDetailsV2 {
370 port: 123,
371 tunnel_port: 234,
372 metadata_port: 456,
373 public_key: x25519.public_key().to_base58_string(),
374 }),
375 lewes_protocol: Some(dummy_signed_lp.into()),
376 mixnet_websockets: WebSocketsV2 {
377 ws_port: 9000,
378 wss_port: None,
379 },
380 },
381 }
382}