swarm_nl/
setup.rs

1// Copyright 2024 Algorealm, Inc.
2// Apache 2.0 License
3
4//! Data structures and functions to setup a node and configure it for networking.
5
6#![doc = include_str!("../doc/setup/NodeSetup.md")]
7use crate::core::gossipsub_cfg::Blacklist;
8pub use crate::prelude::*;
9pub use libp2p_identity::{rsa::Keypair as RsaKeypair, KeyType, Keypair, PeerId};
10
11/// Import the contents of the exported modules into this module.
12use super::*;
13
14/// Configuration data required for node bootstrap.
15#[derive(Debug)]
16pub struct BootstrapConfig {
17	/// The port to listen on if using the TCP/IP protocol.
18	tcp_port: Port,
19	/// The port to listen on if using the UDP or QUIC protocol.
20	udp_port: Port,
21	/// The Cryptographic Keypair for node identification and message auth.
22	keypair: Keypair,
23	/// Bootstrap peers.
24	boot_nodes: Nodes,
25	/// Blacklisted peers
26	blacklist: Blacklist,
27}
28
29impl BootstrapConfig {
30	/// Read from a bootstrap config file on disk.
31	///
32	/// # Panics
33	///
34	/// This function will panic if the file is not found at the specified path.
35	pub fn from_file(file_path: &str) -> Self {
36		util::read_ini_file(file_path).unwrap()
37	}
38
39	/// Return a new `BootstrapConfig` struct populated by default (empty) values.
40	///
41	/// Must be called first if the config is to be explicitly built without reading `.ini` file
42	/// from disk.
43	pub fn new() -> Self {
44		BootstrapConfig {
45			// Default TCP/IP port if not specified.
46			tcp_port: MIN_PORT,
47			// Default UDP port if not specified.
48			udp_port: MAX_PORT,
49			// Default node keypair type i.e Ed25519.
50			keypair: Keypair::generate_ed25519(),
51			boot_nodes: Default::default(),
52			// List of blacklisted peers
53			blacklist: Default::default(),
54		}
55	}
56
57	/// Configure available bootnodes.
58	pub fn with_bootnodes(mut self, boot_nodes: Nodes) -> Self {
59		// Additive operation
60		self.boot_nodes.extend(boot_nodes.into_iter());
61		self
62	}
63
64	/// Configure a list of peers to add to blacklist.
65	pub fn with_blacklist(mut self, list: Vec<PeerId>) -> Self {
66		// additive operation
67		self.blacklist.list.extend(list.into_iter());
68		self
69	}
70
71	/// Configure the TCP/IP port.
72	///
73	/// Note: Port must range between [`MIN_PORT`] and [`MAX_PORT`].
74	pub fn with_tcp(self, tcp_port: Port) -> Self {
75		if tcp_port > MIN_PORT && tcp_port < MAX_PORT {
76			BootstrapConfig { tcp_port, ..self }
77		} else {
78			self
79		}
80	}
81
82	/// Configure the UDP port.
83	///
84	/// Note: Port must range between [`MIN_PORT`] and [`MAX_PORT`]
85	pub fn with_udp(self, udp_port: Port) -> Self {
86		if udp_port > MIN_PORT && udp_port < MAX_PORT {
87			BootstrapConfig { udp_port, ..self }
88		} else {
89			self
90		}
91	}
92
93	/// Generate a Cryptographic Keypair for node identity creation and message signing.
94	///
95	/// An RSA keypair cannot be generated on-the-fly. It has to be generated from a `.pk8` file.
96	/// Hence the `rsa_pk8_filepath` parameter must always be set to `None` except in the case of
97	/// RSA. Please note that calling this function overrides whatever might have been read from the
98	/// `.ini` file
99	///
100	/// # Panics
101	///
102	/// This function will panic if:
103	///
104	/// 1. The RSA key type is specified and the `rsa_pk8_filepath` is set to `None`.
105	/// 2. If the file contains invalid data and an RSA keypair cannot be generated from it.
106	pub fn generate_keypair(self, key_type: KeyType, rsa_pk8_filepath: Option<&str>) -> Self {
107		if rsa_pk8_filepath.is_none() && key_type == KeyType::RSA {
108			panic!("RSA keypair specified without a .pk8 file");
109		}
110
111		let keypair = match key_type {
112			// Generate a Ed25519 Keypair
113			KeyType::Ed25519 => Keypair::generate_ed25519(),
114			KeyType::RSA => {
115				let mut bytes = std::fs::read(rsa_pk8_filepath.unwrap()).unwrap_or_default();
116				// Return RSA keypair generated from a .pk8 binary file
117				Keypair::rsa_from_pkcs8(&mut bytes).unwrap()
118			},
119			KeyType::Secp256k1 => Keypair::generate_secp256k1(),
120			KeyType::Ecdsa => Keypair::generate_ecdsa(),
121		};
122
123		BootstrapConfig { keypair, ..self }
124	}
125
126	/// Generate a Cryptographic Keypair from a protobuf format.
127	///
128	/// This will override any already set keypair.
129	///
130	/// # Panics
131	///
132	/// This function will panic if the `u8` buffer is not parsable into the specified key type.
133	/// This could be because one of two reasons:
134	///
135	/// 1. If the key type is valid, but the keypair data is not valid for that key type.
136	/// 2. If the key type is invalid.
137	pub fn generate_keypair_from_protobuf(self, key_type_str: &str, bytes: &mut [u8]) -> Self {
138		// Parse the key type
139		if let Some(key_type) = <KeyType as CustomFrom>::from(key_type_str) {
140			let raw_keypair = Keypair::from_protobuf_encoding(bytes)
141				.expect("Invalid keypair: protobuf bytes not parsable into keypair");
142
143			let keypair = match key_type {
144				// Generate a Ed25519 Keypair
145				KeyType::Ed25519 => Keypair::try_into_ed25519(raw_keypair).unwrap().into(),
146				// Generate a RSA Keypair
147				KeyType::RSA => Keypair::rsa_from_pkcs8(bytes).unwrap(),
148				// Generate a Secp256k1 Keypair
149				KeyType::Secp256k1 => Keypair::try_into_secp256k1(raw_keypair).unwrap().into(),
150				// Generate a Ecdsa Keypair
151				KeyType::Ecdsa => Keypair::try_into_ecdsa(raw_keypair).unwrap().into(),
152			};
153
154			BootstrapConfig { keypair, ..self }
155		} else {
156			// Generate a default Ed25519 keypair
157			BootstrapConfig {
158				keypair: Keypair::generate_ed25519(),
159				..self
160			}
161		}
162	}
163
164	/// Return a node's cryptographic keypair.
165	pub fn keypair(&self) -> Keypair {
166		self.keypair.clone()
167	}
168
169	/// Return the configured ports in a tuple i.e (TCP Port, UDP port).
170	pub fn ports(&self) -> (Port, Port) {
171		(self.tcp_port, self.udp_port)
172	}
173
174	/// Return the configured bootnodes for the network.
175	pub fn bootnodes(&self) -> Nodes {
176		self.boot_nodes.clone()
177	}
178
179	/// Return the 	`PeerId`'s of nodes that are to be blacklisted.
180	pub fn blacklist(&self) -> Blacklist {
181		self.blacklist.clone()
182	}
183}
184
185/// [`Default`] implementation for [`BootstrapConfig`].
186impl Default for BootstrapConfig {
187	fn default() -> Self {
188		Self::new()
189	}
190}
191
192#[cfg(test)]
193mod tests {
194	use super::*;
195	use std::collections::HashMap;
196	use std::fs;
197	use std::panic;
198	use std::process::Command;
199
200	// helper to generate RSA keypair files
201	// commands taken from https://docs.rs/libp2p-identity/0.2.8/libp2p_identity/struct.Keypair.html#example-generating-rsa-keys-with-openssl
202	fn generate_rsa_keypair_files() {
203		// Generate RSA private key
204		let genrsa_output = Command::new("openssl")
205			.args(&["genrsa", "-out", "private.pem", "2048"])
206			.output()
207			.expect("Failed to execute openssl genrsa command");
208
209		// Convert private key to PKCS8 format
210		let pkcs8_output = Command::new("openssl")
211			.args(&[
212				"pkcs8",
213				"-in",
214				"private.pem",
215				"-inform",
216				"PEM",
217				"-topk8",
218				"-out",
219				"private.pk8",
220				"-outform",
221				"DER",
222				"-nocrypt",
223			])
224			.output()
225			.expect("Failed to execute openssl pkcs8 command");
226
227		// Check command outputs for success or failure
228		if genrsa_output.status.success() {
229			println!("RSA private key generated successfully");
230		} else {
231			eprintln!(
232				"Failed to generate RSA private key:\n{}",
233				String::from_utf8_lossy(&genrsa_output.stderr)
234			);
235		}
236
237		if pkcs8_output.status.success() {
238			println!("RSA private key converted to PKCS8 format successfully");
239		} else {
240			eprintln!(
241				"Failed to convert RSA private key to PKCS8 format:\n{}",
242				String::from_utf8_lossy(&pkcs8_output.stderr)
243			);
244		}
245	}
246
247	#[test]
248	fn file_read_should_panic() {
249		let result = panic::catch_unwind(|| {
250			BootstrapConfig::from_file("non_existent_file.ini");
251		});
252		assert!(result.is_err());
253	}
254
255	#[test]
256	fn default_config_works() {
257		let bootstrap_config = BootstrapConfig::default();
258
259		// Default port values
260		assert_eq!(bootstrap_config.tcp_port, MIN_PORT);
261		assert_eq!(bootstrap_config.udp_port, MAX_PORT);
262
263		// .. and we know that the default is Ed25519
264		let keypair = bootstrap_config.keypair;
265		assert_eq!(keypair.key_type(), KeyType::Ed25519);
266
267		// Bootnodes aren't configured by default so we expect an empty HashMap
268		assert_eq!(bootstrap_config.boot_nodes, HashMap::new());
269	}
270
271	#[test]
272	fn new_config_with_bootnodes_works() {
273		// Setup test data
274		let mut bootnodes: Nodes = HashMap::new();
275		let key_1 = "12D3KooWBmwXN3rsVfnLsZKbXeBrSLfczHxZHwVjPrbKwpLfYm3t".to_string();
276		let val_1 = "/ip4/192.168.1.205/tcp/1509".to_string();
277		let key_2 = "12A0ZooWBmwXN3rsVfnLsZKbXeBrSLfczHxZHwVjPrbKwpLfYm3t".to_string();
278		let val_2 = "/ip4/192.168.1.205/tcp/1588".to_string();
279		bootnodes.insert(key_1.clone(), val_1.clone());
280		bootnodes.insert(key_2.clone(), val_2.clone());
281
282		// We've inserted two bootnodes
283		let bootstrap_config = BootstrapConfig::new().with_bootnodes(bootnodes);
284		assert_eq!(bootstrap_config.bootnodes().len(), 2);
285
286		// we can also check that the bootnodes method returns the correct values
287		let bootnodes = bootstrap_config.bootnodes();
288		assert_eq!(bootnodes.get_key_value(&key_1), Some((&key_1, &val_1)));
289		assert_eq!(bootnodes.get_key_value(&key_2), Some((&key_2, &val_2)));
290	}
291
292	#[test]
293	fn new_config_with_tcp_port_works() {
294		// First assert that the default is MIN_PORT
295		let bootstrap_config = BootstrapConfig::default();
296		assert_eq!(bootstrap_config.ports().0, MIN_PORT);
297
298		// Now set a custom port
299		let bootstrap_config_with_tcp = bootstrap_config.with_tcp(49666);
300		assert_eq!(bootstrap_config_with_tcp.ports().0, 49666);
301
302		// Now set an invalid port and check it falls back to the default tcp port value
303		// Note: MAX_PORT+1 would overflow the u16 type
304		let bootstrap_config_invalid_tcp_port = BootstrapConfig::new().with_tcp(MIN_PORT - 42);
305
306		// TCP will always be reset to MIN_PORT if out of bounds
307		assert_eq!(bootstrap_config_invalid_tcp_port.ports().0, MIN_PORT);
308	}
309
310	#[test]
311	fn new_config_with_udp_port_works() {
312		// Default should be MAX_PORT
313		let bootstrap_config = BootstrapConfig::default();
314		assert_eq!(bootstrap_config.ports().1, MAX_PORT);
315
316		// Now set a custom port
317		let bootstrap_config_with_udp = bootstrap_config.with_udp(55555);
318		assert_eq!(bootstrap_config_with_udp.ports().1, 55555);
319
320		// Now set an invalid port and check it falls back to the default udp port value
321		let bootstrap_config_invalid_udp_port = BootstrapConfig::new().with_udp(MIN_PORT - 42);
322		assert_eq!(bootstrap_config_invalid_udp_port.ports().1, MAX_PORT);
323	}
324
325	#[test]
326	fn key_type_is_invalid() {
327		// Invalid keytype
328		let invalid_keytype = "SomeMagicCryptoType";
329
330		// Valid keypair
331		let mut ed25519_serialized_keypair =
332			Keypair::generate_ed25519().to_protobuf_encoding().unwrap();
333
334		// Should not panic but default to ed25519
335		let result = panic::catch_unwind(move || {
336			let bootstrap_config = BootstrapConfig::default()
337				.generate_keypair_from_protobuf(invalid_keytype, &mut ed25519_serialized_keypair);
338
339			assert_eq!(bootstrap_config.keypair().key_type(), KeyType::Ed25519);
340		});
341
342		assert!(result.is_ok());
343	}
344
345	#[test]
346	#[should_panic(expected = "Invalid keypair: protobuf bytes not parsable into keypair")]
347	fn key_pair_is_invalid() {
348		let valid_key_types = ["Ed25519", "RSA", "Secp256k1", "Ecdsa"];
349		let mut invalid_keypair: [u8; 2] = [0; 2];
350
351		// Keypair is invalid for each valid key type
352		let _ = BootstrapConfig::default()
353			.generate_keypair_from_protobuf(valid_key_types[0], &mut invalid_keypair);
354		let _ = BootstrapConfig::default()
355			.generate_keypair_from_protobuf(valid_key_types[1], &mut invalid_keypair);
356		let _ = BootstrapConfig::default()
357			.generate_keypair_from_protobuf(valid_key_types[2], &mut invalid_keypair);
358		let _ = BootstrapConfig::default()
359			.generate_keypair_from_protobuf(valid_key_types[3], &mut invalid_keypair);
360	}
361
362	#[test]
363	#[should_panic(expected = "RSA keypair specified without a .pk8 file")]
364	fn rsa_specified_without_filepath_panics() {
365		let bootstrap_config = BootstrapConfig::default();
366		let _ = bootstrap_config.generate_keypair(KeyType::RSA, None);
367	}
368
369	#[test]
370	#[should_panic]
371	fn rsa_specified_with_nonexistant_file() {
372		let bootstrap_config = BootstrapConfig::default();
373		let _ = bootstrap_config.generate_keypair(KeyType::RSA, Some("invalid_rsa_file.pk8"));
374	}
375
376	#[test]
377	fn rsa_with_invalid_contents_should_panic() {
378		// Create an RSA file with invalid contents
379		let file_path = "invalid_rsa_keypair_temp_file.pk8";
380		let invalid_keypair: [u8; 64] = [0; 64];
381		std::fs::write(file_path, invalid_keypair).unwrap();
382
383		let result = panic::catch_unwind(|| {
384			// Should panic when parsing invalid RSA file
385			let _ = BootstrapConfig::default().generate_keypair(KeyType::RSA, Some(file_path));
386		});
387
388		// This will return an error
389		assert!(result.is_err());
390
391		// Clean-up invalid_rsa_keypair_temp_file.pk8
392		fs::remove_file(file_path).unwrap_or_default();
393	}
394
395	#[test]
396	fn rsa_from_valid_file_works() {
397		// Create a valid private.pk8 file
398		generate_rsa_keypair_files();
399
400		let bootstrap_config =
401			BootstrapConfig::new().generate_keypair(KeyType::RSA, Some("private.pk8"));
402
403		assert_eq!(bootstrap_config.keypair().key_type(), KeyType::RSA);
404
405		// Clean-up RSA files
406		fs::remove_file("private.pk8").unwrap_or_default();
407		fs::remove_file("private.pem").unwrap_or_default();
408	}
409
410	#[test]
411	fn generate_keypair_from_protobuf_ed25519_works() {
412		// Generate a valid keypair for ed25519
413		let key_type_str = "Ed25519";
414		let mut ed25519_serialized_keypair =
415			Keypair::generate_ed25519().to_protobuf_encoding().unwrap();
416
417		// Add to bootstrap config from protobuf
418		let bootstrap_config = BootstrapConfig::new()
419			.generate_keypair_from_protobuf(key_type_str, &mut ed25519_serialized_keypair);
420
421		assert_eq!(bootstrap_config.keypair().key_type(), KeyType::Ed25519);
422	}
423
424	#[test]
425	fn generate_keypair_from_protobuf_ecdsa_works() {
426		// Generate a valid keypair for ecdsa
427		let key_type_str = "Ecdsa";
428		let mut ecdsa_serialized_keypair =
429			Keypair::generate_ecdsa().to_protobuf_encoding().unwrap();
430
431		// Add to bootstrap config from protobuf
432		let bootstrap_config = BootstrapConfig::new()
433			.generate_keypair_from_protobuf(key_type_str, &mut ecdsa_serialized_keypair);
434
435		assert_eq!(bootstrap_config.keypair().key_type(), KeyType::Ecdsa);
436	}
437
438	#[test]
439	fn generate_keypair_from_protobuf_secp256k1_works() {
440		// Generate a valid keypair for Secp256k1
441		let key_type_str = "Secp256k1";
442		let mut secp256k1_serialized_keypair = Keypair::generate_secp256k1()
443			.to_protobuf_encoding()
444			.unwrap();
445
446		// Add to bootstrap config from protobuf
447		let bootstrap_config = BootstrapConfig::new()
448			.generate_keypair_from_protobuf(key_type_str, &mut secp256k1_serialized_keypair);
449
450		assert_eq!(bootstrap_config.keypair().key_type(), KeyType::Secp256k1);
451	}
452}