Files
nym/common/topology/src/lib.rs
T
2025-01-28 09:29:51 +00:00

541 lines
18 KiB
Rust

// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use ::serde::{Deserialize, Serialize};
use nym_api_requests::nym_nodes::SkimmedNode;
use nym_sphinx_addressing::nodes::NodeIdentity;
use nym_sphinx_types::Node as SphinxNode;
use rand::prelude::IteratorRandom;
use rand::{CryptoRng, Rng};
use std::borrow::Borrow;
use std::collections::{HashMap, HashSet};
use std::fmt::Display;
use std::net::IpAddr;
use tracing::{debug, trace, warn};
pub use crate::node::{EntryDetails, RoutingNode, SupportedRoles};
pub use error::NymTopologyError;
pub use nym_mixnet_contract_common::nym_node::Role;
pub use nym_mixnet_contract_common::{EpochRewardedSet, NodeId};
pub use rewarded_set::CachedEpochRewardedSet;
pub mod error;
pub mod node;
pub mod rewarded_set;
#[cfg(feature = "provider-trait")]
pub mod provider_trait;
#[cfg(feature = "wasm-serde-types")]
pub mod wasm_helpers;
#[cfg(feature = "provider-trait")]
pub use provider_trait::{HardcodedTopologyProvider, TopologyProvider};
#[deprecated]
#[derive(Debug, Clone)]
pub enum NetworkAddress {
IpAddr(IpAddr),
Hostname(String),
}
#[allow(deprecated)]
mod deprecated_network_address_impls {
use crate::NetworkAddress;
use std::convert::Infallible;
use std::fmt::{Display, Formatter};
use std::net::{SocketAddr, ToSocketAddrs};
use std::str::FromStr;
use std::{fmt, io};
impl NetworkAddress {
pub fn as_hostname(self) -> Option<String> {
match self {
NetworkAddress::IpAddr(_) => None,
NetworkAddress::Hostname(s) => Some(s),
}
}
}
impl NetworkAddress {
pub fn to_socket_addrs(&self, port: u16) -> io::Result<Vec<SocketAddr>> {
match self {
NetworkAddress::IpAddr(addr) => Ok(vec![SocketAddr::new(*addr, port)]),
NetworkAddress::Hostname(hostname) => {
Ok((hostname.as_str(), port).to_socket_addrs()?.collect())
}
}
}
}
impl FromStr for NetworkAddress {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(ip_addr) = s.parse() {
Ok(NetworkAddress::IpAddr(ip_addr))
} else {
Ok(NetworkAddress::Hostname(s.to_string()))
}
}
}
impl Display for NetworkAddress {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
NetworkAddress::IpAddr(ip_addr) => ip_addr.fmt(f),
NetworkAddress::Hostname(hostname) => hostname.fmt(f),
}
}
}
}
pub type MixLayer = u8;
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct NymTopology {
// for the purposes of future VRF, everyone will need the same view of the network, regardless of performance filtering
// so we use the same 'master' rewarded set information for that
//
// how do we solve the problem of "we have to go through a node that we want to filter out?"
// ¯\_(ツ)_/¯ that's a future problem
rewarded_set: CachedEpochRewardedSet,
node_details: HashMap<NodeId, RoutingNode>,
}
#[derive(Clone, Debug, Default)]
pub struct NymRouteProvider {
pub topology: NymTopology,
/// Allow constructing routes with final hop at nodes that are not entry/exit gateways in the current epoch
pub ignore_egress_epoch_roles: bool,
}
impl From<NymTopology> for NymRouteProvider {
fn from(topology: NymTopology) -> Self {
NymRouteProvider {
topology,
ignore_egress_epoch_roles: false,
}
}
}
impl NymRouteProvider {
pub fn new(topology: NymTopology, ignore_egress_epoch_roles: bool) -> Self {
NymRouteProvider {
topology,
ignore_egress_epoch_roles,
}
}
pub fn new_empty(ignore_egress_epoch_roles: bool) -> NymRouteProvider {
let this: Self = NymTopology::default().into();
this.with_ignore_egress_epoch_roles(ignore_egress_epoch_roles)
}
pub fn update(&mut self, new_topology: NymTopology) {
self.topology = new_topology;
}
pub fn clear_topology(&mut self) {
self.topology = Default::default();
}
pub fn with_ignore_egress_epoch_roles(mut self, ignore_egress_epoch_roles: bool) -> Self {
self.ignore_egress_epoch_roles(ignore_egress_epoch_roles);
self
}
pub fn ignore_egress_epoch_roles(&mut self, ignore_egress_epoch_roles: bool) {
self.ignore_egress_epoch_roles = ignore_egress_epoch_roles;
}
pub fn egress_by_identity(
&self,
node_identity: NodeIdentity,
) -> Result<&RoutingNode, NymTopologyError> {
self.topology
.egress_by_identity(node_identity, self.ignore_egress_epoch_roles)
}
pub fn node_by_identity(&self, node_identity: NodeIdentity) -> Option<&RoutingNode> {
self.topology.find_node_by_identity(node_identity)
}
/// Tries to create a route to the egress point, such that it goes through mixnode on layer 1,
/// mixnode on layer2, .... mixnode on layer n and finally the target egress, which can be any known node
pub fn random_route_to_egress<R>(
&self,
rng: &mut R,
egress_identity: NodeIdentity,
) -> Result<Vec<SphinxNode>, NymTopologyError>
where
R: Rng + CryptoRng + ?Sized,
{
self.topology
.random_route_to_egress(rng, egress_identity, self.ignore_egress_epoch_roles)
}
pub fn random_path_to_egress<R>(
&self,
rng: &mut R,
egress_identity: NodeIdentity,
) -> Result<(Vec<&RoutingNode>, &RoutingNode), NymTopologyError>
where
R: Rng + CryptoRng + ?Sized,
{
self.topology
.random_path_to_egress(rng, egress_identity, self.ignore_egress_epoch_roles)
}
}
impl NymTopology {
pub fn new_empty(rewarded_set: impl Into<CachedEpochRewardedSet>) -> Self {
NymTopology {
rewarded_set: rewarded_set.into(),
node_details: Default::default(),
}
}
pub fn new(
rewarded_set: impl Into<CachedEpochRewardedSet>,
node_details: Vec<RoutingNode>,
) -> Self {
NymTopology {
rewarded_set: rewarded_set.into(),
node_details: node_details.into_iter().map(|n| (n.node_id, n)).collect(),
}
}
#[cfg(feature = "persistence")]
pub fn new_from_file<P: AsRef<std::path::Path>>(path: P) -> std::io::Result<Self> {
let file = std::fs::File::open(path)?;
serde_json::from_reader(file).map_err(Into::into)
}
pub fn add_skimmed_nodes(&mut self, nodes: &[SkimmedNode]) {
self.add_additional_nodes(nodes.iter())
}
pub fn add_routing_nodes<B: Borrow<RoutingNode>>(
&mut self,
nodes: impl IntoIterator<Item = B>,
) {
for node_details in nodes {
let node_details = node_details.borrow();
let node_id = node_details.node_id;
if self
.node_details
.insert(node_id, node_details.clone())
.is_some()
{
debug!("overwriting node details for node {node_id}")
}
}
}
pub fn add_additional_nodes<N>(&mut self, nodes: impl Iterator<Item = N>)
where
N: TryInto<RoutingNode>,
<N as TryInto<RoutingNode>>::Error: Display,
{
for node in nodes {
match node.try_into() {
Ok(node_details) => {
let node_id = node_details.node_id;
if self.node_details.insert(node_id, node_details).is_some() {
debug!("overwriting node details for node {node_id}")
}
}
Err(err) => {
debug!("malformed node details: {err}")
}
}
}
}
pub fn has_node_details(&self, node_id: NodeId) -> bool {
self.node_details.contains_key(&node_id)
}
pub fn insert_node_details(&mut self, node_details: RoutingNode) {
self.node_details.insert(node_details.node_id, node_details);
}
pub fn rewarded_set(&self) -> &CachedEpochRewardedSet {
&self.rewarded_set
}
pub fn force_set_active(&mut self, node_id: NodeId, role: Role) {
match role {
Role::EntryGateway => self.rewarded_set.entry_gateways.insert(node_id),
Role::Layer1 => self.rewarded_set.layer1.insert(node_id),
Role::Layer2 => self.rewarded_set.layer2.insert(node_id),
Role::Layer3 => self.rewarded_set.layer3.insert(node_id),
Role::ExitGateway => self.rewarded_set.exit_gateways.insert(node_id),
Role::Standby => self.rewarded_set.standby.insert(node_id),
};
}
fn node_details_exists(&self, ids: &HashSet<NodeId>) -> bool {
for id in ids {
if self.node_details.contains_key(id) {
return true;
}
}
false
}
pub fn is_minimally_routable(&self) -> bool {
let has_layer1 = self.node_details_exists(&self.rewarded_set.layer1);
let has_layer2 = self.node_details_exists(&self.rewarded_set.layer2);
let has_layer3 = self.node_details_exists(&self.rewarded_set.layer3);
let has_exit_gateways = !self.rewarded_set.exit_gateways.is_empty();
let has_entry_gateways = !self.rewarded_set.entry_gateways.is_empty();
trace!(
has_layer1 = %has_layer1,
has_layer2 = %has_layer2,
has_layer3 = %has_layer3,
has_entry_gateways = %has_entry_gateways,
has_exit_gateways = %has_exit_gateways,
"network status"
);
has_layer1 && has_layer2 && has_layer3 && (has_exit_gateways || has_entry_gateways)
}
pub fn ensure_minimally_routable(&self) -> Result<(), NymTopologyError> {
if !self.is_minimally_routable() {
return Err(NymTopologyError::InsufficientMixingNodes);
}
Ok(())
}
pub fn is_empty(&self) -> bool {
self.rewarded_set.is_empty() || self.node_details.is_empty()
}
pub fn ensure_not_empty(&self) -> Result<(), NymTopologyError> {
if self.is_empty() {
return Err(NymTopologyError::EmptyNetworkTopology);
}
Ok(())
}
fn find_valid_mix_hop<R>(
&self,
rng: &mut R,
id_choices: Vec<NodeId>,
) -> Result<&RoutingNode, NymTopologyError>
where
R: Rng + CryptoRng + ?Sized,
{
let mut id_choices = id_choices;
while !id_choices.is_empty() {
let index = rng.gen_range(0..id_choices.len());
// SAFETY: this is not run if the vector is empty
let candidate_id = id_choices[index];
match self.node_details.get(&candidate_id) {
Some(node) => {
return Ok(node);
}
// this will mess with VRF, but that's a future problem
None => {
id_choices.remove(index);
continue;
}
}
}
Err(NymTopologyError::NoMixnodesAvailable)
}
fn choose_mixing_node<R>(
&self,
rng: &mut R,
assigned_nodes: &HashSet<NodeId>,
) -> Result<&RoutingNode, NymTopologyError>
where
R: Rng + CryptoRng + ?Sized,
{
// try first choice without cloning the ids (because I reckon, more often than not, it will actually work)
// HashSet's iterator implements `ExactSizeIterator` so choosing **one** random element
// is actually not that expensive
let Some(candidate) = assigned_nodes.iter().choose(rng) else {
return Err(NymTopologyError::NoMixnodesAvailable);
};
match self.node_details.get(candidate) {
Some(node) => Ok(node),
None => {
let remaining_choices = assigned_nodes
.iter()
.filter(|&n| n != candidate)
.copied()
.collect();
self.find_valid_mix_hop(rng, remaining_choices)
}
}
}
pub fn find_node_by_identity(&self, node_identity: NodeIdentity) -> Option<&RoutingNode> {
self.node_details
.values()
.find(|n| n.identity_key == node_identity)
}
pub fn find_node(&self, node_id: NodeId) -> Option<&RoutingNode> {
self.node_details.get(&node_id)
}
pub fn egress_by_identity(
&self,
node_identity: NodeIdentity,
ignore_epoch_roles: bool,
) -> Result<&RoutingNode, NymTopologyError> {
let Some(node) = self.find_node_by_identity(node_identity) else {
return Err(NymTopologyError::NonExistentNode {
node_identity: Box::new(node_identity),
});
};
// a 'valid' egress is one that is currently **not** acting as a mixnode
if !ignore_epoch_roles {
if let Some(role) = self.rewarded_set.role(node.node_id) {
if role.is_mixnode() {
return Err(NymTopologyError::InvalidEgressRole {
node_identity: Box::new(node_identity),
});
}
}
}
Ok(node)
}
fn egress_node_by_identity(
&self,
node_identity: NodeIdentity,
ignore_epoch_roles: bool,
) -> Result<SphinxNode, NymTopologyError> {
self.egress_by_identity(node_identity, ignore_epoch_roles)
.map(Into::into)
}
fn random_mix_path_nodes<R>(&self, rng: &mut R) -> Result<Vec<&RoutingNode>, NymTopologyError>
where
R: Rng + CryptoRng + ?Sized,
{
if self.rewarded_set.is_empty() || self.node_details.is_empty() {
return Err(NymTopologyError::EmptyNetworkTopology);
}
// we reserve an additional item in the route because we'll have to push an egress
let mut mix_route = Vec::with_capacity(4);
mix_route.push(self.choose_mixing_node(rng, &self.rewarded_set.layer1)?);
mix_route.push(self.choose_mixing_node(rng, &self.rewarded_set.layer2)?);
mix_route.push(self.choose_mixing_node(rng, &self.rewarded_set.layer3)?);
Ok(mix_route)
}
pub fn random_mix_route<R>(&self, rng: &mut R) -> Result<Vec<SphinxNode>, NymTopologyError>
where
R: Rng + CryptoRng + ?Sized,
{
Ok(self
.random_mix_path_nodes(rng)?
.into_iter()
.map(Into::into)
.collect())
}
/// Tries to create a route to the egress point, such that it goes through mixnode on layer 1,
/// mixnode on layer2, .... mixnode on layer n and finally the target egress, which can be any known node
pub fn random_route_to_egress<R>(
&self,
rng: &mut R,
egress_identity: NodeIdentity,
ignore_epoch_roles: bool,
) -> Result<Vec<SphinxNode>, NymTopologyError>
where
R: Rng + CryptoRng + ?Sized,
{
let egress = self.egress_node_by_identity(egress_identity, ignore_epoch_roles)?;
let mut mix_route = self.random_mix_route(rng)?;
mix_route.push(egress);
Ok(mix_route)
}
pub fn random_path_to_egress<R>(
&self,
rng: &mut R,
egress_identity: NodeIdentity,
ignore_epoch_roles: bool,
) -> Result<(Vec<&RoutingNode>, &RoutingNode), NymTopologyError>
where
R: Rng + CryptoRng + ?Sized,
{
let egress = self.egress_by_identity(egress_identity, ignore_epoch_roles)?;
let mix_route = self.random_mix_path_nodes(rng)?;
Ok((mix_route, egress))
}
pub fn nodes_with_role(&self, role: Role) -> impl Iterator<Item = &'_ RoutingNode> {
self.node_details.values().filter(move |node| match role {
Role::EntryGateway => self.rewarded_set.entry_gateways.contains(&node.node_id),
Role::Layer1 => self.rewarded_set.layer1.contains(&node.node_id),
Role::Layer2 => self.rewarded_set.layer2.contains(&node.node_id),
Role::Layer3 => self.rewarded_set.layer3.contains(&node.node_id),
Role::ExitGateway => self.rewarded_set.exit_gateways.contains(&node.node_id),
Role::Standby => self.rewarded_set.standby.contains(&node.node_id),
})
}
pub fn set_testable_node(&mut self, role: Role, node: impl Into<RoutingNode>) {
fn init_set(node: NodeId) -> HashSet<NodeId> {
let mut set = HashSet::new();
set.insert(node);
set
}
let node = node.into();
let node_id = node.node_id;
self.node_details.insert(node.node_id, node);
match role {
Role::EntryGateway => self.rewarded_set.entry_gateways = init_set(node_id),
Role::Layer1 => self.rewarded_set.layer1 = init_set(node_id),
Role::Layer2 => self.rewarded_set.layer2 = init_set(node_id),
Role::Layer3 => self.rewarded_set.layer3 = init_set(node_id),
Role::ExitGateway => self.rewarded_set.exit_gateways = init_set(node_id),
Role::Standby => {
warn!("attempting to test node in 'standby' mode - are you sure that's what you meant to do?");
self.rewarded_set.standby = init_set(node_id)
}
}
}
pub fn entry_gateways(&self) -> impl Iterator<Item = &RoutingNode> {
self.node_details
.values()
.filter(|n| self.rewarded_set.entry_gateways.contains(&n.node_id))
}
// ideally this shouldn't exist...
pub fn entry_capable_nodes(&self) -> impl Iterator<Item = &RoutingNode> {
self.node_details
.values()
.filter(|n| n.supported_roles.mixnet_entry)
}
pub fn mixnodes(&self) -> impl Iterator<Item = &RoutingNode> {
self.node_details
.values()
.filter(|n| self.rewarded_set.is_active_mixnode(&n.node_id))
}
}