use super::sphinx::{
KxPublic, MixnodeIndex, PeerId, RawMixnodeIndex, Target, MAX_HOPS, MAX_MIXNODE_INDEX,
};
use arrayvec::ArrayVec;
use either::Either;
use rand::{seq::SliceRandom, CryptoRng, Rng};
use std::{
cmp::{max, min},
fmt,
};
#[derive(Clone)]
pub struct Mixnode<X> {
pub kx_public: KxPublic,
pub peer_id: PeerId,
pub extra: X,
}
enum LocalNode {
Mixnode(MixnodeIndex),
NonMixnode(Vec<MixnodeIndex>),
}
#[derive(Debug, thiserror::Error)]
pub enum TopologyErr {
#[error("Bad mixnode index ({0})")]
BadMixnodeIndex(MixnodeIndex),
#[error("Too few mixnodes; this should have been caught earlier")]
TooFewMixnodes,
#[error("The local node has not managed to connect to any gateway mixnodes")]
NoConnectedGatewayMixnodes,
}
pub struct Topology<X> {
mixnodes: Vec<Mixnode<X>>,
local_kx_public: KxPublic,
local_node: LocalNode,
}
impl<X> Topology<X> {
pub fn new(
rng: &mut impl Rng,
mixnodes: Vec<Mixnode<X>>,
local_kx_public: &KxPublic,
num_gateway_mixnodes: u32,
) -> Self {
debug_assert!(mixnodes.len() <= (MAX_MIXNODE_INDEX + 1) as usize);
let local_node = mixnodes
.iter()
.position(|mixnode| &mixnode.kx_public == local_kx_public)
.map_or_else(
|| {
LocalNode::NonMixnode(
rand::seq::index::sample(
rng,
mixnodes.len(),
min(num_gateway_mixnodes as usize, mixnodes.len()),
)
.iter()
.map(|index| {
index
.try_into()
.expect("Topology::new() contract limits size of mixnode set")
})
.collect(),
)
},
|index| {
LocalNode::Mixnode(
index
.try_into()
.expect("Topology::new() contract limits size of mixnode set"),
)
},
);
Self { mixnodes, local_kx_public: *local_kx_public, local_node }
}
pub fn is_mixnode(&self) -> bool {
matches!(self.local_node, LocalNode::Mixnode(_))
}
pub fn reserved_peers(&self) -> impl Iterator<Item = &Mixnode<X>> {
let indices = match &self.local_node {
LocalNode::Mixnode(local_index) => Either::Left({
let num = self.mixnodes.len() as RawMixnodeIndex;
(0..local_index.get()).chain((local_index.get() + 1)..num)
}),
LocalNode::NonMixnode(gateway_indices) =>
Either::Right(gateway_indices.iter().map(|index| index.get())),
};
indices.map(|index| &self.mixnodes[index as usize])
}
pub fn mixnode_index_to_peer_id(&self, index: MixnodeIndex) -> Result<PeerId, TopologyErr> {
self.mixnodes
.get(index.get() as usize)
.map(|mixnode| mixnode.peer_id)
.ok_or(TopologyErr::BadMixnodeIndex(index))
}
pub fn target_to_peer_id(&self, target: &Target) -> Result<PeerId, TopologyErr> {
match target {
Target::MixnodeIndex(index) => self.mixnode_index_to_peer_id(*index),
Target::PeerId(peer_id) => Ok(*peer_id),
}
}
}
impl<X> fmt::Display for Topology<X> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match &self.local_node {
LocalNode::Mixnode(local_index) => write!(fmt, "Local node is mixnode {local_index}"),
LocalNode::NonMixnode(gateway_indices) => {
write!(fmt, "Local node is not a mixnode; gateway mixnodes are ")?;
for (i, gateway_index) in gateway_indices.iter().enumerate() {
if i == 0 {
gateway_index.fmt(fmt)?;
} else {
write!(fmt, ", {gateway_index}")?;
}
}
Ok(())
},
}
}
}
pub trait NetworkStatus {
fn local_peer_id(&self) -> PeerId;
fn is_connected(&self, peer_id: &PeerId) -> bool;
}
const MAX_CONNECTED_GATEWAY_INDICES: usize = 5;
pub enum RouteKind {
ToMixnode(MixnodeIndex),
FromMixnode(MixnodeIndex),
Loop,
}
struct UsedIndices(ArrayVec<MixnodeIndex, { MAX_HOPS + 1 }>);
impl UsedIndices {
fn new() -> Self {
Self(ArrayVec::new())
}
fn insert(&mut self, index: MixnodeIndex) {
match self.0.iter().position(|used_index| *used_index >= index) {
Some(i) =>
if self.0[i] != index {
self.0.insert(i, index);
},
None => self.0.push(index),
}
}
fn iter(&self) -> impl ExactSizeIterator<Item = MixnodeIndex> + '_ {
self.0.iter().copied()
}
fn as_option(&self) -> Option<MixnodeIndex> {
debug_assert!(self.0.len() <= 1);
self.0.first().copied()
}
}
pub struct RouteGenerator<'topology, X> {
topology: &'topology Topology<X>,
local_peer_id: PeerId,
connected_gateway_indices: ArrayVec<MixnodeIndex, MAX_CONNECTED_GATEWAY_INDICES>,
}
impl<'topology, X> RouteGenerator<'topology, X> {
pub fn new(topology: &'topology Topology<X>, ns: &dyn NetworkStatus) -> Self {
let connected_gateway_indices = match &topology.local_node {
LocalNode::Mixnode(_) => ArrayVec::new(),
LocalNode::NonMixnode(gateway_indices) => gateway_indices
.iter()
.copied()
.filter(|gateway_index| {
let mixnode = &topology.mixnodes[gateway_index.get() as usize];
ns.is_connected(&mixnode.peer_id)
})
.take(MAX_CONNECTED_GATEWAY_INDICES)
.collect(),
};
Self { topology, local_peer_id: ns.local_peer_id(), connected_gateway_indices }
}
pub fn topology(&self) -> &'topology Topology<X> {
self.topology
}
fn choose_mixnode_index(
&self,
rng: &mut (impl Rng + CryptoRng),
exclude_indices: impl ExactSizeIterator<Item = MixnodeIndex>,
) -> Result<MixnodeIndex, TopologyErr> {
let num_allowed =
self.topology
.mixnodes
.len()
.checked_sub(exclude_indices.len())
.expect("No duplicate or invalid indices in exclude_indices") as RawMixnodeIndex;
if num_allowed == 0 {
return Err(TopologyErr::TooFewMixnodes)
}
let mut chosen = rng.gen_range(0..num_allowed);
for exclude_index in exclude_indices {
if chosen >= exclude_index.get() {
chosen += 1;
}
}
debug_assert!((chosen as usize) < self.topology.mixnodes.len());
Ok(chosen.try_into().expect("Topology::new() contract limits size of mixnode set"))
}
pub fn choose_destination_index(
&self,
rng: &mut (impl Rng + CryptoRng),
) -> Result<MixnodeIndex, TopologyErr> {
let exclude_index = match self.topology.local_node {
LocalNode::Mixnode(local_index) => Some(local_index),
LocalNode::NonMixnode(_) => match self.connected_gateway_indices.as_slice() {
[gateway_index] => Some(*gateway_index),
_ => None,
},
};
self.choose_mixnode_index(rng, exclude_index.iter().copied())
}
fn choose_connected_gateway_index(
&self,
rng: &mut (impl Rng + CryptoRng),
try_exclude_index: Option<MixnodeIndex>,
) -> Result<MixnodeIndex, TopologyErr> {
try_exclude_index
.and_then(|try_exclude_index| {
if !self.connected_gateway_indices.iter().any(|index| *index == try_exclude_index) {
return None
}
let (&first, rest) = self.connected_gateway_indices.split_first()?;
let Some(&chosen) = rest.choose(rng) else {
return Some(first)
};
Some(if chosen == try_exclude_index { first } else { chosen })
})
.or_else(|| self.connected_gateway_indices.choose(rng).copied())
.ok_or(TopologyErr::NoConnectedGatewayMixnodes)
}
pub fn gen_route(
&self,
targets: &mut ArrayVec<Target, { MAX_HOPS - 1 }>,
their_kx_publics: &mut ArrayVec<KxPublic, MAX_HOPS>,
rng: &mut (impl Rng + CryptoRng),
kind: RouteKind,
num_hops: usize,
) -> Result<MixnodeIndex, TopologyErr> {
let mut used_indices = UsedIndices::new();
let (from_local, to_local) = match kind {
RouteKind::ToMixnode(index) => {
used_indices.insert(index);
(true, false)
},
RouteKind::FromMixnode(index) => {
used_indices.insert(index);
(false, true)
},
RouteKind::Loop => (true, true),
};
debug_assert!(from_local || to_local);
if let LocalNode::Mixnode(index) = self.topology.local_node {
used_indices.insert(index);
}
let special_first_index = match self.topology.local_node {
LocalNode::NonMixnode(_) if from_local => {
let index = self.choose_connected_gateway_index(rng, used_indices.as_option())?;
used_indices.insert(index);
Some(index)
},
_ => None,
};
let special_penultimate_index = match self.topology.local_node {
LocalNode::NonMixnode(_) if to_local => {
let index = self.choose_connected_gateway_index(rng, used_indices.as_option())?;
used_indices.insert(index);
Some(index)
},
_ => None,
};
let min_hops = [
special_first_index.is_some(),
special_first_index.is_some() && (special_first_index == special_penultimate_index),
special_penultimate_index.is_some(),
true,
]
.iter()
.map(|need_hop| *need_hop as usize)
.sum();
let num_hops = max(num_hops, min_hops);
let mut first_index = None;
for i in 0..num_hops {
let mut index = match (i, num_hops - i, special_first_index, special_penultimate_index)
{
(0, _, Some(index), _) => Some(index),
(_, 2, _, Some(index)) => Some(index),
(_, 1, _, _) => match kind {
RouteKind::ToMixnode(index) => Some(index),
RouteKind::FromMixnode(_) => None,
RouteKind::Loop => None,
},
_ => {
let index = self.choose_mixnode_index(rng, used_indices.iter())?;
used_indices.insert(index);
Some(index)
},
};
their_kx_publics.push(match index {
Some(index) => self.topology.mixnodes[index.get() as usize].kx_public,
None => self.topology.local_kx_public,
});
if index.is_none() {
if let LocalNode::Mixnode(local_index) = self.topology.local_node {
index = Some(local_index);
}
}
if i == 0 {
debug_assert!(index.is_some());
first_index = index;
} else {
targets.push(match index {
Some(index) => Target::MixnodeIndex(index),
None => Target::PeerId(self.local_peer_id),
});
}
}
Ok(first_index.expect("At least one hop"))
}
}