Skip to main content

soil_cli/params/
node_key_params.rs

1// This file is part of Soil.
2
3// Copyright (C) Soil contributors.
4// Copyright (C) Parity Technologies (UK) Ltd.
5// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
6
7use clap::Args;
8use soil_network::config::{ed25519, NodeKeyConfig};
9use soil_service::Role;
10use std::{path::PathBuf, str::FromStr};
11use subsoil::core::H256;
12
13use crate::{arg_enums::NodeKeyType, error, Error};
14
15/// The file name of the node's Ed25519 secret key inside the chain-specific
16/// network config directory, if neither `--node-key` nor `--node-key-file`
17/// is specified in combination with `--node-key-type=ed25519`.
18pub(crate) const NODE_KEY_ED25519_FILE: &str = "secret_ed25519";
19
20/// Parameters used to create the `NodeKeyConfig`, which determines the keypair
21/// used for libp2p networking.
22#[derive(Debug, Clone, Args)]
23pub struct NodeKeyParams {
24	/// Secret key to use for p2p networking.
25	///
26	/// The value is a string that is parsed according to the choice of
27	/// `--node-key-type` as follows:
28	///
29	///  - `ed25519`: the value is parsed as a hex-encoded Ed25519 32 byte secret key (64 hex
30	///    chars)
31	///
32	/// The value of this option takes precedence over `--node-key-file`.
33	///
34	/// WARNING: Secrets provided as command-line arguments are easily exposed.
35	/// Use of this option should be limited to development and testing. To use
36	/// an externally managed secret key, use `--node-key-file` instead.
37	#[arg(long, value_name = "KEY")]
38	pub node_key: Option<String>,
39
40	/// Crypto primitive to use for p2p networking.
41	///
42	/// The secret key of the node is obtained as follows:
43	///
44	/// - If the `--node-key` option is given, the value is parsed as a secret key according to the
45	///   type. See the documentation for `--node-key`.
46	///
47	/// - If the `--node-key-file` option is given, the secret key is read from the specified file.
48	///   See the documentation for `--node-key-file`.
49	///
50	/// - Otherwise, the secret key is read from a file with a predetermined, type-specific name
51	///   from the chain-specific network config directory inside the base directory specified by
52	///   `--base-dir`. If this file does not exist, it is created with a newly generated secret
53	///   key of the chosen type.
54	///
55	/// The node's secret key determines the corresponding public key and hence the
56	/// node's peer ID in the context of libp2p.
57	#[arg(long, value_name = "TYPE", value_enum, ignore_case = true, default_value_t = NodeKeyType::Ed25519)]
58	pub node_key_type: NodeKeyType,
59
60	/// File from which to read the node's secret key to use for p2p networking.
61	///
62	/// The contents of the file are parsed according to the choice of `--node-key-type`
63	/// as follows:
64	///
65	/// - `ed25519`: the file must contain an unencoded 32 byte or hex encoded Ed25519 secret key.
66	///
67	/// If the file does not exist, it is created with a newly generated secret key of
68	/// the chosen type.
69	#[arg(long, value_name = "FILE")]
70	pub node_key_file: Option<PathBuf>,
71
72	/// Forces key generation if node-key-file file does not exist.
73	///
74	/// This is an unsafe feature for production networks, because as an active authority
75	/// other authorities may depend on your node having a stable identity and they might
76	/// not being able to reach you if your identity changes after entering the active set.
77	///
78	/// For minimal node downtime if no custom `node-key-file` argument is provided
79	/// the network-key is usually persisted accross nodes restarts,
80	/// in the `network` folder from directory provided in `--base-path`
81	///
82	/// Warning!! If you ever run the node with this argument, make sure
83	/// you remove it for the subsequent restarts.
84	#[arg(long)]
85	pub unsafe_force_node_key_generation: bool,
86}
87
88impl NodeKeyParams {
89	/// Create a `NodeKeyConfig` from the given `NodeKeyParams` in the context
90	/// of an optional network config storage directory.
91	pub fn node_key(
92		&self,
93		net_config_dir: &PathBuf,
94		role: Role,
95		is_dev: bool,
96	) -> error::Result<NodeKeyConfig> {
97		Ok(match self.node_key_type {
98			NodeKeyType::Ed25519 => {
99				let secret = if let Some(node_key) = self.node_key.as_ref() {
100					parse_ed25519_secret(node_key)?
101				} else {
102					let key_path = self
103						.node_key_file
104						.clone()
105						.unwrap_or_else(|| net_config_dir.join(NODE_KEY_ED25519_FILE));
106					if !self.unsafe_force_node_key_generation
107						&& role.is_authority()
108						&& !is_dev && !key_path.exists()
109					{
110						return Err(Error::NetworkKeyNotFound(key_path));
111					}
112					soil_network::config::Secret::File(key_path)
113				};
114
115				NodeKeyConfig::Ed25519(secret)
116			},
117		})
118	}
119}
120
121/// Create an error caused by an invalid node key argument.
122fn invalid_node_key(e: impl std::fmt::Display) -> error::Error {
123	error::Error::Input(format!("Invalid node key: {}", e))
124}
125
126/// Parse a Ed25519 secret key from a hex string into a `soil_network::Secret`.
127fn parse_ed25519_secret(hex: &str) -> error::Result<soil_network::config::Ed25519Secret> {
128	H256::from_str(hex).map_err(invalid_node_key).and_then(|bytes| {
129		ed25519::SecretKey::try_from_bytes(bytes)
130			.map(soil_network::config::Secret::Input)
131			.map_err(invalid_node_key)
132	})
133}
134
135#[cfg(test)]
136mod tests {
137	use super::*;
138	use clap::ValueEnum;
139	use soil_network::config::ed25519;
140	use std::fs::{self, File};
141	use tempfile::TempDir;
142
143	#[test]
144	fn test_node_key_config_input() {
145		fn secret_input(net_config_dir: &PathBuf) -> error::Result<()> {
146			NodeKeyType::value_variants().iter().try_for_each(|t| {
147				let node_key_type = *t;
148				let sk = match node_key_type {
149					NodeKeyType::Ed25519 => ed25519::SecretKey::generate().as_ref().to_vec(),
150				};
151				let params = NodeKeyParams {
152					node_key_type,
153					node_key: Some(format!("{:x}", H256::from_slice(sk.as_ref()))),
154					node_key_file: None,
155					unsafe_force_node_key_generation: false,
156				};
157				params.node_key(net_config_dir, Role::Authority, false).and_then(|c| match c {
158					NodeKeyConfig::Ed25519(soil_network::config::Secret::Input(ref ski))
159						if node_key_type == NodeKeyType::Ed25519 && &sk[..] == ski.as_ref() =>
160					{
161						Ok(())
162					},
163					_ => Err(error::Error::Input("Unexpected node key config".into())),
164				})
165			})
166		}
167
168		assert!(secret_input(&PathBuf::from_str("x").unwrap()).is_ok());
169	}
170
171	#[test]
172	fn test_node_key_config_file() {
173		fn check_key(file: PathBuf, key: &ed25519::SecretKey) {
174			let params = NodeKeyParams {
175				node_key_type: NodeKeyType::Ed25519,
176				node_key: None,
177				node_key_file: Some(file),
178				unsafe_force_node_key_generation: false,
179			};
180
181			let node_key = params
182				.node_key(&PathBuf::from("not-used"), Role::Authority, false)
183				.expect("Creates node key config")
184				.into_keypair()
185				.expect("Creates node key pair");
186
187			if node_key.secret().as_ref() != key.as_ref() {
188				panic!("Invalid key")
189			}
190		}
191
192		let tmp = tempfile::Builder::new().prefix("alice").tempdir().expect("Creates tempfile");
193		let file = tmp.path().join("mysecret").to_path_buf();
194		let key = ed25519::SecretKey::generate();
195
196		fs::write(&file, array_bytes::bytes2hex("", key.as_ref())).expect("Writes secret key");
197		check_key(file.clone(), &key);
198
199		fs::write(&file, &key).expect("Writes secret key");
200		check_key(file.clone(), &key);
201	}
202
203	#[test]
204	fn test_node_key_config_default() {
205		fn with_def_params<F>(f: F, unsafe_force_node_key_generation: bool) -> error::Result<()>
206		where
207			F: Fn(NodeKeyParams) -> error::Result<()>,
208		{
209			NodeKeyType::value_variants().iter().try_for_each(|t| {
210				let node_key_type = *t;
211				f(NodeKeyParams {
212					node_key_type,
213					node_key: None,
214					node_key_file: None,
215					unsafe_force_node_key_generation,
216				})
217			})
218		}
219
220		fn some_config_dir(
221			net_config_dir: &PathBuf,
222			unsafe_force_node_key_generation: bool,
223			role: Role,
224			is_dev: bool,
225		) -> error::Result<()> {
226			with_def_params(
227				|params| {
228					let dir = PathBuf::from(net_config_dir.clone());
229					let typ = params.node_key_type;
230					params.node_key(net_config_dir, role, is_dev).and_then(move |c| match c {
231						NodeKeyConfig::Ed25519(soil_network::config::Secret::File(ref f))
232							if typ == NodeKeyType::Ed25519
233								&& f == &dir.join(NODE_KEY_ED25519_FILE) =>
234						{
235							Ok(())
236						},
237						_ => Err(error::Error::Input("Unexpected node key config".into())),
238					})
239				},
240				unsafe_force_node_key_generation,
241			)
242		}
243
244		assert!(some_config_dir(&PathBuf::from_str("x").unwrap(), false, Role::Full, false).is_ok());
245		assert!(
246			some_config_dir(&PathBuf::from_str("x").unwrap(), false, Role::Authority, true).is_ok()
247		);
248		assert!(
249			some_config_dir(&PathBuf::from_str("x").unwrap(), true, Role::Authority, false).is_ok()
250		);
251		assert!(matches!(
252			some_config_dir(&PathBuf::from_str("x").unwrap(), false, Role::Authority, false),
253			Err(Error::NetworkKeyNotFound(_))
254		));
255
256		let tempdir = TempDir::new().unwrap();
257		let _file = File::create(tempdir.path().join(NODE_KEY_ED25519_FILE)).unwrap();
258		assert!(some_config_dir(&tempdir.path().into(), false, Role::Authority, false).is_ok());
259	}
260}