referrerpolicy=no-referrer-when-downgrade

staging_chain_spec_builder/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
5
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with this program. If not, see <https://www.gnu.org/licenses/>.
18#![doc = include_str!("../README.md")]
19#[cfg(feature = "generate-readme")]
20docify::compile_markdown!("README.docify.md", "README.md");
21
22use clap::{Parser, Subcommand};
23use sc_chain_spec::{
24	json_patch, set_code_substitute_in_json_chain_spec, update_code_in_json_chain_spec, ChainType,
25	GenericChainSpec, GenesisConfigBuilderRuntimeCaller,
26};
27use serde::{Deserialize, Serialize};
28use serde_json::Value;
29use std::{
30	borrow::Cow,
31	fs,
32	path::{Path, PathBuf},
33};
34
35/// A utility to easily create a chain spec definition.
36#[derive(Debug, Parser)]
37#[command(rename_all = "kebab-case", version, about)]
38pub struct ChainSpecBuilder {
39	#[command(subcommand)]
40	pub command: ChainSpecBuilderCmd,
41	/// The path where the chain spec should be saved.
42	#[arg(long, short, default_value = "./chain_spec.json")]
43	pub chain_spec_path: PathBuf,
44}
45
46#[derive(Debug, Subcommand)]
47#[command(rename_all = "kebab-case")]
48pub enum ChainSpecBuilderCmd {
49	Create(CreateCmd),
50	Verify(VerifyCmd),
51	UpdateCode(UpdateCodeCmd),
52	ConvertToRaw(ConvertToRawCmd),
53	ListPresets(ListPresetsCmd),
54	DisplayPreset(DisplayPresetCmd),
55	AddCodeSubstitute(AddCodeSubstituteCmd),
56}
57
58/// Create a new chain spec by interacting with the provided runtime wasm blob.
59#[derive(Parser, Debug)]
60pub struct CreateCmd {
61	/// The name of chain.
62	#[arg(long, short = 'n', default_value = "Custom")]
63	chain_name: String,
64	/// The chain id.
65	#[arg(long, short = 'i', default_value = "custom")]
66	chain_id: String,
67	/// The chain type.
68	#[arg(value_enum, short = 't', default_value = "live")]
69	chain_type: ChainType,
70	/// The para ID for your chain.
71	#[arg(long, value_enum, short = 'p', requires = "relay_chain")]
72	pub para_id: Option<u32>,
73	/// The relay chain you wish to connect to.
74	#[arg(long, value_enum, short = 'c')]
75	pub relay_chain: Option<String>,
76	/// The path to runtime wasm blob.
77	#[arg(long, short, alias = "runtime-wasm-path")]
78	runtime: PathBuf,
79	/// Export chainspec as raw storage.
80	#[arg(long, short = 's')]
81	raw_storage: bool,
82	/// Verify the genesis config. This silently generates the raw storage from genesis config. Any
83	/// errors will be reported.
84	#[arg(long, short = 'v')]
85	verify: bool,
86	/// Chain properties in `KEY=VALUE` format.
87	///
88	/// Multiple `KEY=VALUE` entries can be specified and separated by a comma.
89	///
90	/// Example: `--properties tokenSymbol=UNIT,tokenDecimals=12,ss58Format=42,isEthereum=false`
91	/// Or: `--properties tokenSymbol=UNIT --properties tokenDecimals=12 --properties ss58Format=42
92	/// --properties=isEthereum=false`
93	///
94	/// The first uses comma as separation and the second passes the argument multiple times. Both
95	/// styles can also be mixed.
96	#[arg(long, default_value = "tokenSymbol=UNIT,tokenDecimals=12")]
97	pub properties: Vec<String>,
98	#[command(subcommand)]
99	action: GenesisBuildAction,
100
101	/// Allows to provide the runtime code blob, instead of reading it from the provided file path.
102	#[clap(skip)]
103	code: Option<Cow<'static, [u8]>>,
104}
105
106#[derive(Subcommand, Debug, Clone)]
107enum GenesisBuildAction {
108	Patch(PatchCmd),
109	Full(FullCmd),
110	Default(DefaultCmd),
111	NamedPreset(NamedPresetCmd),
112}
113
114/// Patches the runtime's default genesis config with provided patch.
115#[derive(Parser, Debug, Clone)]
116struct PatchCmd {
117	/// The path to the runtime genesis config patch.
118	patch_path: PathBuf,
119}
120
121/// Build the genesis config for runtime using provided json file. No defaults will be used.
122#[derive(Parser, Debug, Clone)]
123struct FullCmd {
124	/// The path to the full runtime genesis config json file.
125	config_path: PathBuf,
126}
127
128/// Gets the default genesis config for the runtime and uses it in ChainSpec. Please note that
129/// default genesis config may not be valid. For some runtimes initial values should be added there
130/// (e.g. session keys, babe epoch).
131#[derive(Parser, Debug, Clone)]
132struct DefaultCmd {}
133
134/// Uses named preset provided by runtime to build the chains spec.
135#[derive(Parser, Debug, Clone)]
136struct NamedPresetCmd {
137	preset_name: String,
138}
139
140/// Updates the code in the provided input chain spec.
141///
142/// The code field of the chain spec will be updated with the runtime provided in the
143/// command line. This operation supports both plain and raw formats.
144///
145/// This command does not update chain-spec file in-place. The result of this command will be stored
146/// in a file given as `-c/--chain-spec-path` command line argument.
147#[derive(Parser, Debug, Clone)]
148pub struct UpdateCodeCmd {
149	/// Chain spec to be updated.
150	///
151	/// Please note that the file will not be updated in-place.
152	pub input_chain_spec: PathBuf,
153	/// The path to new runtime wasm blob to be stored into chain-spec.
154	#[arg(alias = "runtime-wasm-path")]
155	pub runtime: PathBuf,
156}
157
158/// Add a code substitute in the chain spec.
159///
160/// The `codeSubstitute` object of the chain spec will be updated with the block height as key and
161/// runtime code as value. This operation supports both plain and raw formats. The `codeSubstitute`
162/// field instructs the node to use the provided runtime code at the given block height. This is
163/// useful when the chain can not progress on its own due to a bug that prevents block-building.
164///
165/// Note: For parachains, the validation function on the relaychain needs to be adjusted too,
166/// otherwise blocks built using the substituted parachain runtime will be rejected.
167#[derive(Parser, Debug, Clone)]
168pub struct AddCodeSubstituteCmd {
169	/// Chain spec to be updated.
170	pub input_chain_spec: PathBuf,
171	/// New runtime wasm blob that should replace the existing code.
172	#[arg(alias = "runtime-wasm-path")]
173	pub runtime: PathBuf,
174	/// The block height at which the code should be substituted.
175	pub block_height: u64,
176}
177
178/// Converts the given chain spec into the raw format.
179#[derive(Parser, Debug, Clone)]
180pub struct ConvertToRawCmd {
181	/// Chain spec to be converted.
182	pub input_chain_spec: PathBuf,
183}
184
185/// Lists available presets
186#[derive(Parser, Debug, Clone)]
187pub struct ListPresetsCmd {
188	/// The path to runtime wasm blob.
189	#[arg(long, short, alias = "runtime-wasm-path")]
190	pub runtime: PathBuf,
191}
192
193/// Displays given preset
194#[derive(Parser, Debug, Clone)]
195pub struct DisplayPresetCmd {
196	/// The path to runtime wasm blob.
197	#[arg(long, short, alias = "runtime-wasm-path")]
198	pub runtime: PathBuf,
199	/// Preset to be displayed. If none is given default will be displayed.
200	#[arg(long, short)]
201	pub preset_name: Option<String>,
202}
203
204/// Verifies the provided input chain spec.
205///
206/// Silently checks if given input chain spec can be converted to raw. It allows to check if all
207/// RuntimeGenesisConfig fields are properly initialized and if the json does not contain invalid
208/// fields.
209#[derive(Parser, Debug, Clone)]
210pub struct VerifyCmd {
211	/// Chain spec to be verified.
212	pub input_chain_spec: PathBuf,
213}
214
215#[derive(Deserialize, Serialize, Clone)]
216pub struct ParachainExtension {
217	/// The relay chain of the Parachain.
218	pub relay_chain: String,
219	/// The id of the Parachain.
220	pub para_id: Option<u32>,
221}
222
223type ChainSpec = GenericChainSpec<()>;
224
225impl ChainSpecBuilder {
226	/// Executes the internal command.
227	pub fn run(&self) -> Result<(), String> {
228		let chain_spec_path = self.chain_spec_path.to_path_buf();
229
230		match &self.command {
231			ChainSpecBuilderCmd::Create(cmd) => {
232				let chain_spec_json = generate_chain_spec_for_runtime(&cmd)?;
233				fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())?;
234			},
235			ChainSpecBuilderCmd::UpdateCode(UpdateCodeCmd {
236				ref input_chain_spec,
237				ref runtime,
238			}) => {
239				let mut chain_spec_json = extract_chain_spec_json(input_chain_spec.as_path())?;
240
241				update_code_in_json_chain_spec(
242					&mut chain_spec_json,
243					&fs::read(runtime.as_path())
244						.map_err(|e| format!("Wasm blob file could not be read: {e}"))?[..],
245				);
246
247				let chain_spec_json = serde_json::to_string_pretty(&chain_spec_json)
248					.map_err(|e| format!("to pretty failed: {e}"))?;
249				fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())?;
250			},
251			ChainSpecBuilderCmd::AddCodeSubstitute(AddCodeSubstituteCmd {
252				ref input_chain_spec,
253				ref runtime,
254				block_height,
255			}) => {
256				let mut chain_spec_json = extract_chain_spec_json(input_chain_spec.as_path())?;
257
258				set_code_substitute_in_json_chain_spec(
259					&mut chain_spec_json,
260					&fs::read(runtime.as_path())
261						.map_err(|e| format!("Wasm blob file could not be read: {e}"))?[..],
262					*block_height,
263				);
264				let chain_spec_json = serde_json::to_string_pretty(&chain_spec_json)
265					.map_err(|e| format!("to pretty failed: {e}"))?;
266				fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())?;
267			},
268			ChainSpecBuilderCmd::ConvertToRaw(ConvertToRawCmd { ref input_chain_spec }) => {
269				let chain_spec = ChainSpec::from_json_file(input_chain_spec.clone())?;
270
271				let mut genesis_json =
272					serde_json::from_str::<serde_json::Value>(&chain_spec.as_json(true)?)
273						.map_err(|e| format!("Conversion to json failed: {e}"))?;
274
275				// We want to extract only raw genesis ("genesis::raw" key), and apply it as a patch
276				// for the original json file.
277				genesis_json.as_object_mut().map(|map| {
278					map.retain(|key, _| key == "genesis");
279				});
280
281				let mut org_chain_spec_json = extract_chain_spec_json(input_chain_spec.as_path())?;
282
283				// The original plain genesis ("genesis::runtimeGenesis") is no longer needed, so
284				// just remove it:
285				org_chain_spec_json
286					.get_mut("genesis")
287					.and_then(|genesis| genesis.as_object_mut())
288					.and_then(|genesis| genesis.remove("runtimeGenesis"));
289				json_patch::merge(&mut org_chain_spec_json, genesis_json);
290
291				let chain_spec_json = serde_json::to_string_pretty(&org_chain_spec_json)
292					.map_err(|e| format!("Conversion to pretty failed: {e}"))?;
293				fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())?;
294			},
295			ChainSpecBuilderCmd::Verify(VerifyCmd { ref input_chain_spec }) => {
296				let chain_spec = ChainSpec::from_json_file(input_chain_spec.clone())?;
297				serde_json::from_str::<serde_json::Value>(&chain_spec.as_json(true)?)
298					.map_err(|e| format!("Conversion to json failed: {e}"))?;
299			},
300			ChainSpecBuilderCmd::ListPresets(ListPresetsCmd { runtime }) => {
301				let code = fs::read(runtime.as_path())
302					.map_err(|e| format!("wasm blob shall be readable {e}"))?;
303				let caller: GenesisConfigBuilderRuntimeCaller =
304					GenesisConfigBuilderRuntimeCaller::new(&code[..]);
305				let presets = caller
306					.preset_names()
307					.map_err(|e| format!("getting default config from runtime should work: {e}"))?;
308				println!("{}", serde_json::json!({"presets":presets}).to_string());
309			},
310			ChainSpecBuilderCmd::DisplayPreset(DisplayPresetCmd { runtime, preset_name }) => {
311				let code = fs::read(runtime.as_path())
312					.map_err(|e| format!("wasm blob shall be readable {e}"))?;
313				let caller: GenesisConfigBuilderRuntimeCaller =
314					GenesisConfigBuilderRuntimeCaller::new(&code[..]);
315				let preset = caller
316					.get_named_preset(preset_name.as_ref())
317					.map_err(|e| format!("getting default config from runtime should work: {e}"))?;
318				println!("{preset}");
319			},
320		}
321		Ok(())
322	}
323
324	/// Sets the code used by [`CreateCmd`]
325	///
326	/// The file pointed by `CreateCmd::runtime` field will not be read. Provided blob will used
327	/// instead for chain spec generation.
328	pub fn set_create_cmd_runtime_code(&mut self, code: Cow<'static, [u8]>) {
329		match &mut self.command {
330			ChainSpecBuilderCmd::Create(cmd) => {
331				cmd.code = Some(code);
332			},
333			_ => {
334				panic!("Overwriting code blob is only supported for CreateCmd");
335			},
336		};
337	}
338}
339
340fn process_action<T: Serialize + Clone + Sync + 'static>(
341	cmd: &CreateCmd,
342	code: &[u8],
343	builder: sc_chain_spec::ChainSpecBuilder<T>,
344) -> Result<String, String> {
345	let builder = match cmd.action {
346		GenesisBuildAction::NamedPreset(NamedPresetCmd { ref preset_name }) =>
347			builder.with_genesis_config_preset_name(&preset_name),
348		GenesisBuildAction::Patch(PatchCmd { ref patch_path }) => {
349			let patch = fs::read(patch_path.as_path())
350				.map_err(|e| format!("patch file {patch_path:?} shall be readable: {e}"))?;
351			builder.with_genesis_config_patch(serde_json::from_slice::<Value>(&patch[..]).map_err(
352				|e| format!("patch file {patch_path:?} shall contain a valid json: {e}"),
353			)?)
354		},
355		GenesisBuildAction::Full(FullCmd { ref config_path }) => {
356			let config = fs::read(config_path.as_path())
357				.map_err(|e| format!("config file {config_path:?} shall be readable: {e}"))?;
358			builder.with_genesis_config(serde_json::from_slice::<Value>(&config[..]).map_err(
359				|e| format!("config file {config_path:?} shall contain a valid json: {e}"),
360			)?)
361		},
362		GenesisBuildAction::Default(DefaultCmd {}) => {
363			let caller: GenesisConfigBuilderRuntimeCaller =
364				GenesisConfigBuilderRuntimeCaller::new(&code);
365			let default_config = caller
366				.get_default_config()
367				.map_err(|e| format!("getting default config from runtime should work: {e}"))?;
368			builder.with_genesis_config(default_config)
369		},
370	};
371
372	let chain_spec = builder.build();
373
374	match (cmd.verify, cmd.raw_storage) {
375		(_, true) => chain_spec.as_json(true),
376		(true, false) => {
377			chain_spec.as_json(true)?;
378			println!("Genesis config verification: OK");
379			chain_spec.as_json(false)
380		},
381		(false, false) => chain_spec.as_json(false),
382	}
383}
384
385impl CreateCmd {
386	/// Returns the associated runtime code.
387	///
388	/// If the code blob was previously set, returns it. Otherwise reads the file.
389	fn get_runtime_code(&self) -> Result<Cow<'static, [u8]>, String> {
390		Ok(if let Some(code) = self.code.clone() {
391			code
392		} else {
393			fs::read(self.runtime.as_path())
394				.map_err(|e| format!("wasm blob shall be readable {e}"))?
395				.into()
396		})
397	}
398}
399
400/// Parses chain properties passed as a comma-separated KEY=VALUE pairs.
401fn parse_properties(raw: &String, props: &mut sc_chain_spec::Properties) -> Result<(), String> {
402	for pair in raw.split(',') {
403		let mut iter = pair.splitn(2, '=');
404		let key = iter
405			.next()
406			.ok_or_else(|| format!("Invalid chain property key: {pair}"))?
407			.trim()
408			.to_owned();
409		let value_str = iter
410			.next()
411			.ok_or_else(|| format!("Invalid chain property value for key: {key}"))?
412			.trim();
413
414		// Try to parse as bool, number, or fallback to String
415		let value = match value_str.parse::<bool>() {
416			Ok(b) => Value::Bool(b),
417			Err(_) => match value_str.parse::<u32>() {
418				Ok(i) => Value::Number(i.into()),
419				Err(_) => Value::String(value_str.to_string()),
420			},
421		};
422
423		props.insert(key, value);
424	}
425	Ok(())
426}
427
428/// Processes `CreateCmd` and returns string representation of JSON version of `ChainSpec`.
429pub fn generate_chain_spec_for_runtime(cmd: &CreateCmd) -> Result<String, String> {
430	let code = cmd.get_runtime_code()?;
431
432	let chain_type = &cmd.chain_type;
433
434	let mut properties = sc_chain_spec::Properties::new();
435	for raw in &cmd.properties {
436		parse_properties(raw, &mut properties)?;
437	}
438
439	let builder = ChainSpec::builder(&code[..], Default::default())
440		.with_name(&cmd.chain_name[..])
441		.with_id(&cmd.chain_id[..])
442		.with_properties(properties)
443		.with_chain_type(chain_type.clone());
444
445	let chain_spec_json_string = process_action(&cmd, &code[..], builder)?;
446	let parachain_properties = cmd.relay_chain.as_ref().map(|rc| {
447		cmd.para_id
448			.map(|para_id| {
449				serde_json::json!({
450					"relay_chain": rc,
451					"para_id": para_id,
452				})
453			})
454			.unwrap_or(serde_json::json!({
455				"relay_chain": rc,
456			}))
457	});
458
459	let chain_spec = parachain_properties
460		.map(|props| {
461			let chain_spec_json_blob = serde_json::from_str(chain_spec_json_string.as_str())
462				.map_err(|e| format!("deserialization a json failed {e}"));
463			chain_spec_json_blob.and_then(|mut cs| {
464				json_patch::merge(&mut cs, props);
465				serde_json::to_string_pretty(&cs).map_err(|e| format!("to pretty failed: {e}"))
466			})
467		})
468		.unwrap_or(Ok(chain_spec_json_string));
469	chain_spec
470}
471
472/// Extract any chain spec and convert it to JSON
473fn extract_chain_spec_json(input_chain_spec: &Path) -> Result<serde_json::Value, String> {
474	let chain_spec = &fs::read(input_chain_spec)
475		.map_err(|e| format!("Provided chain spec could not be read: {e}"))?;
476
477	serde_json::from_slice(&chain_spec).map_err(|e| format!("Conversion to json failed: {e}"))
478}