Skip to main content

nym_node_requests/api/v1/node/
models.rs

1// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use celes::Country;
5use nym_crypto::asymmetric::ed25519::{self, serde_helpers::bs58_ed25519_pubkey};
6use nym_crypto::asymmetric::x25519::{
7    self, serde_helpers::bs58_x25519_pubkey, serde_helpers::option_bs58_x25519_pubkey,
8};
9use nym_noise_keys::VersionedNoiseKeyV1;
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12use std::net::IpAddr;
13
14pub use crate::api::SignedHostInformation;
15use crate::api::v2::node::models::AuxiliaryDetailsV2;
16pub use nym_bin_common::build_information::BinaryBuildInformationOwned;
17
18#[derive(Clone, Default, Debug, Copy, Serialize, Deserialize, JsonSchema)]
19#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
20pub struct NodeRoles {
21    pub mixnode_enabled: bool,
22    pub gateway_enabled: bool,
23    pub network_requester_enabled: bool,
24    pub ip_packet_router_enabled: bool,
25}
26
27impl NodeRoles {
28    pub fn can_operate_mixnode(&self) -> bool {
29        self.mixnode_enabled
30    }
31
32    pub fn can_operate_entry_gateway(&self) -> bool {
33        self.gateway_enabled
34    }
35
36    pub fn can_operate_exit_gateway(&self) -> bool {
37        self.gateway_enabled && self.network_requester_enabled && self.ip_packet_router_enabled
38    }
39}
40
41#[derive(Clone, Copy, Default, Debug, Serialize, Deserialize, JsonSchema)]
42#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
43pub struct AnnouncePorts {
44    pub verloc_port: Option<u16>,
45    pub mix_port: Option<u16>,
46}
47
48#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
49#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
50pub struct HostInformation {
51    /// Ip address(es) of this host, such as `1.1.1.1`.
52    #[cfg_attr(feature = "openapi", schema(value_type = Vec<String>, format = Byte, example = json!(["1.1.1.1"])))]
53    pub ip_address: Vec<IpAddr>,
54
55    /// Optional hostname of this node, for example `nymtech.net`.
56    #[cfg_attr(feature = "openapi", schema(example = "nymtech.net"))]
57    pub hostname: Option<String>,
58
59    /// Public keys associated with this node.
60    pub keys: HostKeys,
61}
62
63impl HostInformation {
64    pub fn check_ips(&self) -> bool {
65        for ip in &self.ip_address {
66            if ip.is_unspecified() || ip.is_loopback() || ip.is_multicast() {
67                return false;
68            }
69        }
70        true
71    }
72}
73
74#[derive(Serialize)]
75pub struct LegacyHostInformationV3 {
76    pub ip_address: Vec<IpAddr>,
77    pub hostname: Option<String>,
78    pub keys: LegacyHostKeysV3,
79}
80
81#[derive(Serialize)]
82pub struct LegacyHostInformationV2 {
83    pub ip_address: Vec<IpAddr>,
84    pub hostname: Option<String>,
85    pub keys: LegacyHostKeysV2,
86}
87
88#[derive(Serialize)]
89pub struct LegacyHostInformationV1 {
90    pub ip_address: Vec<IpAddr>,
91    pub hostname: Option<String>,
92    pub keys: LegacyHostKeysV1,
93}
94
95impl From<HostInformation> for LegacyHostInformationV3 {
96    fn from(value: HostInformation) -> Self {
97        LegacyHostInformationV3 {
98            ip_address: value.ip_address,
99            hostname: value.hostname,
100            keys: value.keys.into(),
101        }
102    }
103}
104
105impl From<LegacyHostInformationV3> for LegacyHostInformationV2 {
106    fn from(value: LegacyHostInformationV3) -> Self {
107        LegacyHostInformationV2 {
108            ip_address: value.ip_address,
109            hostname: value.hostname,
110            keys: value.keys.into(),
111        }
112    }
113}
114
115impl From<LegacyHostInformationV2> for LegacyHostInformationV1 {
116    fn from(value: LegacyHostInformationV2) -> Self {
117        LegacyHostInformationV1 {
118            ip_address: value.ip_address,
119            hostname: value.hostname,
120            keys: value.keys.into(),
121        }
122    }
123}
124
125#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
126#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
127#[serde(from = "HostKeysDeHelper")]
128pub struct HostKeys {
129    /// Base58-encoded ed25519 public key of this node. Currently, it corresponds to either mixnode's or gateway's identity.
130    #[schemars(with = "String")]
131    #[cfg_attr(feature = "openapi", schema(value_type = String))]
132    #[serde(with = "bs58_ed25519_pubkey")]
133    pub ed25519_identity: ed25519::PublicKey,
134
135    #[deprecated(note = "use explicit primary_x25519_sphinx_key instead")]
136    #[schemars(with = "String")]
137    #[cfg_attr(feature = "openapi", schema(value_type = String))]
138    #[serde(with = "bs58_x25519_pubkey")]
139    pub x25519_sphinx: x25519::PublicKey,
140
141    /// Current, active, x25519 sphinx key clients are expected to use when constructing packets
142    /// with this node in the route.
143    pub primary_x25519_sphinx_key: SphinxKey,
144
145    /// Pre-announced x25519 sphinx key clients will use during the following key rotation
146    pub pre_announced_x25519_sphinx_key: Option<SphinxKey>,
147
148    /// Base58-encoded x25519 public key of this node used for the noise protocol.
149    #[serde(default)]
150    pub x25519_versioned_noise: Option<VersionedNoiseKeyV1>,
151}
152
153// we need the intermediate struct to help us with the new explicit sphinx key fields
154#[allow(deprecated)]
155impl From<HostKeysDeHelper> for HostKeys {
156    fn from(value: HostKeysDeHelper) -> Self {
157        let primary_x25519_sphinx_key = match value.primary_x25519_sphinx_key {
158            None => {
159                // legacy
160                SphinxKey::new_legacy(value.x25519_sphinx)
161            }
162            Some(primary_x25519_sphinx_key) => primary_x25519_sphinx_key,
163        };
164
165        HostKeys {
166            ed25519_identity: value.ed25519_identity,
167            x25519_sphinx: value.x25519_sphinx,
168            primary_x25519_sphinx_key,
169            pre_announced_x25519_sphinx_key: value.pre_announced_x25519_sphinx_key,
170            x25519_versioned_noise: value.x25519_versioned_noise,
171        }
172    }
173}
174
175#[derive(Debug, Serialize, Deserialize)]
176struct HostKeysDeHelper {
177    /// Base58-encoded ed25519 public key of this node. Currently, it corresponds to either mixnode's or gateway's identity.
178    #[serde(alias = "ed25519")]
179    #[serde(with = "bs58_ed25519_pubkey")]
180    pub ed25519_identity: ed25519::PublicKey,
181
182    #[deprecated(note = "use explicit primary_x25519_sphinx_key instead")]
183    #[serde(alias = "x25519")]
184    #[serde(with = "bs58_x25519_pubkey")]
185    pub x25519_sphinx: x25519::PublicKey,
186
187    /// Current, active, x25519 sphinx key clients are expected to use when constructing packets
188    /// with this node in the route.
189    pub primary_x25519_sphinx_key: Option<SphinxKey>,
190
191    /// Pre-announced x25519 sphinx key clients will use during the following key rotation
192    #[serde(default)]
193    pub pre_announced_x25519_sphinx_key: Option<SphinxKey>,
194
195    /// Base58-encoded x25519 public key of this node used for the noise protocol.
196    #[serde(default)]
197    pub x25519_versioned_noise: Option<VersionedNoiseKeyV1>,
198}
199
200#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
201#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
202pub struct SphinxKey {
203    pub rotation_id: u32,
204
205    #[serde(with = "bs58_x25519_pubkey")]
206    #[schemars(with = "String")]
207    #[cfg_attr(feature = "openapi", schema(value_type = String))]
208    pub public_key: x25519::PublicKey,
209}
210
211impl SphinxKey {
212    pub fn new_legacy(public_key: x25519::PublicKey) -> SphinxKey {
213        SphinxKey {
214            rotation_id: u32::MAX,
215            public_key,
216        }
217    }
218
219    pub fn is_legacy(&self) -> bool {
220        self.rotation_id == u32::MAX
221    }
222}
223
224#[derive(Serialize)]
225pub struct LegacyHostKeysV3 {
226    #[serde(alias = "ed25519")]
227    #[serde(with = "bs58_ed25519_pubkey")]
228    pub ed25519_identity: ed25519::PublicKey,
229
230    #[serde(alias = "x25519")]
231    #[serde(with = "bs58_x25519_pubkey")]
232    pub x25519_sphinx: x25519::PublicKey,
233
234    #[serde(default)]
235    #[serde(with = "option_bs58_x25519_pubkey")]
236    pub x25519_noise: Option<x25519::PublicKey>,
237}
238
239#[derive(Serialize)]
240pub struct LegacyHostKeysV2 {
241    pub ed25519_identity: String,
242    pub x25519_sphinx: String,
243    pub x25519_noise: String,
244}
245
246#[derive(Serialize)]
247pub struct LegacyHostKeysV1 {
248    pub ed25519: String,
249    pub x25519: String,
250}
251
252impl From<HostKeys> for LegacyHostKeysV3 {
253    fn from(value: HostKeys) -> Self {
254        LegacyHostKeysV3 {
255            ed25519_identity: value.ed25519_identity,
256            x25519_sphinx: value.primary_x25519_sphinx_key.public_key,
257            x25519_noise: value.x25519_versioned_noise.map(|k| k.x25519_pubkey),
258        }
259    }
260}
261
262impl From<LegacyHostKeysV3> for LegacyHostKeysV2 {
263    fn from(value: LegacyHostKeysV3) -> Self {
264        LegacyHostKeysV2 {
265            ed25519_identity: value.ed25519_identity.to_base58_string(),
266            x25519_sphinx: value.x25519_sphinx.to_base58_string(),
267            x25519_noise: value
268                .x25519_noise
269                .map(|k| k.to_base58_string())
270                .unwrap_or_default(),
271        }
272    }
273}
274
275impl From<LegacyHostKeysV2> for LegacyHostKeysV1 {
276    fn from(value: LegacyHostKeysV2) -> Self {
277        LegacyHostKeysV1 {
278            ed25519: value.ed25519_identity,
279            x25519: value.x25519_sphinx,
280        }
281    }
282}
283
284#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
285#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
286pub struct HostSystem {
287    /// Name of the operating system of the host machine.
288    pub system_name: Option<String>,
289
290    /// Version of the kernel of the host machine, if applicable.
291    pub kernel_version: Option<String>,
292
293    /// Version of the operating system of the host machine, if applicable.
294    pub os_version: Option<String>,
295
296    /// The CPU architecture of the host machine (eg. x86, amd64, aarch64, ...).
297    pub cpu_arch: Option<String>,
298
299    /// Hardware information of the host machine.
300    pub hardware: Option<Hardware>,
301}
302
303#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
304#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
305pub struct Hardware {
306    /// The information of the host CPU.
307    pub cpu: Vec<Cpu>,
308
309    /// Total memory, in bytes, available on the host.
310    pub total_memory: u64,
311
312    /// Detailed information about availability of crypto-specific instructions for future optimisations.
313    pub crypto: Option<CryptoHardware>,
314}
315
316#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
317#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
318pub struct Cpu {
319    pub name: String,
320
321    /// The CPU frequency in MHz.
322    pub frequency: u64,
323
324    pub vendor_id: String,
325
326    pub brand: String,
327}
328
329#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
330#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
331pub struct CryptoHardware {
332    /// Flag to indicate whether the host machine supports AES-NI x86 extension instruction set
333    pub aesni: bool,
334
335    /// Flag to indicate whether the host machine supports AVX2 x86 extension instruction set
336    pub avx2: bool,
337
338    /// Number of SMT logical processors available.
339    pub smt_logical_processor_count: Vec<u32>,
340
341    /// Flag to indicate whether the host machine supports OSXSAVE instruction
342    pub osxsave: bool,
343
344    /// Flag to indicate whether the host machine supports Intel Software Guard Extensions (SGX) set of instruction codes
345    pub sgx: bool,
346
347    /// Flag to indicate whether the host machine supports XSAVE instruction
348    pub xsave: bool,
349}
350
351#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
352#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
353pub struct NodeDescription {
354    /// moniker defines a human-readable name for the node.
355    pub moniker: String,
356
357    /// website defines an optional website link.
358    pub website: String,
359
360    /// security contact defines an optional email for security contact.
361    pub security_contact: String,
362
363    /// details define other optional details.
364    pub details: String,
365}
366
367/// Auxiliary details of the associated Nym Node.
368#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
369#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
370pub struct AuxiliaryDetailsV1 {
371    /// Optional ISO 3166 alpha-2 two-letter country code of the node's **physical** location
372    #[cfg_attr(feature = "openapi", schema(example = "PL", value_type = Option<String>))]
373    #[schemars(with = "Option<String>")]
374    #[schemars(length(equal = 2))]
375    pub location: Option<Country>,
376
377    #[serde(default)]
378    pub announce_ports: AnnouncePorts,
379
380    /// Specifies whether this node operator has agreed to the terms and conditions
381    /// as defined at <https://nymtech.net/terms-and-conditions/operators/v1.0.0>
382    // make sure to include the default deserialisation as this field hasn't existed when the struct was first created
383    #[serde(default)]
384    pub accepted_operator_terms_and_conditions: bool,
385}
386
387impl From<AuxiliaryDetailsV2> for AuxiliaryDetailsV1 {
388    fn from(v2: AuxiliaryDetailsV2) -> Self {
389        Self {
390            location: v2.location,
391            announce_ports: v2.announce_ports,
392            accepted_operator_terms_and_conditions: v2.accepted_operator_terms_and_conditions,
393        }
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    #[test]
402    fn legacy_host_information_deserialisation() {
403        let legacy_raw = r#"
404        {
405          "data": {
406            "ip_address": [
407              "194.182.184.55"
408            ],
409            "hostname": null,
410            "keys": {
411              "ed25519_identity": "2RMWm7PoadaoWpk3KhT2tcFFfA4oKUyC44KwmVvjxNDS",
412              "x25519_sphinx": "Awn4R2AHX91tYeiMJMxW3mFfoePuHWzZYUFdDQnydZCD",
413              "x25519_noise": null
414            }
415          },
416          "signature": "5JcXh766JANhz3bu2hMBS8onTLihQn6vnGgduJg1qd8JAcPGPbXBwBTKmmQPYCVGeZYFHW4CMGhfHVBu2A1rE5f7"
417        }
418        "#;
419
420        let res = serde_json::from_str::<SignedHostInformation>(legacy_raw);
421        assert!(res.is_ok());
422    }
423}