nym_api_requests/models/
described.rs

1// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::models::{BinaryBuildInformationOwned, OffsetDateTimeJsonSchemaWrapper};
5use crate::nym_nodes::{BasicEntryInformation, NodeRole, SemiSkimmedNode, SkimmedNode};
6use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey;
7use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey;
8use nym_crypto::asymmetric::{ed25519, x25519};
9use nym_mixnet_contract_common::reward_params::Performance;
10use nym_mixnet_contract_common::NodeId;
11use nym_network_defaults::{
12    DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, WG_METADATA_PORT, WG_TUNNEL_PORT,
13};
14use nym_node_requests::api::v1::authenticator::models::Authenticator;
15use nym_node_requests::api::v1::gateway::models::Wireguard;
16use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter;
17use nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol;
18use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, NodeRoles};
19use nym_noise_keys::VersionedNoiseKey;
20use serde::{Deserialize, Serialize};
21use std::net::IpAddr;
22use tracing::warn;
23use utoipa::ToSchema;
24
25#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
26pub struct HostInformation {
27    #[schema(value_type = Vec<String>)]
28    pub ip_address: Vec<IpAddr>,
29    pub hostname: Option<String>,
30    pub keys: HostKeys,
31}
32
33impl From<nym_node_requests::api::v1::node::models::HostInformation> for HostInformation {
34    fn from(value: nym_node_requests::api::v1::node::models::HostInformation) -> Self {
35        HostInformation {
36            ip_address: value.ip_address,
37            hostname: value.hostname,
38            keys: value.keys.into(),
39        }
40    }
41}
42
43#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
44pub struct HostKeys {
45    #[serde(with = "bs58_ed25519_pubkey")]
46    #[schemars(with = "String")]
47    #[schema(value_type = String)]
48    pub ed25519: ed25519::PublicKey,
49
50    #[deprecated(note = "use the current_x25519_sphinx_key with explicit rotation information")]
51    #[serde(with = "bs58_x25519_pubkey")]
52    #[schemars(with = "String")]
53    #[schema(value_type = String)]
54    pub x25519: x25519::PublicKey,
55
56    pub current_x25519_sphinx_key: SphinxKey,
57
58    #[serde(default)]
59    pub pre_announced_x25519_sphinx_key: Option<SphinxKey>,
60
61    #[serde(default)]
62    pub x25519_versioned_noise: Option<VersionedNoiseKey>,
63}
64
65#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
66pub struct SphinxKey {
67    pub rotation_id: u32,
68
69    #[serde(with = "bs58_x25519_pubkey")]
70    #[schemars(with = "String")]
71    #[schema(value_type = String)]
72    pub public_key: x25519::PublicKey,
73}
74
75impl From<nym_node_requests::api::v1::node::models::SphinxKey> for SphinxKey {
76    fn from(value: nym_node_requests::api::v1::node::models::SphinxKey) -> Self {
77        SphinxKey {
78            rotation_id: value.rotation_id,
79            public_key: value.public_key,
80        }
81    }
82}
83
84impl From<nym_node_requests::api::v1::node::models::HostKeys> for HostKeys {
85    fn from(value: nym_node_requests::api::v1::node::models::HostKeys) -> Self {
86        HostKeys {
87            ed25519: value.ed25519_identity,
88            x25519: value.x25519_sphinx,
89            current_x25519_sphinx_key: value.primary_x25519_sphinx_key.into(),
90            pre_announced_x25519_sphinx_key: value.pre_announced_x25519_sphinx_key.map(Into::into),
91            x25519_versioned_noise: value.x25519_versioned_noise,
92        }
93    }
94}
95
96#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
97pub struct WebSockets {
98    pub ws_port: u16,
99
100    pub wss_port: Option<u16>,
101}
102
103impl From<nym_node_requests::api::v1::gateway::models::WebSockets> for WebSockets {
104    fn from(value: nym_node_requests::api::v1::gateway::models::WebSockets) -> Self {
105        WebSockets {
106            ws_port: value.ws_port,
107            wss_port: value.wss_port,
108        }
109    }
110}
111
112#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
113pub struct NoiseDetails {
114    pub key: VersionedNoiseKey,
115
116    pub mixnet_port: u16,
117
118    #[schema(value_type = Vec<String>)]
119    pub ip_addresses: Vec<IpAddr>,
120}
121
122#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
123pub struct NymNodeDescription {
124    #[schema(value_type = u32)]
125    pub node_id: NodeId,
126    pub contract_node_type: DescribedNodeType,
127    pub description: NymNodeData,
128}
129
130impl NymNodeDescription {
131    pub fn version(&self) -> &str {
132        &self.description.build_information.build_version
133    }
134
135    pub fn entry_information(&self) -> BasicEntryInformation {
136        BasicEntryInformation {
137            hostname: self.description.host_information.hostname.clone(),
138            ws_port: self.description.mixnet_websockets.ws_port,
139            wss_port: self.description.mixnet_websockets.wss_port,
140        }
141    }
142
143    pub fn ed25519_identity_key(&self) -> ed25519::PublicKey {
144        self.description.host_information.keys.ed25519
145    }
146
147    pub fn current_sphinx_key(&self, current_rotation_id: u32) -> x25519::PublicKey {
148        let keys = &self.description.host_information.keys;
149
150        if keys.current_x25519_sphinx_key.rotation_id == u32::MAX {
151            // legacy case (i.e. node doesn't support rotation)
152            return keys.current_x25519_sphinx_key.public_key;
153        }
154
155        if current_rotation_id == keys.current_x25519_sphinx_key.rotation_id {
156            // it's the 'current' key
157            return keys.current_x25519_sphinx_key.public_key;
158        }
159
160        if let Some(pre_announced) = &keys.pre_announced_x25519_sphinx_key {
161            if pre_announced.rotation_id == current_rotation_id {
162                return pre_announced.public_key;
163            }
164        }
165
166        warn!(
167            "unexpected key rotation {current_rotation_id} for node {}",
168            self.node_id
169        );
170        // this should never be reached, but just in case, return the fallback option
171        keys.current_x25519_sphinx_key.public_key
172    }
173
174    pub fn to_skimmed_node(
175        &self,
176        current_rotation_id: u32,
177        role: NodeRole,
178        performance: Performance,
179    ) -> SkimmedNode {
180        let keys = &self.description.host_information.keys;
181        let entry = if self.description.declared_role.entry {
182            Some(self.entry_information())
183        } else {
184            None
185        };
186
187        SkimmedNode {
188            node_id: self.node_id,
189            ed25519_identity_pubkey: keys.ed25519,
190            ip_addresses: self.description.host_information.ip_address.clone(),
191            mix_port: self.description.mix_port(),
192            x25519_sphinx_pubkey: self.current_sphinx_key(current_rotation_id),
193            // we can't use the declared roles, we have to take whatever was provided in the contract.
194            // why? say this node COULD operate as an exit, but it might be the case the contract decided
195            // to assign it an ENTRY role only. we have to use that one instead.
196            role,
197            supported_roles: self.description.declared_role,
198            entry,
199            performance,
200        }
201    }
202
203    pub fn to_semi_skimmed_node(
204        &self,
205        current_rotation_id: u32,
206        role: NodeRole,
207        performance: Performance,
208    ) -> SemiSkimmedNode {
209        let skimmed_node = self.to_skimmed_node(current_rotation_id, role, performance);
210
211        SemiSkimmedNode {
212            basic: skimmed_node,
213            x25519_noise_versioned_key: self
214                .description
215                .host_information
216                .keys
217                .x25519_versioned_noise,
218        }
219    }
220}
221
222#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
223#[serde(rename_all = "snake_case")]
224#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
225#[cfg_attr(
226    feature = "generate-ts",
227    ts(
228        export,
229        export_to = "ts-packages/types/src/types/rust/DescribedNodeType.ts"
230    )
231)]
232pub enum DescribedNodeType {
233    LegacyMixnode,
234    LegacyGateway,
235    NymNode,
236}
237
238impl DescribedNodeType {
239    pub fn is_nym_node(&self) -> bool {
240        matches!(self, DescribedNodeType::NymNode)
241    }
242}
243
244#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
245#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
246#[cfg_attr(
247    feature = "generate-ts",
248    ts(
249        export,
250        export_to = "ts-packages/types/src/types/rust/DeclaredRoles.ts"
251    )
252)]
253pub struct DeclaredRoles {
254    pub mixnode: bool,
255    pub entry: bool,
256    pub exit_nr: bool,
257    pub exit_ipr: bool,
258}
259
260impl DeclaredRoles {
261    pub fn can_operate_exit_gateway(&self) -> bool {
262        self.exit_ipr && self.exit_nr
263    }
264}
265
266impl From<NodeRoles> for DeclaredRoles {
267    fn from(value: NodeRoles) -> Self {
268        DeclaredRoles {
269            mixnode: value.mixnode_enabled,
270            entry: value.gateway_enabled,
271            exit_nr: value.gateway_enabled && value.network_requester_enabled,
272            exit_ipr: value.gateway_enabled && value.ip_packet_router_enabled,
273        }
274    }
275}
276
277#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
278pub struct NetworkRequesterDetails {
279    /// address of the embedded network requester
280    pub address: String,
281
282    /// flag indicating whether this network requester uses the exit policy rather than the deprecated allow list
283    pub uses_exit_policy: bool,
284}
285
286#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
287pub struct IpPacketRouterDetails {
288    /// address of the embedded ip packet router
289    pub address: String,
290}
291
292// works for current simple case.
293impl From<IpPacketRouter> for IpPacketRouterDetails {
294    fn from(value: IpPacketRouter) -> Self {
295        IpPacketRouterDetails {
296            address: value.address,
297        }
298    }
299}
300
301#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
302pub struct AuthenticatorDetails {
303    /// address of the embedded authenticator
304    pub address: String,
305}
306
307// works for current simple case.
308impl From<Authenticator> for AuthenticatorDetails {
309    fn from(value: Authenticator) -> Self {
310        AuthenticatorDetails {
311            address: value.address,
312        }
313    }
314}
315#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
316pub struct WireguardDetails {
317    // NOTE: the port field is deprecated in favour of tunnel_port
318    pub port: u16,
319    #[serde(default = "default_tunnel_port")]
320    pub tunnel_port: u16,
321    #[serde(default = "default_metadata_port")]
322    pub metadata_port: u16,
323    pub public_key: String,
324}
325
326fn default_tunnel_port() -> u16 {
327    WG_TUNNEL_PORT
328}
329fn default_metadata_port() -> u16 {
330    WG_METADATA_PORT
331}
332
333// works for current simple case.
334impl From<Wireguard> for WireguardDetails {
335    fn from(value: Wireguard) -> Self {
336        WireguardDetails {
337            port: value.port,
338            tunnel_port: value.tunnel_port,
339            metadata_port: value.metadata_port,
340            public_key: value.public_key,
341        }
342    }
343}
344
345#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
346pub struct LewesProtocolDetails {
347    /// Helper field that specifies whether the LP listener(s) is enabled on this node.
348    /// It is directly controlled by the node's role (i.e. it is enabled if it supports 'entry' mode)
349    pub enabled: bool,
350
351    /// LP TCP control address (default: 41264) for establishing LP sessions
352    pub control_port: u16,
353
354    /// LP UDP data address (default: 51264) for Sphinx packets wrapped in LP
355    pub data_port: u16,
356}
357
358impl From<LewesProtocol> for LewesProtocolDetails {
359    fn from(value: LewesProtocol) -> Self {
360        LewesProtocolDetails {
361            enabled: value.enabled,
362            control_port: value.control_port,
363            data_port: value.data_port,
364        }
365    }
366}
367
368// this struct is getting quite bloated...
369#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
370pub struct NymNodeData {
371    #[serde(default)]
372    pub last_polled: OffsetDateTimeJsonSchemaWrapper,
373
374    pub host_information: HostInformation,
375
376    #[serde(default)]
377    pub declared_role: DeclaredRoles,
378
379    #[serde(default)]
380    pub auxiliary_details: AuxiliaryDetails,
381
382    // TODO: do we really care about ALL build info or just the version?
383    pub build_information: BinaryBuildInformationOwned,
384
385    #[serde(default)]
386    pub network_requester: Option<NetworkRequesterDetails>,
387
388    #[serde(default)]
389    pub ip_packet_router: Option<IpPacketRouterDetails>,
390
391    #[serde(default)]
392    pub authenticator: Option<AuthenticatorDetails>,
393
394    #[serde(default)]
395    pub wireguard: Option<WireguardDetails>,
396
397    #[serde(default)]
398    pub lewes_protocol: Option<LewesProtocolDetails>,
399
400    // for now we only care about their ws/wss situation, nothing more
401    pub mixnet_websockets: WebSockets,
402}
403
404impl NymNodeData {
405    pub fn mix_port(&self) -> u16 {
406        self.auxiliary_details
407            .announce_ports
408            .mix_port
409            .unwrap_or(DEFAULT_MIX_LISTENING_PORT)
410    }
411
412    pub fn verloc_port(&self) -> u16 {
413        self.auxiliary_details
414            .announce_ports
415            .verloc_port
416            .unwrap_or(DEFAULT_VERLOC_LISTENING_PORT)
417    }
418}