1use crate::models::described::type_translation::LewesProtocolDetailsV1;
5use crate::models::described::v1::NymNodeDescriptionV1;
6use crate::models::described::v2::{
7 AnnouncePortsV2, AuthenticatorDetailsV2, DeclaredRolesV2, DescribedNodeTypeV2,
8 HostInformationV2, HostKeysV2, IpPacketRouterDetailsV2, NetworkRequesterDetailsV2,
9 NymNodeAuxiliaryDetailsV2, NymNodeDataV2, NymNodeDescriptionV2, SphinxKeyV2,
10 VersionedNoiseKeyV2, WebSocketsV2, WireguardDetailsV2,
11};
12use crate::models::{BinaryBuildInformationOwned, OffsetDateTimeJsonSchemaWrapper};
13use crate::nym_nodes::{
14 BasicEntryInformation, NodeRole, SemiSkimmedNodeV1, SemiSkimmedNodeV3, SkimmedNodeV1,
15};
16use celes::Country;
17use nym_crypto::asymmetric::{ed25519, x25519};
18use nym_mixnet_contract_common::reward_params::Performance;
19use nym_mixnet_contract_common::NodeId;
20use nym_network_defaults::{DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT};
21use serde::{Deserialize, Serialize};
22use tracing::warn;
23use utoipa::ToSchema;
24
25pub type AnnouncePortsV3 = AnnouncePortsV2;
27pub type HostInformationV3 = HostInformationV2;
28pub type DeclaredRolesV3 = DeclaredRolesV2;
29pub type NetworkRequesterDetailsV3 = NetworkRequesterDetailsV2;
30pub type IpPacketRouterDetailsV3 = IpPacketRouterDetailsV2;
31pub type AuthenticatorDetailsV3 = AuthenticatorDetailsV2;
32pub type WireguardDetailsV3 = WireguardDetailsV2;
33pub type WebSocketsV3 = WebSocketsV2;
34pub type DescribedNodeTypeV3 = DescribedNodeTypeV2;
35pub type HostKeysV3 = HostKeysV2;
36pub type SphinxKeyV3 = SphinxKeyV2;
37pub type VersionedNoiseKeyV3 = VersionedNoiseKeyV2;
38pub type LewesProtocolDetailsV3 = LewesProtocolDetailsV1;
39
40#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
41pub struct NymNodeDescriptionV3 {
42 #[schema(value_type = u32)]
43 pub node_id: NodeId,
44 pub contract_node_type: DescribedNodeTypeV3,
45 pub description: NymNodeDataV3,
46}
47
48impl NymNodeDescriptionV3 {
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
160impl From<NymNodeDescriptionV3> for NymNodeDescriptionV2 {
161 fn from(value: NymNodeDescriptionV3) -> Self {
162 NymNodeDescriptionV2 {
163 node_id: value.node_id,
164 contract_node_type: value.contract_node_type,
165 description: value.description.into(),
166 }
167 }
168}
169
170impl From<NymNodeDescriptionV3> for NymNodeDescriptionV1 {
171 fn from(value: NymNodeDescriptionV3) -> Self {
172 NymNodeDescriptionV1 {
173 node_id: value.node_id,
174 contract_node_type: value.contract_node_type,
175 description: NymNodeDataV2::from(value.description).into(),
176 }
177 }
178}
179
180#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
181pub struct NymNodeDataV3 {
182 #[serde(default)]
183 pub last_polled: OffsetDateTimeJsonSchemaWrapper,
184
185 pub host_information: HostInformationV3,
186
187 #[serde(default)]
188 pub declared_role: DeclaredRolesV3,
189
190 #[serde(default)]
191 pub auxiliary_details: NymNodeAuxiliaryDetailsV3,
192
193 pub build_information: BinaryBuildInformationOwned,
195
196 #[serde(default)]
197 pub network_requester: Option<NetworkRequesterDetailsV3>,
198
199 #[serde(default)]
200 pub ip_packet_router: Option<IpPacketRouterDetailsV3>,
201
202 #[serde(default)]
203 pub authenticator: Option<AuthenticatorDetailsV3>,
204
205 #[serde(default)]
206 pub wireguard: Option<WireguardDetailsV3>,
207
208 pub mixnet_websockets: WebSocketsV3,
210
211 #[serde(default)]
212 pub lewes_protocol: Option<LewesProtocolDetailsV3>,
213}
214
215impl NymNodeDataV3 {
216 pub fn mix_port(&self) -> u16 {
217 self.auxiliary_details
218 .announce_ports
219 .mix_port
220 .unwrap_or(DEFAULT_MIX_LISTENING_PORT)
221 }
222
223 pub fn verloc_port(&self) -> u16 {
224 self.auxiliary_details
225 .announce_ports
226 .verloc_port
227 .unwrap_or(DEFAULT_VERLOC_LISTENING_PORT)
228 }
229}
230
231impl From<NymNodeDataV3> for NymNodeDataV2 {
232 fn from(data: NymNodeDataV3) -> Self {
233 NymNodeDataV2 {
234 last_polled: data.last_polled,
235 host_information: data.host_information,
236 declared_role: data.declared_role,
237 auxiliary_details: data.auxiliary_details.into(),
238 build_information: data.build_information,
239 network_requester: data.network_requester,
240 ip_packet_router: data.ip_packet_router,
241 authenticator: data.authenticator,
242 wireguard: data.wireguard,
243 mixnet_websockets: data.mixnet_websockets,
244 lewes_protocol: data.lewes_protocol,
245 }
246 }
247}
248
249#[derive(
250 Clone, Debug, Default, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq,
251)]
252pub struct NymNodeAuxiliaryDetailsV3 {
253 #[schema(example = "PL", value_type = Option<String>)]
255 #[schemars(with = "Option<String>")]
256 #[schemars(length(equal = 2))]
257 pub location: Option<Country>,
258
259 #[serde(default)]
261 pub address: Option<String>,
262
263 #[serde(default)]
264 pub announce_ports: AnnouncePortsV3,
265
266 #[serde(default)]
270 pub accepted_operator_terms_and_conditions: bool,
271}
272
273impl From<NymNodeAuxiliaryDetailsV3> for NymNodeAuxiliaryDetailsV2 {
274 fn from(value: NymNodeAuxiliaryDetailsV3) -> Self {
275 NymNodeAuxiliaryDetailsV2 {
276 location: value.location,
277 announce_ports: value.announce_ports,
278 accepted_operator_terms_and_conditions: value.accepted_operator_terms_and_conditions,
279 }
280 }
281}
282
283#[cfg(any(test, feature = "mock-fixtures"))]
284pub fn mock_nym_node_description(seed: u64) -> NymNodeDescriptionV3 {
285 use nym_node_requests::api::v1::lewes_protocol::models::{LPHashFunction, LPKEM};
286 use nym_test_utils::helpers::{u64_seeded_rng, RngCore};
287
288 let mut rng = u64_seeded_rng(seed);
289
290 let ed25519 = nym_crypto::asymmetric::ed25519::KeyPair::new(&mut rng);
291
292 let x25519 = nym_crypto::asymmetric::x25519::KeyPair::new(&mut rng);
294
295 let mut dummy_kems = std::collections::BTreeMap::new();
296 for kem in [LPKEM::McEliece, LPKEM::McEliece] {
297 let mut kem_digests = std::collections::BTreeMap::new();
298 for (i, sf) in [
299 LPHashFunction::Blake3,
300 LPHashFunction::Shake128,
301 LPHashFunction::Shake256,
302 LPHashFunction::Sha256,
303 ]
304 .iter()
305 .enumerate()
306 {
307 kem_digests.insert(*sf, hex::encode([((seed + i as u64) % 256) as u8; 32]));
308 }
309 dummy_kems.insert(kem, kem_digests);
310 }
311
312 let dummy_lp = nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol {
314 enabled: false,
315 control_port: 123,
316 data_port: 345,
317 x25519: (*x25519.public_key()).into(),
318 kem_keys: dummy_kems,
319 };
320 let dummy_signed_lp =
321 nym_node_requests::api::SignedLewesProtocol::new(dummy_lp, ed25519.private_key()).unwrap();
322
323 NymNodeDescriptionV3 {
324 node_id: rng.next_u32(),
325 contract_node_type: DescribedNodeTypeV3::NymNode,
326 description: NymNodeDataV3 {
327 last_polled: time::OffsetDateTime::from_unix_timestamp(1767225600)
328 .unwrap()
329 .into(),
330 host_information: HostInformationV3 {
331 ip_address: vec![
332 std::net::IpAddr::V4(std::net::Ipv4Addr::new(1, 2, 3, (seed % 255) as u8)),
333 ],
334 hostname: Some(format!("my-awesome-node-{seed}.com")),
335 keys: HostKeysV3 {
336 ed25519: *ed25519.public_key(),
337 x25519: *x25519.public_key(),
338 current_x25519_sphinx_key: SphinxKeyV3 {
339 rotation_id: 123,
340 public_key: *x25519.public_key(),
341 },
342 pre_announced_x25519_sphinx_key: None,
343 x25519_versioned_noise: Some(VersionedNoiseKeyV3 {
344 supported_version: nym_noise_keys::NoiseVersion::V1,
345 x25519_pubkey: *x25519.public_key(),
346 }),
347 },
348 },
349 declared_role: DeclaredRolesV3 {
350 mixnode: false,
351 entry: true,
352 exit_nr: true,
353 exit_ipr: true,
354 },
355 auxiliary_details: NymNodeAuxiliaryDetailsV3 {
356 location: Some(celes::Country::switzerland()),
357 address: Some("n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf".to_string()),
358 announce_ports: Default::default(),
359 accepted_operator_terms_and_conditions: true,
360 },
361 build_information: BinaryBuildInformationOwned {
362 binary_name: "dummy-node".to_string(),
363 build_timestamp: "2021-02-23T20:14:46.558472672+00:00".to_string(),
364 build_version: "0.1.0-9-g46f83e1".to_string(),
365 commit_sha: "46f83e112520533338245862d366f6a02cef07d4".to_string(),
366 commit_timestamp: "2021-02-23T08:08:02-05:00".to_string(),
367 commit_branch: "master".to_string(),
368 rustc_version: "1.52.0-nightly".to_string(),
369 rustc_channel: "nightly".to_string(),
370 cargo_profile: "release".to_string(),
371 cargo_triple: "wasm32-unknown-unknown".to_string(),
372 },
373 network_requester: Some(NetworkRequesterDetailsV3 {
374 address: "FhtkzizQg2JbZ19kGkRKXdjV2QnFbT5ww88ZAKaD4nkF.7Remi4UVYzn1yL3qYtEcQBGh6tzTYxMdYB4uqyHVc5Z4@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve".to_string(),
375 uses_exit_policy: true,
376 }),
377 ip_packet_router: Some(IpPacketRouterDetailsV3 {
378 address: "FhtkzizQg2JbZ19kGkRKXdjV2QnFbT5ww88ZAKaD4nkF.7Remi4UVYzn1yL3qYtEcQBGh6tzTYxMdYB4uqyHVc5Z4@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve".to_string(),
379 }),
380 authenticator: Some(AuthenticatorDetailsV3 {
381 address: "FhtkzizQg2JbZ19kGkRKXdjV2QnFbT5ww88ZAKaD4nkF.7Remi4UVYzn1yL3qYtEcQBGh6tzTYxMdYB4uqyHVc5Z4@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve".to_string(),
382 }),
383 wireguard: Some(WireguardDetailsV3 {
384 port: 123,
385 tunnel_port: 234,
386 metadata_port: 456,
387 public_key: x25519.public_key().to_base58_string(),
388 }),
389 lewes_protocol: Some(dummy_signed_lp.into()),
390 mixnet_websockets: WebSocketsV3 {
391 ws_port: 9000,
392 wss_port: None,
393 },
394 },
395 }
396}