nym_topology/
lib.rs

1// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use ::serde::{Deserialize, Serialize};
5use nym_api_requests::nym_nodes::SkimmedNode;
6use nym_crypto::asymmetric::ed25519;
7use nym_mixnet_contract_common::EpochId;
8use nym_sphinx_addressing::nodes::NodeIdentity;
9use nym_sphinx_types::Node as SphinxNode;
10use rand::prelude::IteratorRandom;
11use rand::{CryptoRng, Rng};
12use std::borrow::Borrow;
13use std::collections::{HashMap, HashSet};
14use std::fmt::Display;
15use std::net::IpAddr;
16use time::OffsetDateTime;
17use tracing::{debug, trace, warn};
18
19pub use crate::node::{EntryDetails, RoutingNode, SupportedRoles};
20pub use error::NymTopologyError;
21pub use nym_mixnet_contract_common::nym_node::Role;
22pub use nym_mixnet_contract_common::{EpochRewardedSet, NodeId};
23pub use rewarded_set::CachedEpochRewardedSet;
24
25pub mod error;
26pub mod node;
27pub mod rewarded_set;
28
29#[cfg(feature = "provider-trait")]
30pub mod provider_trait;
31#[cfg(feature = "wasm-serde-types")]
32pub mod wasm_helpers;
33
34#[cfg(feature = "provider-trait")]
35pub use provider_trait::{HardcodedTopologyProvider, TopologyProvider};
36
37#[deprecated]
38#[derive(Debug, Clone)]
39pub enum NetworkAddress {
40    IpAddr(IpAddr),
41    Hostname(String),
42}
43
44#[allow(deprecated)]
45mod deprecated_network_address_impls {
46    use crate::NetworkAddress;
47    use std::convert::Infallible;
48    use std::fmt::{Display, Formatter};
49    use std::net::{SocketAddr, ToSocketAddrs};
50    use std::str::FromStr;
51    use std::{fmt, io};
52
53    impl NetworkAddress {
54        pub fn as_hostname(self) -> Option<String> {
55            match self {
56                NetworkAddress::IpAddr(_) => None,
57                NetworkAddress::Hostname(s) => Some(s),
58            }
59        }
60    }
61
62    impl NetworkAddress {
63        pub fn to_socket_addrs(&self, port: u16) -> io::Result<Vec<SocketAddr>> {
64            match self {
65                NetworkAddress::IpAddr(addr) => Ok(vec![SocketAddr::new(*addr, port)]),
66                NetworkAddress::Hostname(hostname) => {
67                    Ok((hostname.as_str(), port).to_socket_addrs()?.collect())
68                }
69            }
70        }
71    }
72
73    impl FromStr for NetworkAddress {
74        type Err = Infallible;
75
76        fn from_str(s: &str) -> Result<Self, Self::Err> {
77            if let Ok(ip_addr) = s.parse() {
78                Ok(NetworkAddress::IpAddr(ip_addr))
79            } else {
80                Ok(NetworkAddress::Hostname(s.to_string()))
81            }
82        }
83    }
84
85    impl Display for NetworkAddress {
86        fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
87            match self {
88                NetworkAddress::IpAddr(ip_addr) => ip_addr.fmt(f),
89                NetworkAddress::Hostname(hostname) => hostname.fmt(f),
90            }
91        }
92    }
93}
94
95pub type MixLayer = u8;
96
97#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
98pub struct NymTopologyMetadata {
99    pub key_rotation_id: u32,
100    // we have to keep track of key rotation id anyway, so we might as well also include the epoch id
101    // to keep track of the data staleness
102    pub absolute_epoch_id: EpochId,
103
104    #[serde(with = "time::serde::rfc3339")]
105    pub refreshed_at: OffsetDateTime,
106}
107
108impl NymTopologyMetadata {
109    pub fn new(
110        key_rotation_id: u32,
111        absolute_epoch_id: EpochId,
112        refreshed_at: impl Into<OffsetDateTime>,
113    ) -> Self {
114        NymTopologyMetadata {
115            key_rotation_id,
116            absolute_epoch_id,
117            refreshed_at: refreshed_at.into(),
118        }
119    }
120}
121
122impl Default for NymTopologyMetadata {
123    fn default() -> Self {
124        // that's not ideal, but we don't want to break backwards compatibility : /
125        NymTopologyMetadata {
126            key_rotation_id: u32::MAX,
127            absolute_epoch_id: 0,
128            refreshed_at: OffsetDateTime::now_utc(),
129        }
130    }
131}
132
133#[derive(Clone, Debug, Default, Serialize, Deserialize)]
134pub struct NymTopology {
135    // while this is not ideal, use empty values as default to not break backwards compatibility
136    #[serde(default)]
137    metadata: NymTopologyMetadata,
138
139    // for the purposes of future VRF, everyone will need the same view of the network, regardless of performance filtering
140    // so we use the same 'master' rewarded set information for that
141    //
142    // how do we solve the problem of "we have to go through a node that we want to filter out?"
143    // ¯\_(ツ)_/¯ that's a future problem
144    rewarded_set: CachedEpochRewardedSet,
145
146    node_details: HashMap<NodeId, RoutingNode>,
147}
148
149#[derive(Clone, Debug, Default)]
150pub struct NymRouteProvider {
151    pub topology: NymTopology,
152
153    /// Allow constructing routes with final hop at nodes that are not entry/exit gateways in the current epoch
154    pub ignore_egress_epoch_roles: bool,
155}
156
157impl From<NymTopology> for NymRouteProvider {
158    fn from(topology: NymTopology) -> Self {
159        NymRouteProvider {
160            topology,
161            ignore_egress_epoch_roles: false,
162        }
163    }
164}
165
166impl NymRouteProvider {
167    pub fn new(topology: NymTopology, ignore_egress_epoch_roles: bool) -> Self {
168        NymRouteProvider {
169            topology,
170            ignore_egress_epoch_roles,
171        }
172    }
173
174    pub fn current_key_rotation(&self) -> u32 {
175        self.topology.metadata.key_rotation_id
176    }
177
178    pub fn absolute_epoch_id(&self) -> EpochId {
179        self.topology.metadata.absolute_epoch_id
180    }
181
182    pub fn metadata(&self) -> NymTopologyMetadata {
183        self.topology.metadata
184    }
185
186    pub fn new_empty(ignore_egress_epoch_roles: bool) -> NymRouteProvider {
187        let this: Self = NymTopology::default().into();
188        this.with_ignore_egress_epoch_roles(ignore_egress_epoch_roles)
189    }
190
191    pub fn update(&mut self, new_topology: NymTopology) {
192        self.topology = new_topology;
193    }
194
195    pub fn clear_topology(&mut self) {
196        self.topology = Default::default();
197    }
198
199    pub fn with_ignore_egress_epoch_roles(mut self, ignore_egress_epoch_roles: bool) -> Self {
200        self.ignore_egress_epoch_roles(ignore_egress_epoch_roles);
201        self
202    }
203
204    pub fn ignore_egress_epoch_roles(&mut self, ignore_egress_epoch_roles: bool) {
205        self.ignore_egress_epoch_roles = ignore_egress_epoch_roles;
206    }
207
208    pub fn egress_by_identity(
209        &self,
210        node_identity: NodeIdentity,
211    ) -> Result<&RoutingNode, NymTopologyError> {
212        self.topology
213            .egress_by_identity(node_identity, self.ignore_egress_epoch_roles)
214    }
215
216    pub fn node_by_identity(&self, node_identity: NodeIdentity) -> Option<&RoutingNode> {
217        self.topology.find_node_by_identity(node_identity)
218    }
219
220    /// Tries to create a route to the egress point, such that it goes through mixnode on layer 1,
221    /// mixnode on layer2, .... mixnode on layer n and finally the target egress, which can be any known node
222    pub fn random_route_to_egress<R>(
223        &self,
224        rng: &mut R,
225        egress_identity: NodeIdentity,
226    ) -> Result<Vec<SphinxNode>, NymTopologyError>
227    where
228        R: Rng + CryptoRng + ?Sized,
229    {
230        self.topology
231            .random_route_to_egress(rng, egress_identity, self.ignore_egress_epoch_roles)
232    }
233
234    /// Returns a route directly to the egress point, which can be any known node
235    pub fn empty_route_to_egress(
236        &self,
237        egress_identity: NodeIdentity,
238    ) -> Result<Vec<SphinxNode>, NymTopologyError> {
239        let egress = self
240            .topology
241            .egress_node_by_identity(egress_identity, self.ignore_egress_epoch_roles)?;
242        Ok(vec![egress])
243    }
244
245    pub fn random_path_to_egress<R>(
246        &self,
247        rng: &mut R,
248        egress_identity: NodeIdentity,
249    ) -> Result<(Vec<&RoutingNode>, &RoutingNode), NymTopologyError>
250    where
251        R: Rng + CryptoRng + ?Sized,
252    {
253        self.topology
254            .random_path_to_egress(rng, egress_identity, self.ignore_egress_epoch_roles)
255    }
256}
257
258impl NymTopology {
259    #[deprecated]
260    pub fn new_empty(rewarded_set: impl Into<CachedEpochRewardedSet>) -> Self {
261        NymTopology {
262            metadata: NymTopologyMetadata::default(),
263            rewarded_set: rewarded_set.into(),
264            node_details: Default::default(),
265        }
266    }
267
268    pub fn new(
269        metadata: NymTopologyMetadata,
270        rewarded_set: impl Into<CachedEpochRewardedSet>,
271        node_details: Vec<RoutingNode>,
272    ) -> Self {
273        NymTopology {
274            metadata,
275            rewarded_set: rewarded_set.into(),
276            node_details: node_details.into_iter().map(|n| (n.node_id, n)).collect(),
277        }
278    }
279
280    #[cfg(feature = "persistence")]
281    pub fn new_from_file<P: AsRef<std::path::Path>>(path: P) -> std::io::Result<Self> {
282        let file = std::fs::File::open(path)?;
283        serde_json::from_reader(file).map_err(Into::into)
284    }
285
286    pub fn add_skimmed_nodes(&mut self, nodes: &[SkimmedNode]) {
287        self.add_additional_nodes(nodes.iter())
288    }
289
290    pub fn with_skimmed_nodes(mut self, nodes: &[SkimmedNode]) -> Self {
291        self.add_skimmed_nodes(nodes);
292        self
293    }
294
295    pub fn add_routing_nodes<B: Borrow<RoutingNode>>(
296        &mut self,
297        nodes: impl IntoIterator<Item = B>,
298    ) {
299        for node_details in nodes {
300            let node_details = node_details.borrow();
301            let node_id = node_details.node_id;
302            if self
303                .node_details
304                .insert(node_id, node_details.clone())
305                .is_some()
306            {
307                debug!("overwriting node details for node {node_id}")
308            }
309        }
310    }
311
312    pub fn add_additional_nodes<N>(&mut self, nodes: impl Iterator<Item = N>)
313    where
314        N: TryInto<RoutingNode>,
315        <N as TryInto<RoutingNode>>::Error: Display,
316    {
317        for node in nodes {
318            match node.try_into() {
319                Ok(node_details) => {
320                    let node_id = node_details.node_id;
321                    if self.node_details.insert(node_id, node_details).is_some() {
322                        debug!("overwriting node details for node {node_id}")
323                    }
324                }
325                Err(err) => {
326                    debug!("malformed node details: {err}")
327                }
328            }
329        }
330    }
331
332    pub fn with_additional_nodes<N>(mut self, nodes: impl Iterator<Item = N>) -> Self
333    where
334        N: TryInto<RoutingNode>,
335        <N as TryInto<RoutingNode>>::Error: Display,
336    {
337        self.add_additional_nodes(nodes);
338        self
339    }
340
341    pub fn has_node_details(&self, node_id: NodeId) -> bool {
342        self.node_details.contains_key(&node_id)
343    }
344
345    pub fn has_node(&self, identity: ed25519::PublicKey) -> bool {
346        self.node_details
347            .values()
348            .any(|node_details| node_details.identity_key == identity)
349    }
350
351    pub fn insert_node_details(&mut self, node_details: RoutingNode) {
352        self.node_details.insert(node_details.node_id, node_details);
353    }
354
355    pub fn rewarded_set(&self) -> &CachedEpochRewardedSet {
356        &self.rewarded_set
357    }
358
359    pub fn force_set_active(&mut self, node_id: NodeId, role: Role) {
360        match role {
361            Role::EntryGateway => self.rewarded_set.entry_gateways.insert(node_id),
362            Role::Layer1 => self.rewarded_set.layer1.insert(node_id),
363            Role::Layer2 => self.rewarded_set.layer2.insert(node_id),
364            Role::Layer3 => self.rewarded_set.layer3.insert(node_id),
365            Role::ExitGateway => self.rewarded_set.exit_gateways.insert(node_id),
366            Role::Standby => self.rewarded_set.standby.insert(node_id),
367        };
368    }
369
370    fn node_details_exists(&self, ids: &HashSet<NodeId>) -> bool {
371        for id in ids {
372            if self.node_details.contains_key(id) {
373                return true;
374            }
375        }
376        false
377    }
378
379    pub fn is_minimally_routable(&self) -> bool {
380        let has_layer1 = self.node_details_exists(&self.rewarded_set.layer1);
381        let has_layer2 = self.node_details_exists(&self.rewarded_set.layer2);
382        let has_layer3 = self.node_details_exists(&self.rewarded_set.layer3);
383        let has_exit_gateways = !self.rewarded_set.exit_gateways.is_empty();
384        let has_entry_gateways = !self.rewarded_set.entry_gateways.is_empty();
385
386        trace!(
387            has_layer1 = %has_layer1,
388            has_layer2 = %has_layer2,
389            has_layer3 = %has_layer3,
390            has_entry_gateways = %has_entry_gateways,
391            has_exit_gateways = %has_exit_gateways,
392            "network status"
393        );
394
395        has_layer1 && has_layer2 && has_layer3 && (has_exit_gateways || has_entry_gateways)
396    }
397
398    pub fn ensure_minimally_routable(&self) -> Result<(), NymTopologyError> {
399        if !self.is_minimally_routable() {
400            return Err(NymTopologyError::InsufficientMixingNodes);
401        }
402        Ok(())
403    }
404
405    pub fn is_empty(&self) -> bool {
406        self.rewarded_set.is_empty() || self.node_details.is_empty()
407    }
408
409    pub fn ensure_not_empty(&self) -> Result<(), NymTopologyError> {
410        if self.is_empty() {
411            return Err(NymTopologyError::EmptyNetworkTopology);
412        }
413        Ok(())
414    }
415
416    fn find_valid_mix_hop<R>(
417        &self,
418        rng: &mut R,
419        id_choices: Vec<NodeId>,
420    ) -> Result<&RoutingNode, NymTopologyError>
421    where
422        R: Rng + CryptoRng + ?Sized,
423    {
424        let mut id_choices = id_choices;
425        while !id_choices.is_empty() {
426            let index = rng.gen_range(0..id_choices.len());
427
428            // SAFETY: this is not run if the vector is empty
429            let candidate_id = id_choices[index];
430            match self.node_details.get(&candidate_id) {
431                Some(node) => {
432                    return Ok(node);
433                }
434                // this will mess with VRF, but that's a future problem
435                None => {
436                    id_choices.remove(index);
437                    continue;
438                }
439            }
440        }
441
442        Err(NymTopologyError::NoMixnodesAvailable)
443    }
444
445    fn choose_mixing_node<R>(
446        &self,
447        rng: &mut R,
448        assigned_nodes: &HashSet<NodeId>,
449    ) -> Result<&RoutingNode, NymTopologyError>
450    where
451        R: Rng + CryptoRng + ?Sized,
452    {
453        // try first choice without cloning the ids (because I reckon, more often than not, it will actually work)
454        // HashSet's iterator implements `ExactSizeIterator` so choosing **one**  random element
455        // is actually not that expensive
456        let Some(candidate) = assigned_nodes.iter().choose(rng) else {
457            return Err(NymTopologyError::NoMixnodesAvailable);
458        };
459
460        match self.node_details.get(candidate) {
461            Some(node) => Ok(node),
462            None => {
463                let remaining_choices = assigned_nodes
464                    .iter()
465                    .filter(|&n| n != candidate)
466                    .copied()
467                    .collect();
468                self.find_valid_mix_hop(rng, remaining_choices)
469            }
470        }
471    }
472
473    pub fn find_node_by_identity(&self, node_identity: NodeIdentity) -> Option<&RoutingNode> {
474        self.node_details
475            .values()
476            .find(|n| n.identity_key == node_identity)
477    }
478
479    pub fn find_node(&self, node_id: NodeId) -> Option<&RoutingNode> {
480        self.node_details.get(&node_id)
481    }
482
483    pub fn egress_by_identity(
484        &self,
485        node_identity: NodeIdentity,
486        ignore_epoch_roles: bool,
487    ) -> Result<&RoutingNode, NymTopologyError> {
488        let Some(node) = self.find_node_by_identity(node_identity) else {
489            return Err(NymTopologyError::NonExistentNode {
490                node_identity: Box::new(node_identity),
491            });
492        };
493
494        // a 'valid' egress is one that is currently **not** acting as a mixnode
495        if !ignore_epoch_roles
496            && let Some(role) = self.rewarded_set.role(node.node_id)
497            && role.is_mixnode()
498        {
499            return Err(NymTopologyError::InvalidEgressRole {
500                node_identity: Box::new(node_identity),
501            });
502        }
503
504        Ok(node)
505    }
506
507    fn egress_node_by_identity(
508        &self,
509        node_identity: NodeIdentity,
510        ignore_epoch_roles: bool,
511    ) -> Result<SphinxNode, NymTopologyError> {
512        self.egress_by_identity(node_identity, ignore_epoch_roles)
513            .map(Into::into)
514    }
515
516    fn random_mix_path_nodes<R>(&self, rng: &mut R) -> Result<Vec<&RoutingNode>, NymTopologyError>
517    where
518        R: Rng + CryptoRng + ?Sized,
519    {
520        if self.rewarded_set.is_empty() || self.node_details.is_empty() {
521            return Err(NymTopologyError::EmptyNetworkTopology);
522        }
523
524        // we reserve an additional item in the route because we'll have to push an egress
525        let mut mix_route = Vec::with_capacity(4);
526
527        mix_route.push(self.choose_mixing_node(rng, &self.rewarded_set.layer1)?);
528        mix_route.push(self.choose_mixing_node(rng, &self.rewarded_set.layer2)?);
529        mix_route.push(self.choose_mixing_node(rng, &self.rewarded_set.layer3)?);
530
531        Ok(mix_route)
532    }
533
534    pub fn random_mix_route<R>(&self, rng: &mut R) -> Result<Vec<SphinxNode>, NymTopologyError>
535    where
536        R: Rng + CryptoRng + ?Sized,
537    {
538        Ok(self
539            .random_mix_path_nodes(rng)?
540            .into_iter()
541            .map(Into::into)
542            .collect())
543    }
544
545    /// Tries to create a route to the egress point, such that it goes through mixnode on layer 1,
546    /// mixnode on layer2, .... mixnode on layer n and finally the target egress, which can be any known node
547    pub fn random_route_to_egress<R>(
548        &self,
549        rng: &mut R,
550        egress_identity: NodeIdentity,
551        ignore_epoch_roles: bool,
552    ) -> Result<Vec<SphinxNode>, NymTopologyError>
553    where
554        R: Rng + CryptoRng + ?Sized,
555    {
556        let egress = self.egress_node_by_identity(egress_identity, ignore_epoch_roles)?;
557        let mut mix_route = self.random_mix_route(rng)?;
558        mix_route.push(egress);
559        Ok(mix_route)
560    }
561
562    pub fn random_path_to_egress<R>(
563        &self,
564        rng: &mut R,
565        egress_identity: NodeIdentity,
566        ignore_epoch_roles: bool,
567    ) -> Result<(Vec<&RoutingNode>, &RoutingNode), NymTopologyError>
568    where
569        R: Rng + CryptoRng + ?Sized,
570    {
571        let egress = self.egress_by_identity(egress_identity, ignore_epoch_roles)?;
572        let mix_route = self.random_mix_path_nodes(rng)?;
573        Ok((mix_route, egress))
574    }
575
576    pub fn nodes_with_role(&self, role: Role) -> impl Iterator<Item = &'_ RoutingNode> {
577        self.node_details.values().filter(move |node| match role {
578            Role::EntryGateway => self.rewarded_set.entry_gateways.contains(&node.node_id),
579            Role::Layer1 => self.rewarded_set.layer1.contains(&node.node_id),
580            Role::Layer2 => self.rewarded_set.layer2.contains(&node.node_id),
581            Role::Layer3 => self.rewarded_set.layer3.contains(&node.node_id),
582            Role::ExitGateway => self.rewarded_set.exit_gateways.contains(&node.node_id),
583            Role::Standby => self.rewarded_set.standby.contains(&node.node_id),
584        })
585    }
586
587    pub fn set_testable_node(&mut self, role: Role, node: impl Into<RoutingNode>) {
588        fn init_set(node: NodeId) -> HashSet<NodeId> {
589            let mut set = HashSet::new();
590            set.insert(node);
591            set
592        }
593
594        let node = node.into();
595        let node_id = node.node_id;
596        self.node_details.insert(node.node_id, node);
597
598        match role {
599            Role::EntryGateway => self.rewarded_set.entry_gateways = init_set(node_id),
600            Role::Layer1 => self.rewarded_set.layer1 = init_set(node_id),
601            Role::Layer2 => self.rewarded_set.layer2 = init_set(node_id),
602            Role::Layer3 => self.rewarded_set.layer3 = init_set(node_id),
603            Role::ExitGateway => self.rewarded_set.exit_gateways = init_set(node_id),
604            Role::Standby => {
605                warn!(
606                    "attempting to test node in 'standby' mode - are you sure that's what you meant to do?"
607                );
608                self.rewarded_set.standby = init_set(node_id)
609            }
610        }
611    }
612
613    pub fn entry_gateways(&self) -> impl Iterator<Item = &RoutingNode> {
614        self.node_details
615            .values()
616            .filter(|n| self.rewarded_set.entry_gateways.contains(&n.node_id))
617    }
618
619    // ideally this shouldn't exist...
620    pub fn entry_capable_nodes(&self) -> impl Iterator<Item = &RoutingNode> {
621        self.node_details
622            .values()
623            .filter(|n| n.supported_roles.mixnet_entry)
624    }
625
626    pub fn mixnodes(&self) -> impl Iterator<Item = &RoutingNode> {
627        self.node_details
628            .values()
629            .filter(|n| self.rewarded_set.is_active_mixnode(&n.node_id))
630    }
631}