mixnet/core/
topology.rs

1// Copyright 2022 Parity Technologies (UK) Ltd.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a
4// copy of this software and associated documentation files (the "Software"),
5// to deal in the Software without restriction, including without limitation
6// the rights to use, copy, modify, merge, publish, distribute, sublicense,
7// and/or sell copies of the Software, and to permit persons to whom the
8// Software is furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
14// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19// DEALINGS IN THE SOFTWARE.
20
21//! Mixnet topology. A new [`Topology`] is created for every session.
22
23use super::sphinx::{
24	KxPublic, MixnodeIndex, PeerId, RawMixnodeIndex, Target, MAX_HOPS, MAX_MIXNODE_INDEX,
25};
26use arrayvec::ArrayVec;
27use either::Either;
28use rand::{seq::SliceRandom, CryptoRng, Rng};
29use std::{
30	cmp::{max, min},
31	fmt,
32};
33
34/// Per-mixnode data.
35#[derive(Clone)]
36pub struct Mixnode<X> {
37	/// Key-exchange public key for the mixnode.
38	pub kx_public: KxPublic,
39	/// Peer ID for the mixnode.
40	pub peer_id: PeerId,
41	/// Extra data; for use by the crate user.
42	pub extra: X,
43}
44
45enum LocalNode {
46	/// The local node is a mixnode, with the specified index.
47	Mixnode(MixnodeIndex),
48	/// The local node is not a mixnode. It should attempt to connect to the specified gateway
49	/// mixnodes.
50	NonMixnode(Vec<MixnodeIndex>),
51}
52
53/// Topology error.
54#[derive(Debug, thiserror::Error)]
55pub enum TopologyErr {
56	/// An out-of-range mixnode index was encountered.
57	#[error("Bad mixnode index ({0})")]
58	BadMixnodeIndex(MixnodeIndex),
59	/// There aren't enough mixnodes.
60	#[error("Too few mixnodes; this should have been caught earlier")]
61	TooFewMixnodes,
62	/// The local node has not managed to connect to any gateway mixnodes.
63	#[error("The local node has not managed to connect to any gateway mixnodes")]
64	NoConnectedGatewayMixnodes,
65}
66
67pub struct Topology<X> {
68	mixnodes: Vec<Mixnode<X>>,
69	local_kx_public: KxPublic,
70	local_node: LocalNode,
71}
72
73impl<X> Topology<X> {
74	/// `mixnodes` must be no longer than [`MAX_MIXNODE_INDEX + 1`](MAX_MIXNODE_INDEX).
75	pub fn new(
76		rng: &mut impl Rng,
77		mixnodes: Vec<Mixnode<X>>,
78		local_kx_public: &KxPublic,
79		num_gateway_mixnodes: u32,
80	) -> Self {
81		debug_assert!(mixnodes.len() <= (MAX_MIXNODE_INDEX + 1) as usize);
82
83		// Determine if the local node is a mixnode. It is possible for another node to publish our
84		// key-exchange public key as theirs, possibly resulting in a bogus index here. This isn't
85		// particularly harmful so we don't bother doing anything about it:
86		//
87		// - It might result in us thinking we're in the mixnode set when we're really not. Note
88		//   that this situation can only occur if we were trying to register anyway; if we weren't,
89		//   we wouldn't have even generated our key-exchange keys before session registration
90		//   ended.
91		// - We might attempt to connect to ourselves or include ourselves as a hop in packets we
92		//   send. While this is usually avoided, it isn't a big deal.
93		let local_node = mixnodes
94			.iter()
95			.position(|mixnode| &mixnode.kx_public == local_kx_public)
96			.map_or_else(
97				|| {
98					// Local node is not a mixnode. Pick some gateway mixnodes to connect to.
99					LocalNode::NonMixnode(
100						rand::seq::index::sample(
101							rng,
102							mixnodes.len(),
103							min(num_gateway_mixnodes as usize, mixnodes.len()),
104						)
105						.iter()
106						.map(|index| {
107							index
108								.try_into()
109								.expect("Topology::new() contract limits size of mixnode set")
110						})
111						.collect(),
112					)
113				},
114				|index| {
115					// Local node is a mixnode
116					LocalNode::Mixnode(
117						index
118							.try_into()
119							.expect("Topology::new() contract limits size of mixnode set"),
120					)
121				},
122			);
123
124		Self { mixnodes, local_kx_public: *local_kx_public, local_node }
125	}
126
127	pub fn is_mixnode(&self) -> bool {
128		matches!(self.local_node, LocalNode::Mixnode(_))
129	}
130
131	pub fn reserved_peers(&self) -> impl Iterator<Item = &Mixnode<X>> {
132		let indices = match &self.local_node {
133			LocalNode::Mixnode(local_index) => Either::Left({
134				// Connect to all other mixnodes (ie exclude the local node)
135				let num = self.mixnodes.len() as RawMixnodeIndex;
136				(0..local_index.get()).chain((local_index.get() + 1)..num)
137			}),
138			LocalNode::NonMixnode(gateway_indices) =>
139				Either::Right(gateway_indices.iter().map(|index| index.get())),
140		};
141		indices.map(|index| &self.mixnodes[index as usize])
142	}
143
144	pub fn mixnode_index_to_peer_id(&self, index: MixnodeIndex) -> Result<PeerId, TopologyErr> {
145		self.mixnodes
146			.get(index.get() as usize)
147			.map(|mixnode| mixnode.peer_id)
148			.ok_or(TopologyErr::BadMixnodeIndex(index))
149	}
150
151	pub fn target_to_peer_id(&self, target: &Target) -> Result<PeerId, TopologyErr> {
152		match target {
153			Target::MixnodeIndex(index) => self.mixnode_index_to_peer_id(*index),
154			Target::PeerId(peer_id) => Ok(*peer_id),
155		}
156	}
157}
158
159impl<X> fmt::Display for Topology<X> {
160	fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
161		match &self.local_node {
162			LocalNode::Mixnode(local_index) => write!(fmt, "Local node is mixnode {local_index}"),
163			LocalNode::NonMixnode(gateway_indices) => {
164				write!(fmt, "Local node is not a mixnode; gateway mixnodes are ")?;
165				for (i, gateway_index) in gateway_indices.iter().enumerate() {
166					if i == 0 {
167						gateway_index.fmt(fmt)?;
168					} else {
169						write!(fmt, ", {gateway_index}")?;
170					}
171				}
172				Ok(())
173			},
174		}
175	}
176}
177
178/// A trait for querying the peer ID and connectivity of the local node.
179pub trait NetworkStatus {
180	/// Returns the peer ID of the local node.
181	fn local_peer_id(&self) -> PeerId;
182	/// Returns `true` iff the local node is currently connected to the specified peer.
183	fn is_connected(&self, peer_id: &PeerId) -> bool;
184}
185
186const MAX_CONNECTED_GATEWAY_INDICES: usize = 5;
187
188pub enum RouteKind {
189	/// Route begins at the local node and ends at the specified mixnode.
190	ToMixnode(MixnodeIndex),
191	/// Route begins at the specified mixnode and ends at the local node.
192	FromMixnode(MixnodeIndex),
193	/// Route begins and ends at the local node.
194	Loop,
195}
196
197struct UsedIndices(ArrayVec<MixnodeIndex, { MAX_HOPS + 1 }>);
198
199impl UsedIndices {
200	fn new() -> Self {
201		Self(ArrayVec::new())
202	}
203
204	fn insert(&mut self, index: MixnodeIndex) {
205		match self.0.iter().position(|used_index| *used_index >= index) {
206			Some(i) =>
207				if self.0[i] != index {
208					self.0.insert(i, index);
209				},
210			None => self.0.push(index),
211		}
212	}
213
214	fn iter(&self) -> impl ExactSizeIterator<Item = MixnodeIndex> + '_ {
215		self.0.iter().copied()
216	}
217
218	fn as_option(&self) -> Option<MixnodeIndex> {
219		debug_assert!(self.0.len() <= 1);
220		self.0.first().copied()
221	}
222}
223
224pub struct RouteGenerator<'topology, X> {
225	topology: &'topology Topology<X>,
226	local_peer_id: PeerId,
227	/// Always empty if the local node is a mixnode. Otherwise, the subset of the gateway mixnodes
228	/// from the topology that are currently connected.
229	connected_gateway_indices: ArrayVec<MixnodeIndex, MAX_CONNECTED_GATEWAY_INDICES>,
230}
231
232impl<'topology, X> RouteGenerator<'topology, X> {
233	pub fn new(topology: &'topology Topology<X>, ns: &dyn NetworkStatus) -> Self {
234		let connected_gateway_indices = match &topology.local_node {
235			LocalNode::Mixnode(_) => ArrayVec::new(),
236			// If we're not a mixnode, we should have attempted to connect to a number of "gateway"
237			// mixnodes. As we compete with other nodes for slots we might not have managed to
238			// connect to all of them. Check which ones we managed to connect to.
239			LocalNode::NonMixnode(gateway_indices) => gateway_indices
240				.iter()
241				.copied()
242				.filter(|gateway_index| {
243					let mixnode = &topology.mixnodes[gateway_index.get() as usize];
244					ns.is_connected(&mixnode.peer_id)
245				})
246				.take(MAX_CONNECTED_GATEWAY_INDICES)
247				.collect(),
248		};
249
250		Self { topology, local_peer_id: ns.local_peer_id(), connected_gateway_indices }
251	}
252
253	pub fn topology(&self) -> &'topology Topology<X> {
254		self.topology
255	}
256
257	/// Choose a random mixnode and return its index. Exclude mixnodes with indices in
258	/// `exclude_indices` from consideration. `exclude_indices` must be sorted and must not contain
259	/// duplicate or invalid indices.
260	fn choose_mixnode_index(
261		&self,
262		rng: &mut (impl Rng + CryptoRng),
263		exclude_indices: impl ExactSizeIterator<Item = MixnodeIndex>,
264	) -> Result<MixnodeIndex, TopologyErr> {
265		let num_allowed =
266			self.topology
267				.mixnodes
268				.len()
269				.checked_sub(exclude_indices.len())
270				.expect("No duplicate or invalid indices in exclude_indices") as RawMixnodeIndex;
271		if num_allowed == 0 {
272			return Err(TopologyErr::TooFewMixnodes)
273		}
274
275		let mut chosen = rng.gen_range(0..num_allowed);
276		for exclude_index in exclude_indices {
277			if chosen >= exclude_index.get() {
278				chosen += 1;
279			}
280		}
281		// At most exclude_indices.len() added in loop, and chosen was less than
282		// self.topology.mixnodes.len() - exclude_indices.len() before the loop
283		debug_assert!((chosen as usize) < self.topology.mixnodes.len());
284
285		Ok(chosen.try_into().expect("Topology::new() contract limits size of mixnode set"))
286	}
287
288	/// Choose a random mixnode to send a message to and return its index.
289	pub fn choose_destination_index(
290		&self,
291		rng: &mut (impl Rng + CryptoRng),
292	) -> Result<MixnodeIndex, TopologyErr> {
293		let exclude_index = match self.topology.local_node {
294			// If we're a mixnode, don't send to ourselves
295			LocalNode::Mixnode(local_index) => Some(local_index),
296			// If we're not a mixnode, and we are only connected to one gateway mixnode, don't send
297			// to it; it must be the first hop, and we don't want to visit any node more than once
298			LocalNode::NonMixnode(_) => match self.connected_gateway_indices.as_slice() {
299				[gateway_index] => Some(*gateway_index),
300				_ => None,
301			},
302		};
303		self.choose_mixnode_index(rng, exclude_index.iter().copied())
304	}
305
306	fn choose_connected_gateway_index(
307		&self,
308		rng: &mut (impl Rng + CryptoRng),
309		try_exclude_index: Option<MixnodeIndex>,
310	) -> Result<MixnodeIndex, TopologyErr> {
311		try_exclude_index
312			.and_then(|try_exclude_index| {
313				if !self.connected_gateway_indices.iter().any(|index| *index == try_exclude_index) {
314					// Mixnode to exclude is not a connected gateway
315					return None
316				}
317				let (&first, rest) = self.connected_gateway_indices.split_first()?;
318				let Some(&chosen) = rest.choose(rng) else {
319					// Only one connected gateway; must use regardless of try_exclude_index
320					return Some(first)
321				};
322				// try_exclude_index is either first or in rest. If we chose it from rest, replace
323				// it with first.
324				Some(if chosen == try_exclude_index { first } else { chosen })
325			})
326			.or_else(|| self.connected_gateway_indices.choose(rng).copied())
327			.ok_or(TopologyErr::NoConnectedGatewayMixnodes)
328	}
329
330	/// Generate a route through the mixnet. Returns the mixnode index of the first hop. The route
331	/// may contain more hops than `num_hops` if this is necessary.
332	pub fn gen_route(
333		&self,
334		targets: &mut ArrayVec<Target, { MAX_HOPS - 1 }>,
335		their_kx_publics: &mut ArrayVec<KxPublic, MAX_HOPS>,
336		rng: &mut (impl Rng + CryptoRng),
337		kind: RouteKind,
338		num_hops: usize,
339	) -> Result<MixnodeIndex, TopologyErr> {
340		// Mixnode indices we've used already. We avoid using any mixnode more than once.
341		let mut used_indices = UsedIndices::new();
342
343		let (from_local, to_local) = match kind {
344			RouteKind::ToMixnode(index) => {
345				used_indices.insert(index);
346				(true, false)
347			},
348			RouteKind::FromMixnode(index) => {
349				used_indices.insert(index);
350				(false, true)
351			},
352			RouteKind::Loop => (true, true),
353		};
354
355		// If we're a mixnode, make sure we don't include ourselves in the route
356		debug_assert!(from_local || to_local);
357		if let LocalNode::Mixnode(index) = self.topology.local_node {
358			used_indices.insert(index);
359		}
360
361		// If we're not a mixnode, and the packet is to be sent by us, the first hop needs to be to
362		// a connected gateway mixnode
363		let special_first_index = match self.topology.local_node {
364			LocalNode::NonMixnode(_) if from_local => {
365				let index = self.choose_connected_gateway_index(rng, used_indices.as_option())?;
366				used_indices.insert(index);
367				Some(index)
368			},
369			_ => None,
370		};
371
372		// If we're not a mixnode, and the packet is to be received by us, the last hop needs to be
373		// from a connected gateway mixnode
374		let special_penultimate_index = match self.topology.local_node {
375			LocalNode::NonMixnode(_) if to_local => {
376				let index = self.choose_connected_gateway_index(rng, used_indices.as_option())?;
377				used_indices.insert(index);
378				Some(index)
379			},
380			_ => None,
381		};
382
383		let min_hops = [
384			// Special first hop
385			special_first_index.is_some(),
386			// Intermediate hop required if special first and penultimate hops to same mixnode
387			// (this can only happen with RouteKind::Loop)
388			special_first_index.is_some() && (special_first_index == special_penultimate_index),
389			// Special penultimate hop
390			special_penultimate_index.is_some(),
391			// Last hop
392			true,
393		]
394		.iter()
395		.map(|need_hop| *need_hop as usize)
396		.sum();
397		let num_hops = max(num_hops, min_hops);
398
399		let mut first_index = None;
400		for i in 0..num_hops {
401			// Figure out the hop target. This is either a mixnode index (Some) or the local node
402			// (None).
403			let mut index = match (i, num_hops - i, special_first_index, special_penultimate_index)
404			{
405				// Special first hop
406				(0, _, Some(index), _) => Some(index),
407				// Special penultimate hop
408				(_, 2, _, Some(index)) => Some(index),
409				// Last hop
410				(_, 1, _, _) => match kind {
411					RouteKind::ToMixnode(index) => Some(index),
412					RouteKind::FromMixnode(_) => None,
413					RouteKind::Loop => None,
414				},
415				// Intermediate hop
416				_ => {
417					let index = self.choose_mixnode_index(rng, used_indices.iter())?;
418					used_indices.insert(index);
419					Some(index)
420				},
421			};
422
423			// Push the key-exchange public key for the target
424			their_kx_publics.push(match index {
425				Some(index) => self.topology.mixnodes[index.get() as usize].kx_public,
426				None => self.topology.local_kx_public,
427			});
428
429			// Push the target
430			if index.is_none() {
431				// Target is the local node. If the local node is a mixnode, use its index.
432				if let LocalNode::Mixnode(local_index) = self.topology.local_node {
433					index = Some(local_index);
434				}
435			}
436			if i == 0 {
437				// First hop should always be to a mixnode
438				debug_assert!(index.is_some());
439				first_index = index;
440			} else {
441				targets.push(match index {
442					Some(index) => Target::MixnodeIndex(index),
443					None => Target::PeerId(self.local_peer_id),
444				});
445			}
446		}
447
448		Ok(first_index.expect("At least one hop"))
449	}
450}