Skip to main content

soil_cli/commands/
run_cmd.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 crate::{
8	error::{Error, Result},
9	params::{
10		ImportParams, KeystoreParams, NetworkParams, OffchainWorkerParams, RpcEndpoint,
11		SharedParams, TransactionPoolParams,
12	},
13	CliConfiguration, PrometheusParams, RpcParams, RuntimeParams, TelemetryParams,
14};
15use clap::Parser;
16use regex::Regex;
17use soil_service::{
18	config::{
19		BasePath, IpNetwork, PrometheusConfig, RpcBatchRequestConfig, TransactionPoolOptions,
20	},
21	ChainSpec, Role,
22};
23use soil_telemetry::TelemetryEndpoints;
24use std::num::NonZeroU32;
25
26/// The `run` command used to run a node.
27#[derive(Debug, Clone, Parser)]
28pub struct RunCmd {
29	/// Enable validator mode.
30	///
31	/// The node will be started with the authority role and actively
32	/// participate in any consensus task that it can (e.g. depending on
33	/// availability of local keys).
34	#[arg(long)]
35	pub validator: bool,
36
37	/// Disable GRANDPA.
38	///
39	/// Disables voter when running in validator mode, otherwise disable the GRANDPA
40	/// observer.
41	#[arg(long)]
42	pub no_grandpa: bool,
43
44	/// The human-readable name for this node.
45	///
46	/// It's used as network node name.
47	#[arg(long, value_name = "NAME")]
48	pub name: Option<String>,
49
50	#[allow(missing_docs)]
51	#[clap(flatten)]
52	pub rpc_params: RpcParams,
53
54	#[allow(missing_docs)]
55	#[clap(flatten)]
56	pub telemetry_params: TelemetryParams,
57
58	#[allow(missing_docs)]
59	#[clap(flatten)]
60	pub prometheus_params: PrometheusParams,
61
62	#[allow(missing_docs)]
63	#[clap(flatten)]
64	pub runtime_params: RuntimeParams,
65
66	#[allow(missing_docs)]
67	#[clap(flatten)]
68	pub offchain_worker_params: OffchainWorkerParams,
69
70	#[allow(missing_docs)]
71	#[clap(flatten)]
72	pub shared_params: SharedParams,
73
74	#[allow(missing_docs)]
75	#[clap(flatten)]
76	pub import_params: ImportParams,
77
78	#[allow(missing_docs)]
79	#[clap(flatten)]
80	pub network_params: NetworkParams,
81
82	#[allow(missing_docs)]
83	#[clap(flatten)]
84	pub pool_config: TransactionPoolParams,
85
86	#[allow(missing_docs)]
87	#[clap(flatten)]
88	pub keystore_params: KeystoreParams,
89
90	/// Shortcut for `--name Alice --validator`.
91	///
92	/// Session keys for `Alice` are added to keystore.
93	#[arg(long, conflicts_with_all = &["bob", "charlie", "dave", "eve", "ferdie", "one", "two"])]
94	pub alice: bool,
95
96	/// Shortcut for `--name Bob --validator`.
97	///
98	/// Session keys for `Bob` are added to keystore.
99	#[arg(long, conflicts_with_all = &["alice", "charlie", "dave", "eve", "ferdie", "one", "two"])]
100	pub bob: bool,
101
102	/// Shortcut for `--name Charlie --validator`.
103	///
104	/// Session keys for `Charlie` are added to keystore.
105	#[arg(long, conflicts_with_all = &["alice", "bob", "dave", "eve", "ferdie", "one", "two"])]
106	pub charlie: bool,
107
108	/// Shortcut for `--name Dave --validator`.
109	///
110	/// Session keys for `Dave` are added to keystore.
111	#[arg(long, conflicts_with_all = &["alice", "bob", "charlie", "eve", "ferdie", "one", "two"])]
112	pub dave: bool,
113
114	/// Shortcut for `--name Eve --validator`.
115	///
116	/// Session keys for `Eve` are added to keystore.
117	#[arg(long, conflicts_with_all = &["alice", "bob", "charlie", "dave", "ferdie", "one", "two"])]
118	pub eve: bool,
119
120	/// Shortcut for `--name Ferdie --validator`.
121	///
122	/// Session keys for `Ferdie` are added to keystore.
123	#[arg(long, conflicts_with_all = &["alice", "bob", "charlie", "dave", "eve", "one", "two"])]
124	pub ferdie: bool,
125
126	/// Shortcut for `--name One --validator`.
127	///
128	/// Session keys for `One` are added to keystore.
129	#[arg(long, conflicts_with_all = &["alice", "bob", "charlie", "dave", "eve", "ferdie", "two"])]
130	pub one: bool,
131
132	/// Shortcut for `--name Two --validator`.
133	///
134	/// Session keys for `Two` are added to keystore.
135	#[arg(long, conflicts_with_all = &["alice", "bob", "charlie", "dave", "eve", "ferdie", "one"])]
136	pub two: bool,
137
138	/// Enable authoring even when offline.
139	#[arg(long)]
140	pub force_authoring: bool,
141
142	/// Run a temporary node.
143	///
144	/// A temporary directory will be created to store the configuration and will be deleted
145	/// at the end of the process.
146	///
147	/// Note: the directory is random per process execution. This directory is used as base path
148	/// which includes: database, node key and keystore.
149	///
150	/// When `--dev` is given and no explicit `--base-path`, this option is implied.
151	#[arg(long, conflicts_with = "base_path")]
152	pub tmp: bool,
153}
154
155impl RunCmd {
156	/// Get the `Sr25519Keyring` matching one of the flag.
157	pub fn get_keyring(&self) -> Option<subsoil::keyring::Sr25519Keyring> {
158		use subsoil::keyring::Sr25519Keyring::*;
159
160		if self.alice {
161			Some(Alice)
162		} else if self.bob {
163			Some(Bob)
164		} else if self.charlie {
165			Some(Charlie)
166		} else if self.dave {
167			Some(Dave)
168		} else if self.eve {
169			Some(Eve)
170		} else if self.ferdie {
171			Some(Ferdie)
172		} else if self.one {
173			Some(One)
174		} else if self.two {
175			Some(Two)
176		} else {
177			None
178		}
179	}
180}
181
182impl CliConfiguration for RunCmd {
183	fn shared_params(&self) -> &SharedParams {
184		&self.shared_params
185	}
186
187	fn import_params(&self) -> Option<&ImportParams> {
188		Some(&self.import_params)
189	}
190
191	fn network_params(&self) -> Option<&NetworkParams> {
192		Some(&self.network_params)
193	}
194
195	fn keystore_params(&self) -> Option<&KeystoreParams> {
196		Some(&self.keystore_params)
197	}
198
199	fn offchain_worker_params(&self) -> Option<&OffchainWorkerParams> {
200		Some(&self.offchain_worker_params)
201	}
202
203	fn node_name(&self) -> Result<String> {
204		let name: String = match (self.name.as_ref(), self.get_keyring()) {
205			(Some(name), _) => name.to_string(),
206			(_, Some(keyring)) => keyring.to_string(),
207			(None, None) => crate::generate_node_name(),
208		};
209
210		is_node_name_valid(&name).map_err(|msg| {
211			Error::Input(format!(
212				"Invalid node name '{}'. Reason: {}. If unsure, use none.",
213				name, msg
214			))
215		})?;
216
217		Ok(name)
218	}
219
220	fn dev_key_seed(&self, is_dev: bool) -> Result<Option<String>> {
221		Ok(self.get_keyring().map(|a| format!("//{}", a)).or_else(|| {
222			if is_dev {
223				Some("//Alice".into())
224			} else {
225				None
226			}
227		}))
228	}
229
230	fn telemetry_endpoints(
231		&self,
232		chain_spec: &Box<dyn ChainSpec>,
233	) -> Result<Option<TelemetryEndpoints>> {
234		let params = &self.telemetry_params;
235		Ok(if params.no_telemetry {
236			None
237		} else if !params.telemetry_endpoints.is_empty() {
238			Some(
239				TelemetryEndpoints::new(params.telemetry_endpoints.clone())
240					.map_err(|e| e.to_string())?,
241			)
242		} else {
243			chain_spec.telemetry_endpoints().clone()
244		})
245	}
246
247	fn role(&self, is_dev: bool) -> Result<Role> {
248		let keyring = self.get_keyring();
249		let is_authority = self.validator || is_dev || keyring.is_some();
250
251		Ok(if is_authority { Role::Authority } else { Role::Full })
252	}
253
254	fn force_authoring(&self) -> Result<bool> {
255		// Imply forced authoring on --dev
256		Ok(self.shared_params.dev || self.force_authoring)
257	}
258
259	fn prometheus_config(
260		&self,
261		default_listen_port: u16,
262		chain_spec: &Box<dyn ChainSpec>,
263	) -> Result<Option<PrometheusConfig>> {
264		Ok(self
265			.prometheus_params
266			.prometheus_config(default_listen_port, chain_spec.id().to_string()))
267	}
268
269	fn disable_grandpa(&self) -> Result<bool> {
270		Ok(self.no_grandpa)
271	}
272
273	fn rpc_max_connections(&self) -> Result<u32> {
274		Ok(self.rpc_params.rpc_max_connections)
275	}
276
277	fn rpc_cors(&self, is_dev: bool) -> Result<Option<Vec<String>>> {
278		self.rpc_params.rpc_cors(is_dev)
279	}
280
281	fn rpc_addr(&self, default_listen_port: u16) -> Result<Option<Vec<RpcEndpoint>>> {
282		self.rpc_params.rpc_addr(self.is_dev()?, self.validator, default_listen_port)
283	}
284
285	fn rpc_methods(&self) -> Result<soil_service::config::RpcMethods> {
286		Ok(self.rpc_params.rpc_methods.into())
287	}
288
289	fn rpc_max_request_size(&self) -> Result<u32> {
290		Ok(self.rpc_params.rpc_max_request_size)
291	}
292
293	fn rpc_max_response_size(&self) -> Result<u32> {
294		Ok(self.rpc_params.rpc_max_response_size)
295	}
296
297	fn rpc_max_subscriptions_per_connection(&self) -> Result<u32> {
298		Ok(self.rpc_params.rpc_max_subscriptions_per_connection)
299	}
300
301	fn rpc_buffer_capacity_per_connection(&self) -> Result<u32> {
302		Ok(self.rpc_params.rpc_message_buffer_capacity_per_connection)
303	}
304
305	fn rpc_batch_config(&self) -> Result<RpcBatchRequestConfig> {
306		self.rpc_params.rpc_batch_config()
307	}
308
309	fn rpc_rate_limit(&self) -> Result<Option<NonZeroU32>> {
310		Ok(self.rpc_params.rpc_rate_limit)
311	}
312
313	fn rpc_rate_limit_whitelisted_ips(&self) -> Result<Vec<IpNetwork>> {
314		Ok(self.rpc_params.rpc_rate_limit_whitelisted_ips.clone())
315	}
316
317	fn rpc_rate_limit_trust_proxy_headers(&self) -> Result<bool> {
318		Ok(self.rpc_params.rpc_rate_limit_trust_proxy_headers)
319	}
320
321	fn transaction_pool(&self, is_dev: bool) -> Result<TransactionPoolOptions> {
322		Ok(self.pool_config.transaction_pool(is_dev))
323	}
324
325	fn max_runtime_instances(&self) -> Result<Option<usize>> {
326		Ok(Some(self.runtime_params.max_runtime_instances))
327	}
328
329	fn runtime_cache_size(&self) -> Result<u8> {
330		Ok(self.runtime_params.runtime_cache_size)
331	}
332
333	fn base_path(&self) -> Result<Option<BasePath>> {
334		Ok(if self.tmp {
335			Some(BasePath::new_temp_dir()?)
336		} else {
337			match self.shared_params().base_path()? {
338				Some(r) => Some(r),
339				// If `dev` is enabled, we use the temp base path.
340				None if self.shared_params().is_dev() => Some(BasePath::new_temp_dir()?),
341				None => None,
342			}
343		})
344	}
345}
346
347/// Check whether a node name is considered as valid.
348pub fn is_node_name_valid(_name: &str) -> std::result::Result<(), &str> {
349	let name = _name.to_string();
350
351	if name.is_empty() {
352		return Err("Node name cannot be empty");
353	}
354
355	if name.chars().count() >= crate::NODE_NAME_MAX_LENGTH {
356		return Err("Node name too long");
357	}
358
359	let invalid_chars = r"[\\.@]";
360	let re = Regex::new(invalid_chars).unwrap();
361	if re.is_match(&name) {
362		return Err("Node name should not contain invalid chars such as '.' and '@'");
363	}
364
365	let invalid_patterns = r"^https?:";
366	let re = Regex::new(invalid_patterns).unwrap();
367	if re.is_match(&name) {
368		return Err("Node name should not contain urls");
369	}
370
371	Ok(())
372}
373
374#[cfg(test)]
375mod tests {
376	use super::*;
377
378	#[test]
379	fn tests_node_name_good() {
380		assert!(is_node_name_valid("short name").is_ok());
381		assert!(is_node_name_valid("www").is_ok());
382		assert!(is_node_name_valid("aawww").is_ok());
383		assert!(is_node_name_valid("wwwaa").is_ok());
384		assert!(is_node_name_valid("www aa").is_ok());
385	}
386
387	#[test]
388	fn tests_node_name_bad() {
389		assert!(is_node_name_valid("").is_err());
390		assert!(is_node_name_valid(
391			"very very long names are really not very cool for the ui at all, really they're not"
392		)
393		.is_err());
394		assert!(is_node_name_valid("Dots.not.Ok").is_err());
395		// NOTE: the urls below don't include a domain otherwise
396		// they'd get filtered for including a `.`
397		assert!(is_node_name_valid("http://visitme").is_err());
398		assert!(is_node_name_valid("http:/visitme").is_err());
399		assert!(is_node_name_valid("http:visitme").is_err());
400		assert!(is_node_name_valid("https://visitme").is_err());
401		assert!(is_node_name_valid("https:/visitme").is_err());
402		assert!(is_node_name_valid("https:visitme").is_err());
403		assert!(is_node_name_valid("www.visit.me").is_err());
404		assert!(is_node_name_valid("www.visit").is_err());
405		assert!(is_node_name_valid("hello\\world").is_err());
406		assert!(is_node_name_valid("visit.www").is_err());
407		assert!(is_node_name_valid("email@domain").is_err());
408	}
409}