Skip to main content

nym_api_requests/models/described/
v2.rs

1// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::models::described::type_translation::{
5    AnnouncePortsV1, AuthenticatorDetailsV1, DeclaredRolesV1, HostInformationV1, HostKeysV1,
6    IpPacketRouterDetailsV1, LewesProtocolDetailsV1, NetworkRequesterDetailsV1,
7    NymNodeAuxiliaryDetailsV1, SphinxKeyV1, WebSocketsV1, WireguardDetailsV1,
8};
9use crate::models::described::v1::{DescribedNodeTypeV1, NymNodeDataV1, NymNodeDescriptionV1};
10use crate::models::{BinaryBuildInformationOwned, OffsetDateTimeJsonSchemaWrapper};
11use crate::nym_nodes::{BasicEntryInformation, NodeRole, SkimmedNodeV1};
12use nym_crypto::asymmetric::{ed25519, x25519};
13use nym_mixnet_contract_common::reward_params::Performance;
14use nym_mixnet_contract_common::NodeId;
15use nym_network_defaults::{DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT};
16use nym_noise_keys::VersionedNoiseKeyV1;
17use serde::{Deserialize, Serialize};
18use tracing::warn;
19use utoipa::ToSchema;
20
21// no changes for the following types
22pub type HostInformationV2 = HostInformationV1;
23pub type DeclaredRolesV2 = DeclaredRolesV1;
24pub type AnnouncePortsV2 = AnnouncePortsV1;
25pub type NymNodeAuxiliaryDetailsV2 = NymNodeAuxiliaryDetailsV1;
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// to whoever is thinking of modifying this struct.
37// you MUST NOT change its structure in any way - adding, removing or changing fields
38// otherwise, it will break old clients as bincode serialisation is not backwards compatible
39// even if you put `#[serde(default)]` all over the place
40#[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            // legacy case (i.e. node doesn't support rotation)
70            return keys.current_x25519_sphinx_key.public_key;
71        }
72
73        if current_rotation_id == keys.current_x25519_sphinx_key.rotation_id {
74            // it's the 'current' key
75            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        // this should never be reached, but just in case, return the fallback option
89        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            // we can't use the declared roles, we have to take whatever was provided in the contract.
112            // why? say this node COULD operate as an exit, but it might be the case the contract decided
113            // to assign it an ENTRY role only. we have to use that one instead.
114            role,
115            supported_roles: self.description.declared_role,
116            entry,
117            performance,
118        }
119    }
120}
121
122// to whoever is thinking of modifying this struct.
123// you MUST NOT change its structure in any way - adding, removing or changing fields
124// otherwise, it will break old clients as bincode serialisation is not backwards compatible
125// even if you put `#[serde(default)]` all over the place
126#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
127pub struct NymNodeDataV2 {
128    #[serde(default)]
129    pub last_polled: OffsetDateTimeJsonSchemaWrapper,
130
131    pub host_information: HostInformationV2,
132
133    #[serde(default)]
134    pub declared_role: DeclaredRolesV2,
135
136    #[serde(default)]
137    pub auxiliary_details: NymNodeAuxiliaryDetailsV2,
138
139    // TODO: do we really care about ALL build info or just the version?
140    pub build_information: BinaryBuildInformationOwned,
141
142    #[serde(default)]
143    pub network_requester: Option<NetworkRequesterDetailsV2>,
144
145    #[serde(default)]
146    pub ip_packet_router: Option<IpPacketRouterDetailsV2>,
147
148    #[serde(default)]
149    pub authenticator: Option<AuthenticatorDetailsV2>,
150
151    #[serde(default)]
152    pub wireguard: Option<WireguardDetailsV2>,
153
154    // for now we only care about their ws/wss situation, nothing more
155    pub mixnet_websockets: WebSocketsV2,
156
157    #[serde(default)]
158    pub lewes_protocol: Option<LewesProtocolDetailsV1>,
159}
160
161impl NymNodeDataV2 {
162    pub fn mix_port(&self) -> u16 {
163        self.auxiliary_details
164            .announce_ports
165            .mix_port
166            .unwrap_or(DEFAULT_MIX_LISTENING_PORT)
167    }
168
169    pub fn verloc_port(&self) -> u16 {
170        self.auxiliary_details
171            .announce_ports
172            .verloc_port
173            .unwrap_or(DEFAULT_VERLOC_LISTENING_PORT)
174    }
175}
176
177impl From<NymNodeDataV2> for NymNodeDataV1 {
178    fn from(data: NymNodeDataV2) -> Self {
179        NymNodeDataV1 {
180            last_polled: data.last_polled,
181            host_information: data.host_information,
182            declared_role: data.declared_role,
183            auxiliary_details: data.auxiliary_details,
184            build_information: data.build_information,
185            network_requester: data.network_requester,
186            ip_packet_router: data.ip_packet_router,
187            authenticator: data.authenticator,
188            wireguard: data.wireguard,
189            mixnet_websockets: data.mixnet_websockets,
190        }
191    }
192}
193
194impl From<NymNodeDataV1> for NymNodeDataV2 {
195    fn from(data: NymNodeDataV1) -> Self {
196        NymNodeDataV2 {
197            last_polled: data.last_polled,
198            host_information: data.host_information,
199            declared_role: data.declared_role,
200            auxiliary_details: data.auxiliary_details,
201            build_information: data.build_information,
202            network_requester: data.network_requester,
203            ip_packet_router: data.ip_packet_router,
204            authenticator: data.authenticator,
205            wireguard: data.wireguard,
206            mixnet_websockets: data.mixnet_websockets,
207            lewes_protocol: Default::default(),
208        }
209    }
210}
211
212impl From<NymNodeDescriptionV2> for NymNodeDescriptionV1 {
213    fn from(value: NymNodeDescriptionV2) -> Self {
214        NymNodeDescriptionV1 {
215            node_id: value.node_id,
216            contract_node_type: value.contract_node_type,
217            description: value.description.into(),
218        }
219    }
220}
221
222impl From<NymNodeDescriptionV1> for NymNodeDescriptionV2 {
223    fn from(value: NymNodeDescriptionV1) -> Self {
224        NymNodeDescriptionV2 {
225            node_id: value.node_id,
226            contract_node_type: value.contract_node_type,
227            description: value.description.into(),
228        }
229    }
230}