1use ::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 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 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 #[serde(default)]
137 metadata: NymTopologyMetadata,
138
139 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 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 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 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 let candidate_id = id_choices[index];
430 match self.node_details.get(&candidate_id) {
431 Some(node) => {
432 return Ok(node);
433 }
434 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 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 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 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 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 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}