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