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