Skip to main content

soil_cli/commands/
generate_node_key.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
7//! Implementation of the `generate-node-key` subcommand
8
9use crate::{build_network_key_dir_or_default, Error, NODE_KEY_ED25519_FILE};
10use clap::{Args, Parser};
11use libp2p_identity::{ed25519, Keypair};
12use soil_service::BasePath;
13use std::{
14	fs,
15	io::{self, Write},
16	path::PathBuf,
17};
18
19/// Common arguments accross all generate key commands, subkey and node.
20#[derive(Debug, Args, Clone)]
21pub struct GenerateKeyCmdCommon {
22	/// Name of file to save secret key to.
23	/// If not given, the secret key is printed to stdout.
24	#[arg(long)]
25	file: Option<PathBuf>,
26
27	/// The output is in raw binary format.
28	/// If not given, the output is written as an hex encoded string.
29	#[arg(long)]
30	bin: bool,
31}
32
33/// The `generate-node-key` command
34#[derive(Debug, Clone, Parser)]
35#[command(
36	name = "generate-node-key",
37	about = "Generate a random node key, write it to a file or stdout \
38		 	and write the corresponding peer-id to stderr"
39)]
40pub struct GenerateNodeKeyCmd {
41	#[clap(flatten)]
42	pub common: GenerateKeyCmdCommon,
43	/// Specify the chain specification.
44	///
45	/// It can be any of the predefined chains like dev, local, staging, polkadot, kusama.
46	#[arg(long, value_name = "CHAIN_SPEC")]
47	pub chain: Option<String>,
48	/// A directory where the key should be saved. If a key already
49	/// exists in the directory, it won't be overwritten.
50	#[arg(long, conflicts_with_all = ["file", "default_base_path"])]
51	base_path: Option<PathBuf>,
52
53	/// Save the key in the default directory. If a key already
54	/// exists in the directory, it won't be overwritten.
55	#[arg(long, conflicts_with_all = ["base_path", "file"])]
56	default_base_path: bool,
57}
58
59impl GenerateKeyCmdCommon {
60	/// Run the command
61	pub fn run(&self) -> Result<(), Error> {
62		generate_key(&self.file, self.bin, None, &None, false, None)
63	}
64}
65
66impl GenerateNodeKeyCmd {
67	/// Run the command
68	pub fn run(&self, chain_spec_id: &str, executable_name: &String) -> Result<(), Error> {
69		generate_key(
70			&self.common.file,
71			self.common.bin,
72			Some(chain_spec_id),
73			&self.base_path,
74			self.default_base_path,
75			Some(executable_name),
76		)
77	}
78}
79
80// Utility function for generating a key based on the provided CLI arguments
81//
82// `file`  - Name of file to save secret key to
83// `bin`
84fn generate_key(
85	file: &Option<PathBuf>,
86	bin: bool,
87	chain_spec_id: Option<&str>,
88	base_path: &Option<PathBuf>,
89	default_base_path: bool,
90	executable_name: Option<&String>,
91) -> Result<(), Error> {
92	let keypair = ed25519::Keypair::generate();
93
94	let secret = keypair.secret();
95
96	let file_data = if bin {
97		secret.as_ref().to_owned()
98	} else {
99		array_bytes::bytes2hex("", secret).into_bytes()
100	};
101
102	match (file, base_path, default_base_path) {
103		(Some(file), None, false) => fs::write(file, file_data)?,
104		(None, Some(_), false) | (None, None, true) => {
105			let network_path = build_network_key_dir_or_default(
106				base_path.clone().map(BasePath::new),
107				chain_spec_id.unwrap_or_default(),
108				executable_name.ok_or(Error::Input("Executable name not provided".into()))?,
109			);
110
111			fs::create_dir_all(network_path.as_path())?;
112
113			let key_path = network_path.join(NODE_KEY_ED25519_FILE);
114			if key_path.exists() {
115				eprintln!("Skip generation, a key already exists in {:?}", key_path);
116				return Err(Error::KeyAlreadyExistsInPath(key_path));
117			} else {
118				eprintln!("Generating key in {:?}", key_path);
119				fs::write(key_path, file_data)?
120			}
121		},
122		(None, None, false) => io::stdout().lock().write_all(&file_data)?,
123		(_, _, _) => {
124			// This should not happen, arguments are marked as mutually exclusive.
125			return Err(Error::Input("Mutually exclusive arguments provided".into()));
126		},
127	}
128
129	eprintln!("{}", Keypair::from(keypair).public().to_peer_id());
130
131	Ok(())
132}
133
134#[cfg(test)]
135pub mod tests {
136	use crate::DEFAULT_NETWORK_CONFIG_PATH;
137
138	use super::*;
139	use std::io::Read;
140	use tempfile::Builder;
141
142	#[test]
143	fn generate_node_key() {
144		let mut file = Builder::new().prefix("keyfile").tempfile().unwrap();
145		let file_path = file.path().display().to_string();
146		let generate = GenerateNodeKeyCmd::parse_from(&["generate-node-key", "--file", &file_path]);
147		assert!(generate.run("test", &String::from("test")).is_ok());
148		let mut buf = String::new();
149		assert!(file.read_to_string(&mut buf).is_ok());
150		assert!(array_bytes::hex2bytes(&buf).is_ok());
151	}
152
153	#[test]
154	fn generate_node_key_base_path() {
155		let base_dir = Builder::new().prefix("keyfile").tempdir().unwrap();
156		let key_path = base_dir
157			.path()
158			.join("chains/test_id/")
159			.join(DEFAULT_NETWORK_CONFIG_PATH)
160			.join(NODE_KEY_ED25519_FILE);
161		let base_path = base_dir.path().display().to_string();
162		let generate =
163			GenerateNodeKeyCmd::parse_from(&["generate-node-key", "--base-path", &base_path]);
164		assert!(generate.run("test_id", &String::from("test")).is_ok());
165		let buf = fs::read_to_string(key_path.as_path()).unwrap();
166		assert!(array_bytes::hex2bytes(&buf).is_ok());
167
168		assert!(generate.run("test_id", &String::from("test")).is_err());
169		let new_buf = fs::read_to_string(key_path).unwrap();
170		assert_eq!(
171			array_bytes::hex2bytes(&new_buf).unwrap(),
172			array_bytes::hex2bytes(&buf).unwrap()
173		);
174	}
175}