Skip to main content

zombienet_orchestrator/generators/
chain_spec.rs

1use std::{
2    collections::HashMap,
3    path::{Path, PathBuf},
4    process::Stdio,
5};
6
7use anyhow::anyhow;
8use configuration::{
9    types::{AssetLocation, Chain, ChainSpecRuntime, JsonOverrides, ParaId},
10    HrmpChannelConfig,
11};
12use provider::{
13    constants::NODE_CONFIG_DIR,
14    types::{GenerateFileCommand, GenerateFilesOptions, TransferedFile},
15    DynNamespace, ProviderError,
16};
17use sc_chain_spec::{GenericChainSpec, GenesisConfigBuilderRuntimeCaller};
18use serde::{Deserialize, Serialize};
19use serde_json::json;
20use support::{constants::THIS_IS_A_BUG, fs::FileSystem, replacer::apply_replacements};
21use tokio::{fs as tokio_fs, io::AsyncWriteExt, process::Command};
22use tracing::{debug, info, trace, warn};
23
24use super::{
25    chain_spec_key_types::{parse_chain_spec_key_types, ChainSpecKeyType},
26    errors::GeneratorError,
27};
28use crate::{
29    generators::keystore_key_types::KeyScheme,
30    network_spec::{node::NodeSpec, parachain::ParachainSpec, relaychain::RelaychainSpec},
31    ScopedFilesystem,
32};
33
34// Zombie key to insert (//Zombie)
35const ZOMBIE_KEY: &str = "5FTcLfwFc7ctvqp3RhbEig6UuHLHcHVRujuUm8r21wy4dAR8";
36
37// TODO: (javier) move to state
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39pub enum Context {
40    Relay,
41    Para { relay_chain: Chain, para_id: ParaId },
42}
43
44/// Posible chain-spec formats
45#[derive(Debug, Clone, Copy)]
46enum ChainSpecFormat {
47    Plain,
48    Raw,
49}
50/// Key types to replace in spec
51#[derive(Debug, Clone, Copy)]
52enum KeyType {
53    Session,
54    Aura,
55    Grandpa,
56}
57
58#[derive(Debug, Clone, Copy, Default)]
59enum SessionKeyType {
60    // Default derivarion (e.g `//`)
61    #[default]
62    Default,
63    // Stash detivarion (e.g `//<name>/stash`)
64    Stash,
65    // EVM session type
66    Evm,
67}
68
69type MaybeExpectedPath = Option<PathBuf>;
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub enum CommandInContext {
73    Local(String, MaybeExpectedPath),
74    Remote(String, MaybeExpectedPath),
75}
76
77impl CommandInContext {
78    fn cmd(&self) -> &str {
79        match self {
80            CommandInContext::Local(cmd, _) | CommandInContext::Remote(cmd, _) => cmd.as_ref(),
81        }
82    }
83}
84
85#[derive(Debug)]
86pub struct ParaGenesisConfig<T: AsRef<Path>> {
87    pub(crate) state_path: T,
88    pub(crate) wasm_path: T,
89    pub(crate) id: u32,
90    pub(crate) as_parachain: bool,
91}
92
93/// Presets to check if is not set by the user.
94/// We check if the preset is valid for the runtime in order
95/// and if non of them are preset we fallback to the `default config`.
96const DEFAULT_PRESETS_TO_CHECK: [&str; 3] = ["local_testnet", "development", "dev"];
97
98/// Chain-spec builder representation
99///
100/// Multiple options are supported, and the current order is:
101/// IF [`asset_location`] is _some_ -> Use this chain_spec by copying the file from [`AssetLocation`]
102/// ELSE IF [`runtime_location`] is _some_ -> generate the chain-spec using the sc-chain-spec builder.
103/// ELSE -> Fallback to use the `default` or customized cmd.
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct ChainSpec {
107    // Name of the spec file, most of the times could be the same as the chain_name. (e.g rococo-local)
108    chain_spec_name: String,
109    // Location of the chain-spec to use
110    asset_location: Option<AssetLocation>,
111    // Location of the runtime to use
112    runtime: Option<ChainSpecRuntime>,
113    maybe_plain_path: Option<PathBuf>,
114    chain_name: Option<String>,
115    raw_path: Option<PathBuf>,
116    // The binary to build the chain-spec
117    command: Option<CommandInContext>,
118    // The subcommand to exec e.g. `export-chain-spec`
119    subcommand: Option<String>,
120    // Imgae to use for build the chain-spec
121    image: Option<String>,
122    // Contex of the network (e.g relay or para)
123    context: Context,
124}
125
126impl ChainSpec {
127    pub(crate) fn new(chain_spec_name: impl Into<String>, context: Context) -> Self {
128        Self {
129            chain_spec_name: chain_spec_name.into(),
130            chain_name: None,
131            maybe_plain_path: None,
132            asset_location: None,
133            runtime: None,
134            raw_path: None,
135            command: None,
136            subcommand: None,
137            image: None,
138            context,
139        }
140    }
141
142    async fn resolve_subcommand(&mut self, ns: &DynNamespace) -> Result<(), GeneratorError> {
143        if self.subcommand.is_none() {
144            // SAFETY: we ensure that command is some with the first check of the fn
145            // default as empty
146            let main_cmd_parts: Vec<&str> = self
147                .command
148                .as_ref()
149                .unwrap()
150                .cmd()
151                .split_whitespace()
152                .collect();
153            let main_cmd = main_cmd_parts
154                .first()
155                .expect("main_cmd should be preset to build the expect. qed");
156            debug!("resolving subcommand for: {main_cmd}");
157
158            // Prefer `export-chain-spec` when the binary exposes it (see #561), otherwise
159            // fall back to `build-spec` for older nodes. Do not special-case
160            // `test-parachain` / empty-chain `polkadot-parachain`: those binaries in
161            // current polkadot-sdk only ship `export-chain-spec`, and hardcoding
162            // `build-spec` breaks chain-spec generation for them.
163            let help_output = ns
164                .get_node_available_args((main_cmd.to_string(), self.image.clone()))
165                .await?;
166
167            self.subcommand = if help_output.contains("export-chain-spec") {
168                Some("export-chain-spec".to_string())
169            } else {
170                Some("build-spec".to_string())
171            };
172        }
173
174        Ok(())
175    }
176
177    pub(crate) fn chain_spec_name(&self) -> &str {
178        self.chain_spec_name.as_ref()
179    }
180
181    pub(crate) fn chain_name(&self) -> Option<&str> {
182        self.chain_name.as_deref()
183    }
184
185    pub(crate) fn set_chain_name(mut self, chain_name: impl Into<String>) -> Self {
186        self.chain_name = Some(chain_name.into());
187        self
188    }
189
190    pub(crate) fn with_asset_location(mut self, location: AssetLocation) -> Self {
191        self.asset_location = Some(location);
192        self
193    }
194
195    pub(crate) fn asset_location(&self) -> Option<&AssetLocation> {
196        self.asset_location.as_ref()
197    }
198
199    pub(crate) fn runtime(mut self, chain_spec_runtime: ChainSpecRuntime) -> Self {
200        self.runtime = Some(chain_spec_runtime);
201        self
202    }
203
204    pub(crate) fn command(
205        mut self,
206        command: impl Into<String>,
207        is_local: bool,
208        expected_path: Option<&str>,
209    ) -> Self {
210        let maybe_expected_path = expected_path.map(PathBuf::from);
211        let cmd = if is_local {
212            CommandInContext::Local(command.into(), maybe_expected_path)
213        } else {
214            CommandInContext::Remote(command.into(), maybe_expected_path)
215        };
216        self.command = Some(cmd);
217        self
218    }
219
220    pub(crate) fn image(mut self, image: Option<String>) -> Self {
221        self.image = image;
222        self
223    }
224
225    /// Build the chain-spec
226    ///
227    /// Chain spec generation flow:
228    /// if chain_spec_path is set -> use this chain_spec
229    /// else if runtime_path is set and cmd is compatible with chain-spec-builder -> use the chain-spec-builder
230    /// else if chain_spec_command is set -> use this cmd for generate the chain_spec
231    /// else -> use the default command.
232    pub async fn build<'a, T>(
233        &mut self,
234        ns: &DynNamespace,
235        scoped_fs: &ScopedFilesystem<'a, T>,
236    ) -> Result<(), GeneratorError>
237    where
238        T: FileSystem,
239    {
240        if self.asset_location.is_none() && self.command.is_none() && self.runtime.is_none() {
241            return Err(GeneratorError::ChainSpecGeneration(
242                "Can not build the chain spec without set the command, asset_location or runtime"
243                    .to_string(),
244            ));
245        }
246
247        let maybe_plain_spec_path = PathBuf::from(format!("{}-plain.json", self.chain_spec_name));
248
249        // if asset_location is some, then copy the asset to the `base_dir` of the ns with the name `<name>-plain.json`
250        if let Some(location) = self.asset_location.as_ref() {
251            let maybe_plain_spec_full_path = scoped_fs.full_path(maybe_plain_spec_path.as_path());
252            location
253                .dump_asset(maybe_plain_spec_full_path)
254                .await
255                .map_err(|e| {
256                    GeneratorError::ChainSpecGeneration(format!(
257                        "Error {e} dumping location {location:?}"
258                    ))
259                })?;
260        } else if let Some(runtime) = self.runtime.as_ref() {
261            trace!(
262                "Creating chain-spec with runtime from localtion: {}",
263                runtime.location
264            );
265            // First dump the runtime into the ns scoped fs, since we want to easily reproduce
266            let runtime_file_name = PathBuf::from(format!("{}-runtime.wasm", self.chain_spec_name));
267            let runtime_path_ns = scoped_fs.full_path(runtime_file_name.as_path());
268            runtime
269                .location
270                .dump_asset(runtime_path_ns)
271                .await
272                .map_err(|e| {
273                    GeneratorError::ChainSpecGeneration(format!(
274                        "Error {e} dumping location {:?}",
275                        runtime.location
276                    ))
277                })?;
278
279            // list the presets to check if match with the supplied one or one of the defaults
280            let runtime_code = scoped_fs.read(runtime_file_name.as_path()).await?;
281
282            let caller: GenesisConfigBuilderRuntimeCaller =
283                GenesisConfigBuilderRuntimeCaller::new(&runtime_code[..]);
284            let presets = caller.preset_names().map_err(|e| {
285                GeneratorError::ChainSpecGeneration(format!(
286                    "getting default config from runtime should work: {e}"
287                ))
288            })?;
289
290            // check the preset to use with this priorities:
291            // - IF user provide a preset (and if present) use it
292            // - else (user don't provide preset or the provided one isn't preset)
293            //     check the [`DEFAULT_PRESETS_TO_CHECK`] in order to find one valid
294            // - If we can't find any valid preset use the `default config` from the runtime
295
296            let preset_to_check = if let Some(preset) = &runtime.preset {
297                [vec![preset.as_str()], DEFAULT_PRESETS_TO_CHECK.to_vec()].concat()
298            } else {
299                DEFAULT_PRESETS_TO_CHECK.to_vec()
300            };
301            let preset = preset_to_check
302                .iter()
303                .find(|preset| presets.iter().any(|item| item == *preset));
304
305            trace!("presets: {:?} - preset to use: {:?}", presets, preset);
306            let builder = if let Some(preset) = preset {
307                GenericChainSpec::<()>::builder(&runtime_code[..], ())
308                    .with_genesis_config_preset_name(preset)
309            } else {
310                // default config
311                let default_config = caller.get_default_config().map_err(|e| {
312                    GeneratorError::ChainSpecGeneration(format!(
313                        "getting default config from runtime should work: {e}"
314                    ))
315                })?;
316
317                GenericChainSpec::<()>::builder(&runtime_code[..], ())
318                    .with_genesis_config(default_config)
319            };
320
321            let builder = if let Context::Para {
322                relay_chain: _,
323                para_id: _,
324            } = &self.context
325            {
326                builder.with_id(self.chain_spec_name())
327            } else {
328                builder
329            };
330
331            let builder = if let Some(chain_name) = self.chain_name.as_ref() {
332                builder.with_name(chain_name)
333            } else {
334                builder
335            };
336
337            let chain_spec = builder.build();
338
339            let contents = chain_spec.as_json(false).map_err(|e| {
340                GeneratorError::ChainSpecGeneration(format!(
341                    "getting chain-spec as json should work, err: {e}"
342                ))
343            })?;
344
345            scoped_fs.write(&maybe_plain_spec_path, contents).await?;
346        } else {
347            trace!("Creating chain-spec with command");
348            // we should create the chain-spec using command.
349            let mut replacement_value = String::default();
350            if let Some(chain_name) = self.chain_name.as_ref() {
351                if !chain_name.is_empty() {
352                    replacement_value.clone_from(chain_name);
353                }
354            };
355
356            self.resolve_subcommand(ns).await?;
357
358            // SAFETY: we ensure that command is some with the first check of the fn
359            // default as empty
360            let cmd_tpl = self.command.as_ref().unwrap().cmd();
361            let is_export_chain_spec = self.subcommand.as_deref() == Some("export-chain-spec");
362
363            let sanitized_cmd = if !replacement_value.is_empty() {
364                cmd_tpl.to_owned()
365            } else if is_export_chain_spec {
366                // Some released node binaries default `export-chain-spec --chain` to the
367                // literal string `"local"` (fixed in newer polkadot-sdk, but older/released
368                // images still carry the bug), which isn't a valid preset for most
369                // parachains and fails with "Error opening spec file `local`". Pass an
370                // explicit empty id instead of omitting `--chain`, so behavior doesn't
371                // depend on the target binary's own default value.
372                cmd_tpl.replace("--chain {{chainName}}", "--chain=")
373            } else {
374                // build-spec: we need to remove the `--chain` flag
375                cmd_tpl.replace("--chain", "")
376            };
377
378            let mut replacements = HashMap::from([("chainName", replacement_value.as_str())]);
379
380            if is_export_chain_spec {
381                replacements.insert("subCommand", "export-chain-spec");
382                replacements.insert("disableBootnodes", "");
383            } else {
384                // use build-spec
385                replacements.insert("subCommand", "build-spec");
386                replacements.insert("disableBootnodes", "--disable-default-bootnode");
387            }
388
389            let full_cmd = apply_replacements(&sanitized_cmd, &replacements);
390
391            debug!("full_cmd: {:?}", full_cmd);
392
393            let parts: Vec<&str> = full_cmd.split_whitespace().collect();
394            let Some((cmd, args)) = parts.split_first() else {
395                return Err(GeneratorError::ChainSpecGeneration(format!(
396                    "Invalid generator command: {full_cmd}"
397                )));
398            };
399            trace!("cmd: {:?} - args: {:?}", cmd, args);
400
401            let generate_command =
402                GenerateFileCommand::new(cmd, maybe_plain_spec_path.clone()).args(args);
403            if let Some(cmd) = &self.command {
404                match cmd {
405                    CommandInContext::Local(_, expected_path) => {
406                        build_locally(generate_command, scoped_fs, expected_path.as_deref()).await?
407                    },
408                    CommandInContext::Remote(_, expected_path) => {
409                        let options = GenerateFilesOptions::new(
410                            vec![generate_command],
411                            self.image.clone(),
412                            expected_path.clone(),
413                        );
414                        ns.generate_files(options).await?;
415                    },
416                }
417            }
418        }
419
420        // check if the _generated_ spec is in raw mode.
421        if is_raw(maybe_plain_spec_path.clone(), scoped_fs).await? {
422            let spec_path = PathBuf::from(format!("{}.json", self.chain_spec_name));
423            let tf_file = TransferedFile::new(
424                &PathBuf::from_iter([ns.base_dir(), &maybe_plain_spec_path]),
425                &spec_path,
426            );
427            scoped_fs.copy_files(vec![&tf_file]).await.map_err(|e| {
428                GeneratorError::ChainSpecGeneration(format!(
429                    "Error copying file: {tf_file}, err: {e}"
430                ))
431            })?;
432
433            self.raw_path = Some(spec_path);
434        } else {
435            self.maybe_plain_path = Some(maybe_plain_spec_path);
436        }
437        Ok(())
438    }
439
440    pub async fn build_raw<'a, T>(
441        &mut self,
442        ns: &DynNamespace,
443        scoped_fs: &ScopedFilesystem<'a, T>,
444        relay_chain_id: Option<Chain>,
445    ) -> Result<(), GeneratorError>
446    where
447        T: FileSystem,
448    {
449        warn!("Building raw version from {:?}", self);
450        // raw path already set, no more work to do here...
451        let None = self.raw_path else {
452            return Ok(());
453        };
454
455        // expected raw path
456        let raw_spec_path = PathBuf::from(format!("{}.json", self.chain_spec_name));
457
458        match self
459            .try_build_raw_with_generic(scoped_fs, relay_chain_id.clone(), raw_spec_path.as_path())
460            .await
461        {
462            Ok(_) => return Ok(()),
463            Err(err) => {
464                if Self::should_retry_with_command(&err) && self.command.is_some() {
465                    warn!(
466                        "GenericChainSpec raw generation failed ({}). Falling back to command execution.",
467                        err
468                    );
469                } else {
470                    return Err(err);
471                }
472            },
473        }
474
475        self.build_raw_with_command(ns, scoped_fs, raw_spec_path, relay_chain_id)
476            .await?;
477
478        Ok(())
479    }
480
481    async fn try_build_raw_with_generic<'a, T>(
482        &mut self,
483        scoped_fs: &ScopedFilesystem<'a, T>,
484        relay_chain_id: Option<Chain>,
485        raw_spec_path: &Path,
486    ) -> Result<(), GeneratorError>
487    where
488        T: FileSystem,
489    {
490        // `build_raw` is always called after `build`, so `maybe_plain_path` must be set at this point
491        let (json_content, _) = self.read_spec(scoped_fs).await?;
492        let json_bytes: Vec<u8> = json_content.as_bytes().into();
493        let chain_spec = GenericChainSpec::<()>::from_json_bytes(json_bytes).map_err(|e| {
494            GeneratorError::ChainSpecGeneration(format!(
495                "Error loading chain-spec from json_bytes, err: {e}"
496            ))
497        })?;
498
499        self.raw_path = Some(raw_spec_path.to_path_buf());
500        let contents = chain_spec.as_json(true).map_err(|e| {
501            GeneratorError::ChainSpecGeneration(format!(
502                "getting chain-spec as json should work, err: {e}"
503            ))
504        })?;
505        let contents = self
506            .ensure_para_fields_in_raw(&contents, relay_chain_id)
507            .await?;
508        self.write_spec(scoped_fs, contents).await?;
509
510        Ok(())
511    }
512
513    async fn build_raw_with_command<'a, T>(
514        &mut self,
515        ns: &DynNamespace,
516        scoped_fs: &ScopedFilesystem<'a, T>,
517        raw_spec_path: PathBuf,
518        relay_chain_id: Option<Chain>,
519    ) -> Result<(), GeneratorError>
520    where
521        T: FileSystem,
522    {
523        // fallback to use _cmd_ for raw creation
524        let temp_name = format!(
525            "temp-build-raw-{}-{}",
526            self.chain_spec_name,
527            rand::random::<u8>()
528        );
529
530        self.resolve_subcommand(ns).await?;
531
532        let cmd = self
533            .command
534            .as_ref()
535            .ok_or(GeneratorError::ChainSpecGeneration(
536                "Invalid command".into(),
537            ))?;
538        let maybe_plain_path =
539            self.maybe_plain_path
540                .as_ref()
541                .ok_or(GeneratorError::ChainSpecGeneration(
542                    "Invalid plain path".into(),
543                ))?;
544
545        // TODO: we should get the full path from the scoped filesystem
546        let chain_spec_path_local = format!(
547            "{}/{}",
548            ns.base_dir().to_string_lossy(),
549            maybe_plain_path.display()
550        );
551        // Remote path to be injected
552        let chain_spec_path_in_pod = format!("{}/{}", NODE_CONFIG_DIR, maybe_plain_path.display());
553        // Path in the context of the node, this can be different in the context of the providers (e.g native)
554        let chain_spec_path_in_args = if matches!(self.command, Some(CommandInContext::Local(_, _)))
555        {
556            chain_spec_path_local.clone()
557        } else if ns.capabilities().prefix_with_full_path {
558            // In native
559            format!(
560                "{}/{}{}",
561                ns.base_dir().to_string_lossy(),
562                temp_name,
563                chain_spec_path_in_pod
564            )
565        } else {
566            chain_spec_path_in_pod.clone()
567        };
568
569        let mut replacements = HashMap::from([("chainName", chain_spec_path_in_args.as_str())]);
570
571        if self.subcommand.as_deref() == Some("export-chain-spec") {
572            replacements.insert("subCommand", "export-chain-spec");
573            replacements.insert("disableBootnodes", "");
574        } else {
575            // use build-spec
576            replacements.insert("subCommand", "build-spec");
577            replacements.insert("disableBootnodes", "--disable-default-bootnode");
578        }
579
580        let mut full_cmd = apply_replacements(cmd.cmd(), &replacements);
581
582        if !full_cmd.contains("--raw") {
583            full_cmd = format!("{full_cmd} --raw");
584        }
585        trace!("full_cmd: {:?}", full_cmd);
586
587        let parts: Vec<&str> = full_cmd.split_whitespace().collect();
588        let Some((cmd, args)) = parts.split_first() else {
589            return Err(GeneratorError::ChainSpecGeneration(format!(
590                "Invalid generator command: {full_cmd}"
591            )));
592        };
593        trace!("cmd: {:?} - args: {:?}", cmd, args);
594
595        let generate_command = GenerateFileCommand::new(cmd, raw_spec_path.clone()).args(args);
596
597        if let Some(cmd) = &self.command {
598            match cmd {
599                CommandInContext::Local(_, expected_path) => {
600                    build_locally(generate_command, scoped_fs, expected_path.as_deref()).await?
601                },
602                CommandInContext::Remote(_, expected_path) => {
603                    let options = GenerateFilesOptions::with_files(
604                        vec![generate_command],
605                        self.image.clone(),
606                        &[TransferedFile::new(
607                            chain_spec_path_local,
608                            chain_spec_path_in_pod,
609                        )],
610                        expected_path.clone(),
611                    )
612                    .temp_name(temp_name);
613                    trace!("calling generate_files with options: {:#?}", options);
614                    ns.generate_files(options).await?;
615                },
616            }
617        }
618
619        self.raw_path = Some(raw_spec_path.clone());
620        let (content, _) = self.read_spec(scoped_fs).await?;
621        let content = self
622            .ensure_para_fields_in_raw(&content, relay_chain_id)
623            .await?;
624        self.write_spec(scoped_fs, content).await?;
625
626        Ok(())
627    }
628
629    async fn ensure_para_fields_in_raw(
630        &mut self,
631        content: &str,
632        relay_chain_id: Option<Chain>,
633    ) -> Result<String, GeneratorError> {
634        if let Context::Para {
635            relay_chain: _,
636            para_id,
637        } = &self.context
638        {
639            let mut chain_spec_json: serde_json::Value =
640                serde_json::from_str(content).map_err(|e| {
641                    GeneratorError::ChainSpecGeneration(format!(
642                        "getting chain-spec as json should work, err: {e}"
643                    ))
644                })?;
645
646            let mut needs_write = false;
647
648            if chain_spec_json["relay_chain"].is_null() {
649                chain_spec_json["relay_chain"] = json!(relay_chain_id);
650                needs_write = true;
651            }
652
653            if chain_spec_json["para_id"].is_null() {
654                chain_spec_json["para_id"] = json!(para_id);
655                needs_write = true;
656            }
657
658            if needs_write {
659                let contents = serde_json::to_string_pretty(&chain_spec_json).map_err(|e| {
660                    GeneratorError::ChainSpecGeneration(format!(
661                        "getting chain-spec json as pretty string should work, err: {e}"
662                    ))
663                })?;
664                return Ok(contents);
665            }
666        }
667
668        Ok(content.to_string())
669    }
670
671    fn should_retry_with_command(err: &GeneratorError) -> bool {
672        match err {
673            GeneratorError::ChainSpecGeneration(msg) => {
674                let msg_lower = msg.to_lowercase();
675                msg_lower.contains("genesisbuilder_get_preset") || msg_lower.contains("_get_preset")
676            },
677            _ => false,
678        }
679    }
680
681    /// Override the :code in chain-spec raw version
682    pub async fn override_code<'a, T>(
683        &mut self,
684        scoped_fs: &ScopedFilesystem<'a, T>,
685        wasm_override: &AssetLocation,
686    ) -> Result<(), GeneratorError>
687    where
688        T: FileSystem,
689    {
690        // first ensure we have the raw version of the chain-spec
691        let Some(_) = self.raw_path else {
692            return Err(GeneratorError::OverridingWasm(String::from(
693                "Raw path should be set at this point.",
694            )));
695        };
696        let (content, _) = self.read_spec(scoped_fs).await?;
697        // read override wasm
698        let override_content = wasm_override.get_asset().await.map_err(|_| {
699            GeneratorError::OverridingWasm(format!(
700                "Can not get asset to override wasm, asset: {wasm_override}"
701            ))
702        })?;
703
704        // read spec  to json value
705        let mut chain_spec_json: serde_json::Value =
706            serde_json::from_str(&content).map_err(|_| {
707                GeneratorError::ChainSpecGeneration("Can not parse chain-spec as json".into())
708            })?;
709
710        // override :code
711        let Some(code) = chain_spec_json.pointer_mut("/genesis/raw/top/0x3a636f6465") else {
712            return Err(GeneratorError::OverridingWasm(String::from(
713                "Pointer '/genesis/raw/top/0x3a636f6465' should be valid in the raw spec.",
714            )));
715        };
716
717        info!(
718            "🖋  Overriding ':code' (0x3a636f6465) in raw chain-spec with content of {}",
719            wasm_override
720        );
721        *code = json!(format!("0x{}", hex::encode(override_content)));
722
723        let overrided_content = serde_json::to_string_pretty(&chain_spec_json).map_err(|_| {
724            GeneratorError::ChainSpecGeneration("can not parse chain-spec value as json".into())
725        })?;
726        // save it
727        self.write_spec(scoped_fs, overrided_content).await?;
728
729        Ok(())
730    }
731
732    pub async fn read_raw_spec<'a, T>(
733        &self,
734        scoped_fs: &ScopedFilesystem<'a, T>,
735    ) -> Result<serde_json::Value, GeneratorError>
736    where
737        T: FileSystem,
738    {
739        // first ensure we have the raw version of the chain-spec
740        let Some(_) = self.raw_path else {
741            return Err(GeneratorError::OverridingRawSpec(String::from(
742                "Raw path should be set at this point.",
743            )));
744        };
745
746        let (content, _) = self.read_spec(scoped_fs).await?;
747
748        // read spec to json value
749        let chain_spec_json: serde_json::Value = serde_json::from_str(&content).map_err(|_| {
750            GeneratorError::ChainSpecGeneration("Can not parse chain-spec as json".into())
751        })?;
752
753        Ok(chain_spec_json)
754    }
755
756    pub async fn override_raw_spec<'a, T>(
757        &mut self,
758        scoped_fs: &ScopedFilesystem<'a, T>,
759        raw_spec_overrides: &JsonOverrides,
760    ) -> Result<(), GeneratorError>
761    where
762        T: FileSystem,
763    {
764        // read spec to json value
765        let mut chain_spec_json: serde_json::Value = self.read_raw_spec(scoped_fs).await?;
766
767        // read overrides to json value
768        let override_content: serde_json::Value = raw_spec_overrides.get().await.map_err(|_| {
769            GeneratorError::OverridingRawSpec(format!(
770                "Can not parse raw_spec_override contents as json: {raw_spec_overrides}"
771            ))
772        })?;
773
774        // merge overrides with existing spec
775        merge(&mut chain_spec_json, &override_content);
776
777        // save changes
778        let overrided_content = serde_json::to_string_pretty(&chain_spec_json).map_err(|_| {
779            GeneratorError::ChainSpecGeneration("can not parse chain-spec value as json".into())
780        })?;
781        self.write_spec(scoped_fs, overrided_content).await?;
782
783        Ok(())
784    }
785
786    // find from genesis.raw.top
787    pub async fn find_raw_key<'a, T>(
788        &self,
789        scoped_fs: &ScopedFilesystem<'a, T>,
790        key: &str,
791    ) -> Result<bool, GeneratorError>
792    where
793        T: FileSystem,
794    {
795        // first ensure we have the raw version of the chain-spec
796        let Some(_) = self.raw_path else {
797            return Err(GeneratorError::OverridingRawSpec(String::from(
798                "Raw path should be set at this point.",
799            )));
800        };
801
802        let (content, _) = self.read_spec(scoped_fs).await?;
803
804        // read spec to json value
805        let chain_spec_json: serde_json::Value = serde_json::from_str(&content).map_err(|_| {
806            GeneratorError::ChainSpecGeneration("Can not parse chain-spec as json".into())
807        })?;
808
809        let val = &chain_spec_json["genesis"]["raw"]["top"][key];
810
811        Ok(val != &serde_json::Value::Null)
812    }
813
814    pub fn raw_path(&self) -> Option<&Path> {
815        self.raw_path.as_deref()
816    }
817
818    pub fn set_asset_location(&mut self, location: AssetLocation) {
819        self.asset_location = Some(location)
820    }
821
822    pub async fn read_chain_id<'a, T>(
823        &self,
824        scoped_fs: &ScopedFilesystem<'a, T>,
825    ) -> Result<String, GeneratorError>
826    where
827        T: FileSystem,
828    {
829        let (content, _) = self.read_spec(scoped_fs).await?;
830        ChainSpec::chain_id_from_spec(&content)
831    }
832
833    async fn read_spec<'a, T>(
834        &self,
835        scoped_fs: &ScopedFilesystem<'a, T>,
836    ) -> Result<(String, ChainSpecFormat), GeneratorError>
837    where
838        T: FileSystem,
839    {
840        let (path, format) = match (self.maybe_plain_path.as_ref(), self.raw_path.as_ref()) {
841            (Some(path), None) => (path, ChainSpecFormat::Plain),
842            (None, Some(path)) => (path, ChainSpecFormat::Raw),
843            (Some(_), Some(path)) => {
844                // if we have both paths return the raw
845                (path, ChainSpecFormat::Raw)
846            },
847            (None, None) => unreachable!(),
848        };
849
850        let content = scoped_fs.read_to_string(path.clone()).await.map_err(|_| {
851            GeneratorError::ChainSpecGeneration(format!(
852                "Can not read chain-spec from {}",
853                path.to_string_lossy()
854            ))
855        })?;
856
857        Ok((content, format))
858    }
859
860    async fn write_spec<'a, T>(
861        &self,
862        scoped_fs: &ScopedFilesystem<'a, T>,
863        content: impl Into<String>,
864    ) -> Result<(), GeneratorError>
865    where
866        T: FileSystem,
867    {
868        let (path, _format) = match (self.maybe_plain_path.as_ref(), self.raw_path.as_ref()) {
869            (Some(path), None) => (path, ChainSpecFormat::Plain),
870            (None, Some(path)) => (path, ChainSpecFormat::Raw),
871            (Some(_), Some(path)) => {
872                // if we have both paths return the raw
873                (path, ChainSpecFormat::Raw)
874            },
875            (None, None) => unreachable!(),
876        };
877
878        scoped_fs.write(path, content.into()).await.map_err(|_| {
879            GeneratorError::ChainSpecGeneration(format!(
880                "Can not write chain-spec from {}",
881                path.to_string_lossy()
882            ))
883        })?;
884
885        Ok(())
886    }
887
888    // TODO: (javier) move this fns to state aware
889    pub async fn customize_para<'a, T>(
890        &self,
891        para: &ParachainSpec,
892        relay_chain_id: &str,
893        scoped_fs: &ScopedFilesystem<'a, T>,
894    ) -> Result<(), GeneratorError>
895    where
896        T: FileSystem,
897    {
898        let (content, format) = self.read_spec(scoped_fs).await?;
899        let mut chain_spec_json: serde_json::Value =
900            serde_json::from_str(&content).map_err(|_| {
901                GeneratorError::ChainSpecGeneration("Can not parse chain-spec as json".into())
902            })?;
903
904        if let Some(para_id) = chain_spec_json.get_mut("para_id") {
905            *para_id = json!(para.id);
906        };
907        if let Some(para_id) = chain_spec_json.get_mut("paraId") {
908            *para_id = json!(para.id);
909        };
910
911        if let Some(relay_chain_id_field) = chain_spec_json.get_mut("relay_chain") {
912            *relay_chain_id_field = json!(relay_chain_id);
913        };
914
915        if let ChainSpecFormat::Plain = format {
916            let pointer = get_runtime_config_pointer(&chain_spec_json)
917                .map_err(GeneratorError::ChainSpecGeneration)?;
918
919            edit_genesis(
920                &pointer,
921                &mut chain_spec_json,
922                para.genesis_overrides.as_ref(),
923                para.genesis_patch.as_ref(),
924            )?;
925
926            clear_authorities(&pointer, &mut chain_spec_json, &self.context);
927
928            let key_type_to_use = if para.is_evm_based {
929                SessionKeyType::Evm
930            } else {
931                SessionKeyType::Default
932            };
933
934            // Get validators to add as authorities
935            let validators: Vec<&NodeSpec> = para
936                .collators
937                .iter()
938                .filter(|node| node.is_validator)
939                .collect();
940
941            // check chain key types
942            if chain_spec_json
943                .pointer(&format!("{pointer}/session"))
944                .is_some()
945            {
946                add_authorities(&pointer, &mut chain_spec_json, &validators, key_type_to_use);
947            } else if chain_spec_json
948                .pointer(&format!("{pointer}/aura"))
949                .is_some()
950            {
951                add_aura_authorities(&pointer, &mut chain_spec_json, &validators, KeyType::Aura);
952            } else {
953                warn!("Can't customize keys, not `session` or `aura` find in the chain-spec file");
954            };
955
956            // Add nodes to collator
957            let invulnerables: Vec<&NodeSpec> = para
958                .collators
959                .iter()
960                .filter(|node| node.is_invulnerable)
961                .collect();
962
963            add_collator_selection(
964                &pointer,
965                &mut chain_spec_json,
966                &invulnerables,
967                key_type_to_use,
968            );
969
970            // override `parachainInfo/parachainId`
971            override_parachain_info(&pointer, &mut chain_spec_json, para.id);
972
973            // check if `assets` pallet config
974            let balances_to_add =
975                generate_balance_to_add_from_assets_pallet(&pointer, &chain_spec_json)?;
976            add_balances(&pointer, &mut chain_spec_json, balances_to_add)?;
977
978            // write spec
979            let content = serde_json::to_string_pretty(&chain_spec_json).map_err(|_| {
980                GeneratorError::ChainSpecGeneration("can not parse chain-spec value as json".into())
981            })?;
982            self.write_spec(scoped_fs, content).await?;
983        } else if para.genesis_patch.is_some() {
984            return Err(self.patch_needs_plain_spec());
985        } else {
986            warn!("⚠️ Chain spec for para_id: {} is in raw mode", para.id);
987        }
988        Ok(())
989    }
990
991    pub async fn customize_relay<'a, T, U>(
992        &self,
993        relaychain: &RelaychainSpec,
994        hrmp_channels: &[HrmpChannelConfig],
995        para_artifacts: Vec<ParaGenesisConfig<U>>,
996        scoped_fs: &ScopedFilesystem<'a, T>,
997    ) -> Result<(), GeneratorError>
998    where
999        T: FileSystem,
1000        U: AsRef<Path>,
1001    {
1002        let (content, format) = self.read_spec(scoped_fs).await?;
1003        let mut chain_spec_json: serde_json::Value =
1004            serde_json::from_str(&content).map_err(|_| {
1005                GeneratorError::ChainSpecGeneration("Can not parse chain-spec as json".into())
1006            })?;
1007
1008        if let ChainSpecFormat::Plain = format {
1009            // get the tokenDecimals property or set the default (12)
1010            let token_decimals =
1011                if let Some(val) = chain_spec_json.pointer("/properties/tokenDecimals") {
1012                    let val = val.as_u64().unwrap_or(12);
1013                    if val > u8::MAX as u64 {
1014                        12
1015                    } else {
1016                        val as u8
1017                    }
1018                } else {
1019                    12
1020                };
1021            // get the config pointer
1022            let pointer = get_runtime_config_pointer(&chain_spec_json)
1023                .map_err(GeneratorError::ChainSpecGeneration)?;
1024
1025            edit_genesis(
1026                &pointer,
1027                &mut chain_spec_json,
1028                relaychain.runtime_genesis_patch.as_ref(),
1029                relaychain.genesis_patch.as_ref(),
1030            )?;
1031
1032            // get min stake (to store if neede later)
1033            let staking_min = get_staking_min(&pointer, &mut chain_spec_json);
1034
1035            // Clear authorities
1036            clear_authorities(&pointer, &mut chain_spec_json, &self.context);
1037
1038            // add balances
1039            let mut balances_to_add =
1040                generate_balance_to_add_from_nodes(&relaychain.nodes, staking_min);
1041
1042            // ensure zombie account (//Zombie) have funds
1043            // we will use for internal usage (e.g new validators)
1044            balances_to_add.push((
1045                ZOMBIE_KEY.to_string(),
1046                1000 * 10_u128.pow(token_decimals as u32),
1047            ));
1048
1049            add_balances(&pointer, &mut chain_spec_json, balances_to_add)?;
1050
1051            // add staking
1052            add_staking(
1053                &pointer,
1054                &mut chain_spec_json,
1055                &relaychain.nodes,
1056                staking_min,
1057            );
1058
1059            // Get validators to add as authorities
1060            let validators: Vec<&NodeSpec> = relaychain
1061                .nodes
1062                .iter()
1063                .filter(|node| node.is_validator)
1064                .collect();
1065
1066            // check chain key types
1067            if chain_spec_json
1068                .pointer(&format!("{pointer}/session"))
1069                .is_some()
1070            {
1071                add_authorities(
1072                    &pointer,
1073                    &mut chain_spec_json,
1074                    &validators,
1075                    SessionKeyType::Stash,
1076                );
1077            } else {
1078                add_aura_authorities(&pointer, &mut chain_spec_json, &validators, KeyType::Aura);
1079                add_grandpa_authorities(&pointer, &mut chain_spec_json, &validators, KeyType::Aura);
1080            }
1081
1082            // staking && nominators
1083
1084            if !hrmp_channels.is_empty() {
1085                add_hrmp_channels(&pointer, &mut chain_spec_json, hrmp_channels);
1086            }
1087
1088            // paras
1089            for para_genesis_config in para_artifacts.iter() {
1090                add_parachain_to_genesis(
1091                    &pointer,
1092                    &mut chain_spec_json,
1093                    para_genesis_config,
1094                    scoped_fs,
1095                )
1096                .await
1097                .map_err(|e| GeneratorError::ChainSpecGeneration(e.to_string()))?;
1098            }
1099
1100            // TODO:
1101            // - staking
1102            // - nominators
1103
1104            // write spec
1105            let content = serde_json::to_string_pretty(&chain_spec_json).map_err(|_| {
1106                GeneratorError::ChainSpecGeneration("can not parse chain-spec value as json".into())
1107            })?;
1108            self.write_spec(scoped_fs, content).await?;
1109        } else if relaychain.genesis_patch.is_some() {
1110            return Err(self.patch_needs_plain_spec());
1111        } else {
1112            warn!(
1113                "⚠️ Chain Spec for chain {} is in raw mode, can't customize.",
1114                self.chain_spec_name
1115            );
1116        }
1117        Ok(())
1118    }
1119
1120    pub(crate) async fn apply_genesis_override<'a, T>(
1121        &self,
1122        scoped_fs: &ScopedFilesystem<'a, T>,
1123        overrides: &serde_json::Value,
1124    ) -> Result<(), GeneratorError>
1125    where
1126        T: FileSystem,
1127    {
1128        let (content, _) = self.read_spec(scoped_fs).await?;
1129        let mut chain_spec_json: serde_json::Value =
1130            serde_json::from_str(&content).map_err(|_| {
1131                GeneratorError::ChainSpecGeneration("Can not parse chain-spec as json".into())
1132            })?;
1133
1134        let pointer = get_runtime_config_pointer(&chain_spec_json)
1135            .map_err(GeneratorError::ChainSpecGeneration)?;
1136        edit_genesis(&pointer, &mut chain_spec_json, Some(overrides), None)?;
1137
1138        // write spec
1139        let content = serde_json::to_string_pretty(&chain_spec_json).map_err(|_| {
1140            GeneratorError::ChainSpecGeneration("can not parse chain-spec value as json".into())
1141        })?;
1142        self.write_spec(scoped_fs, content).await?;
1143
1144        Ok(())
1145    }
1146
1147    fn patch_needs_plain_spec(&self) -> GeneratorError {
1148        GeneratorError::ChainSpecGeneration(format!(
1149            "{}: genesis_patch needs a plain chain spec",
1150            self.chain_spec_name
1151        ))
1152    }
1153
1154    pub async fn add_bootnodes<'a, T>(
1155        &self,
1156        scoped_fs: &ScopedFilesystem<'a, T>,
1157        bootnodes: &[String],
1158    ) -> Result<(), GeneratorError>
1159    where
1160        T: FileSystem,
1161    {
1162        let (content, _) = self.read_spec(scoped_fs).await?;
1163        let mut chain_spec_json: serde_json::Value =
1164            serde_json::from_str(&content).map_err(|_| {
1165                GeneratorError::ChainSpecGeneration("Can not parse chain-spec as json".into())
1166            })?;
1167
1168        if let Some(bootnodes_on_file) = chain_spec_json.get_mut("bootNodes") {
1169            if let Some(bootnodes_on_file) = bootnodes_on_file.as_array_mut() {
1170                let mut bootnodes_to_add =
1171                    bootnodes.iter().map(|bootnode| json!(bootnode)).collect();
1172                bootnodes_on_file.append(&mut bootnodes_to_add);
1173            } else {
1174                return Err(GeneratorError::ChainSpecGeneration(
1175                    "id should be an string in the chain-spec, this is a bug".into(),
1176                ));
1177            };
1178        } else {
1179            return Err(GeneratorError::ChainSpecGeneration(
1180                "'bootNodes' should be a fields in the chain-spec of the relaychain".into(),
1181            ));
1182        };
1183
1184        // write spec
1185        let content = serde_json::to_string_pretty(&chain_spec_json).map_err(|_| {
1186            GeneratorError::ChainSpecGeneration("can not parse chain-spec value as json".into())
1187        })?;
1188        self.write_spec(scoped_fs, content).await?;
1189
1190        Ok(())
1191    }
1192
1193    /// Get the chain_is from the json content of a chain-spec file.
1194    pub fn chain_id_from_spec(spec_content: &str) -> Result<String, GeneratorError> {
1195        let chain_spec_json: serde_json::Value =
1196            serde_json::from_str(spec_content).map_err(|_| {
1197                GeneratorError::ChainSpecGeneration("Can not parse chain-spec as json".into())
1198            })?;
1199        if let Some(chain_id) = chain_spec_json.get("id") {
1200            if let Some(chain_id) = chain_id.as_str() {
1201                Ok(chain_id.to_string())
1202            } else {
1203                Err(GeneratorError::ChainSpecGeneration(
1204                    "id should be an string in the chain-spec, this is a bug".into(),
1205                ))
1206            }
1207        } else {
1208            Err(GeneratorError::ChainSpecGeneration(
1209                "'id' should be a fields in the chain-spec of the relaychain".into(),
1210            ))
1211        }
1212    }
1213
1214    /// Run a post-processing script on the chain-spec file.
1215    /// The script receives the path to the chain-spec as argument.
1216    pub async fn run_post_process_script<'a, T>(
1217        &self,
1218        script_command: &str,
1219        scoped_fs: &ScopedFilesystem<'a, T>,
1220    ) -> Result<(), GeneratorError>
1221    where
1222        T: FileSystem,
1223    {
1224        let spec_path =
1225            self.maybe_plain_path
1226                .as_ref()
1227                .ok_or(GeneratorError::ChainSpecGeneration(
1228                    "Chain-spec path not found for post-process script".into(),
1229                ))?;
1230        let full_path = scoped_fs.full_path(spec_path);
1231
1232        info!(
1233            "🔧 Running chain-spec post-process script: {} {}",
1234            script_command,
1235            full_path.display()
1236        );
1237
1238        // Read the current spec content and pass it to the script stdin.
1239        let spec_content = scoped_fs.read_to_string(spec_path).await.map_err(|e| {
1240            GeneratorError::ChainSpecGeneration(format!("Failed to read spec: {}", e))
1241        })?;
1242
1243        let mut child = Command::new(script_command)
1244            .stdin(Stdio::piped())
1245            .stdout(Stdio::piped())
1246            .stderr(Stdio::piped())
1247            .spawn()
1248            .map_err(|e| {
1249                GeneratorError::ChainSpecGeneration(format!(
1250                    "Failed to execute chain-spec post-process script: {}",
1251                    e
1252                ))
1253            })?;
1254
1255        if let Some(mut stdin) = child.stdin.take() {
1256            stdin
1257                .write_all(spec_content.as_bytes())
1258                .await
1259                .map_err(|e| {
1260                    GeneratorError::ChainSpecGeneration(format!(
1261                        "Failed to write to script stdin: {}",
1262                        e
1263                    ))
1264                })?;
1265        }
1266
1267        let output = child.wait_with_output().await.map_err(|e| {
1268            GeneratorError::ChainSpecGeneration(format!("Failed to wait for script output: {}", e))
1269        })?;
1270
1271        let stderr = String::from_utf8_lossy(&output.stderr);
1272        if !stderr.trim().is_empty() {
1273            info!("Script stderr: {}", stderr.trim());
1274        }
1275
1276        if !output.status.success() {
1277            return Err(GeneratorError::ChainSpecGeneration(format!(
1278                "Chain-spec post-process script failed with exit code {:?}: {}",
1279                output.status.code(),
1280                stderr
1281            )));
1282        }
1283
1284        let stdout = String::from_utf8_lossy(&output.stdout);
1285        let stdout_trimmed = stdout.trim();
1286        if !stdout_trimmed.is_empty() {
1287            // Validate JSON before overwriting the spec. If invalid, log and skip applying.
1288            if let Err(e) = serde_json::from_str::<serde_json::Value>(stdout_trimmed) {
1289                warn!(
1290                    "Script produced invalid JSON; output will NOT be applied: {}",
1291                    e
1292                );
1293                return Ok(());
1294            }
1295
1296            // Write to a temporary file inside the scoped fs, then copy into place via provider
1297            let tmp_path = PathBuf::from(format!("{}.postproc.tmp", spec_path.to_string_lossy()));
1298            scoped_fs
1299                .write(&tmp_path, stdout_trimmed.to_string())
1300                .await
1301                .map_err(|e| {
1302                    GeneratorError::ChainSpecGeneration(format!(
1303                        "Failed to write temp post-processed spec: {}",
1304                        e
1305                    ))
1306                })?;
1307
1308            let full_tmp = scoped_fs.full_path(&tmp_path);
1309            // Prepare transfer object: copy local tmp -> remote final path inside scoped fs
1310            let tf = TransferedFile::new(full_tmp, spec_path.to_path_buf());
1311            scoped_fs.copy_files(vec![&tf]).await.map_err(|e| {
1312                GeneratorError::ChainSpecGeneration(format!(
1313                    "Failed to copy temp spec into final path: {}",
1314                    e
1315                ))
1316            })?;
1317
1318            // Remove temporary file
1319            let _ = tokio_fs::remove_file(scoped_fs.full_path(&tmp_path)).await;
1320
1321            info!(
1322                "Script output applied to spec (bytes: {})",
1323                stdout_trimmed.len()
1324            );
1325        } else {
1326            info!("Script produced no output; spec left unchanged");
1327        }
1328
1329        Ok(())
1330    }
1331}
1332
1333type GenesisNodeKey = (String, String, HashMap<String, String>);
1334
1335pub async fn build_locally<'a, T>(
1336    generate_command: GenerateFileCommand,
1337    scoped_fs: &ScopedFilesystem<'a, T>,
1338    maybe_output: Option<&Path>,
1339) -> Result<(), GeneratorError>
1340where
1341    T: FileSystem,
1342{
1343    // generate_command.
1344
1345    let result = Command::new(generate_command.program.clone())
1346        .args(generate_command.args.clone())
1347        .output()
1348        .await
1349        .map_err(|err| {
1350            GeneratorError::ChainSpecGeneration(format!(
1351                "Error running cmd: {} args: {}, err: {}",
1352                generate_command.program,
1353                generate_command.args.join(" "),
1354                err
1355            ))
1356        })?;
1357
1358    if result.status.success() {
1359        let raw_output = if let Some(output_path) = maybe_output {
1360            tokio::fs::read(output_path).await.map_err(|err| {
1361                GeneratorError::ChainSpecGeneration(format!(
1362                    "Error reading output file at {}: {}",
1363                    output_path.display(),
1364                    err
1365                ))
1366            })?
1367        } else {
1368            result.stdout
1369        };
1370        scoped_fs
1371            .write(
1372                generate_command.local_output_path,
1373                String::from_utf8_lossy(&raw_output).to_string(),
1374            )
1375            .await?;
1376        Ok(())
1377    } else {
1378        Err(GeneratorError::ChainSpecGeneration(format!(
1379            "Error running cmd: {} args: {}, err: {}",
1380            generate_command.program,
1381            generate_command.args.join(" "),
1382            String::from_utf8_lossy(&result.stderr)
1383        )))
1384    }
1385}
1386
1387async fn is_raw<'a, T>(
1388    file: PathBuf,
1389    scoped_fs: &ScopedFilesystem<'a, T>,
1390) -> Result<bool, ProviderError>
1391where
1392    T: FileSystem,
1393{
1394    let content = scoped_fs.read_to_string(file).await?;
1395    let chain_spec_json: serde_json::Value = serde_json::from_str(&content).unwrap();
1396
1397    Ok(chain_spec_json.pointer("/genesis/raw/top").is_some())
1398}
1399
1400// Internal Chain-spec customizations
1401
1402async fn add_parachain_to_genesis<'a, T, U>(
1403    runtime_config_ptr: &str,
1404    chain_spec_json: &mut serde_json::Value,
1405    para_genesis_config: &ParaGenesisConfig<U>,
1406    scoped_fs: &ScopedFilesystem<'a, T>,
1407) -> Result<(), anyhow::Error>
1408where
1409    T: FileSystem,
1410    U: AsRef<Path>,
1411{
1412    if let Some(val) = chain_spec_json.pointer_mut(runtime_config_ptr) {
1413        let paras_pointer = if val.get("paras").is_some() {
1414            "/paras/paras"
1415        } else if val.get("parachainsParas").is_some() {
1416            // For retro-compatibility with substrate pre Polkadot 0.9.5
1417            "/parachainsParas/paras"
1418        } else {
1419            // The config may not contain paras. Since chainspec allows to contain the RuntimeGenesisConfig patch we can inject it.
1420            val["paras"] = json!({ "paras": [] });
1421            "/paras/paras"
1422        };
1423
1424        let paras = val
1425            .pointer_mut(paras_pointer)
1426            .ok_or(anyhow!("paras pointer should be valid {paras_pointer:?} "))?;
1427        let paras_vec = paras
1428            .as_array_mut()
1429            .ok_or(anyhow!("paras should be an array"))?;
1430
1431        let head = scoped_fs
1432            .read_to_string(para_genesis_config.state_path.as_ref())
1433            .await?;
1434        let wasm = scoped_fs
1435            .read_to_string(para_genesis_config.wasm_path.as_ref())
1436            .await?;
1437
1438        paras_vec.push(json!([
1439            para_genesis_config.id,
1440            [head.trim(), wasm.trim(), para_genesis_config.as_parachain]
1441        ]));
1442
1443        Ok(())
1444    } else {
1445        unreachable!("pointer to runtime config should be valid!")
1446    }
1447}
1448
1449fn get_runtime_config_pointer(chain_spec_json: &serde_json::Value) -> Result<String, String> {
1450    // runtime_genesis_config is no longer in ChainSpec after rococo runtime rework (refer to: https://github.com/paritytech/polkadot-sdk/pull/1256)
1451    // ChainSpec may contain a RuntimeGenesisConfigPatch
1452    let pointers = [
1453        "/genesis/runtimeGenesis/config",
1454        "/genesis/runtimeGenesis/patch",
1455        "/genesis/runtimeGenesisConfigPatch",
1456        "/genesis/runtime/runtime_genesis_config",
1457        "/genesis/runtime",
1458    ];
1459
1460    for pointer in pointers {
1461        if chain_spec_json.pointer(pointer).is_some() {
1462            return Ok(pointer.to_string());
1463        }
1464    }
1465
1466    Err("Can not find the runtime pointer".into())
1467}
1468
1469fn percolate_overrides<'a>(
1470    pointer: &str,
1471    overrides: &'a serde_json::Value,
1472) -> Result<&'a serde_json::Value, anyhow::Error> {
1473    let pointer_parts = pointer.split('/').collect::<Vec<&str>>();
1474    trace!("pointer_parts: {pointer_parts:?}");
1475
1476    let top_level = overrides
1477        .as_object()
1478        .ok_or_else(|| anyhow!("Overrides must be an object"))?;
1479    let top_level_key = top_level
1480        .keys()
1481        .next()
1482        .ok_or_else(|| anyhow!("Invalid override value: {overrides:?}"))?;
1483    trace!("top_level_key: {top_level_key}");
1484    let index = pointer_parts.iter().position(|x| *x == top_level_key);
1485    let Some(i) = index else {
1486        info!("Top level key '{top_level_key}' isn't part of the pointer ({pointer}), returning without percolating");
1487        return Ok(overrides);
1488    };
1489
1490    let p = if i == pointer_parts.len() - 1 {
1491        // top level key is at end of the pointer
1492        let p = format!("/{}", pointer_parts[i]);
1493        trace!("overrides pointer {p}");
1494        p
1495    } else {
1496        // example: pointer is `/genesis/runtimeGenesis/patch` and the overrides start at  `runtimeGenesis`
1497        let p = format!("/{}", pointer_parts[i..].join("/"));
1498        trace!("overrides pointer {p}");
1499        p
1500    };
1501    let overrides_to_use = overrides
1502        .pointer(&p)
1503        .ok_or_else(|| anyhow!("Invalid override value: {overrides:?}"))?;
1504    Ok(overrides_to_use)
1505}
1506
1507#[allow(dead_code)]
1508fn construct_runtime_pointer_from_overrides(
1509    overrides: &serde_json::Value,
1510) -> Result<String, anyhow::Error> {
1511    if overrides.get("genesis").is_some() {
1512        // overrides already start with /genesis
1513        return Ok("/genesis".into());
1514    } else {
1515        // check if we are one level inner
1516        if let Some(top_level) = overrides.as_object() {
1517            let k = top_level
1518                .keys()
1519                .next()
1520                .ok_or_else(|| anyhow!("Invalid override value: {overrides:?}"))?;
1521            match k.as_str() {
1522                "runtimeGenesisConfigPatch" | "runtime" | "runtimeGenesis" => {
1523                    return Ok(("/genesis").into())
1524                },
1525                "config" | "path" => {
1526                    return Ok(("/genesis/runtimeGenesis").into());
1527                },
1528                "runtime_genesis_config" => {
1529                    return Ok(("/genesis/runtime").into());
1530                },
1531                _ => {},
1532            }
1533        }
1534    }
1535
1536    Err(anyhow!("Can not find the runtime pointer"))
1537}
1538
1539/// Merge the `genesis` overrides into the runtime genesis section, then apply
1540/// the RFC 6902 patch to it. Patch paths are relative to the section, and
1541/// nothing is changed if an operation fails.
1542fn edit_genesis(
1543    pointer: &str,
1544    chain_spec_json: &mut serde_json::Value,
1545    overrides: Option<&serde_json::Value>,
1546    patch: Option<&json_patch::Patch>,
1547) -> Result<(), GeneratorError> {
1548    let percolated = overrides
1549        .map(|overrides| percolate_overrides(pointer, overrides))
1550        .transpose()
1551        .map_err(|e| GeneratorError::ChainSpecGeneration(e.to_string()))?;
1552    let Some(section) = chain_spec_json.pointer_mut(pointer) else {
1553        unreachable!("pointer to runtime config should be valid!")
1554    };
1555    if let Some(overrides) = percolated {
1556        merge(section, overrides);
1557    }
1558    if let Some(patch) = patch {
1559        json_patch::patch(section, &patch.0).map_err(|e| {
1560            GeneratorError::ChainSpecGeneration(format!(
1561                "genesis_patch (paths under {pointer}): {e}"
1562            ))
1563        })?;
1564    }
1565    Ok(())
1566}
1567
1568// Merge `patch_section` with `overrides`.
1569pub(crate) fn merge(patch_section: &mut serde_json::Value, overrides: &serde_json::Value) {
1570    trace!("patch: {:?}", patch_section);
1571    trace!("overrides: {:?}", overrides);
1572    if let (Some(genesis_obj), Some(overrides_obj)) =
1573        (patch_section.as_object_mut(), overrides.as_object())
1574    {
1575        for overrides_key in overrides_obj.keys() {
1576            trace!("overrides_key: {:?}", overrides_key);
1577            // we only want to override keys present in the genesis object
1578            if let Some(genesis_value) = genesis_obj.get_mut(overrides_key) {
1579                match (&genesis_value, overrides_obj.get(overrides_key)) {
1580                    // recurse if genesis value is an object
1581                    (serde_json::Value::Object(_), Some(overrides_value))
1582                        if overrides_value.is_object() =>
1583                    {
1584                        merge(genesis_value, overrides_value);
1585                    },
1586                    // override if genesis value not an object
1587                    (_, Some(overrides_value)) => {
1588                        trace!("overriding: {:?} / {:?}", genesis_value, overrides_value);
1589                        *genesis_value = overrides_value.clone();
1590                    },
1591                    _ => {
1592                        trace!("not match!");
1593                    },
1594                }
1595            } else {
1596                // Allow to add keys, see (https://github.com/paritytech/zombienet/issues/1614)
1597                info!("key: {overrides_key} not present in genesis_obj (adding key)");
1598                trace!(
1599                    "key: {overrides_key} not present in genesis_obj: {:?} (adding key)",
1600                    genesis_obj
1601                );
1602                let overrides_value = overrides_obj.get(overrides_key).expect(&format!(
1603                    "overrides_key {overrides_key} should be present in the overrides obj. qed"
1604                ));
1605                genesis_obj.insert(overrides_key.clone(), overrides_value.clone());
1606            }
1607        }
1608    }
1609}
1610
1611fn clear_authorities(
1612    runtime_config_ptr: &str,
1613    chain_spec_json: &mut serde_json::Value,
1614    ctx: &Context,
1615) {
1616    if let Some(val) = chain_spec_json.pointer_mut(runtime_config_ptr) {
1617        // clear keys (session, aura, grandpa)
1618        if val.get("session").is_some() {
1619            val["session"]["keys"] = json!([]);
1620        }
1621
1622        if val.get("aura").is_some() {
1623            val["aura"]["authorities"] = json!([]);
1624        }
1625
1626        if val.get("grandpa").is_some() {
1627            val["grandpa"]["authorities"] = json!([]);
1628        }
1629
1630        // clear collatorSelector
1631        if val.get("collatorSelection").is_some() {
1632            val["collatorSelection"]["invulnerables"] = json!([]);
1633        }
1634
1635        // clear staking but not `validatorCount` if `devStakers` is set
1636        if val.get("staking").is_some() && ctx == &Context::Relay {
1637            val["staking"]["invulnerables"] = json!([]);
1638            val["staking"]["stakers"] = json!([]);
1639
1640            if val["staking"]["devStakers"] == json!(null) {
1641                val["staking"]["validatorCount"] = json!(0);
1642            }
1643        }
1644    } else {
1645        unreachable!("pointer to runtime config should be valid!")
1646    }
1647}
1648
1649fn get_staking_min(runtime_config_ptr: &str, chain_spec_json: &mut serde_json::Value) -> u128 {
1650    // get min staking
1651    let staking_ptr = format!("{runtime_config_ptr}/staking/stakers");
1652    if let Some(stakers) = chain_spec_json.pointer(&staking_ptr) {
1653        // stakers should be an array
1654        let min = stakers[0][2].clone();
1655        min.as_u64().unwrap_or(0).into()
1656    } else {
1657        0
1658    }
1659}
1660
1661fn add_balances(
1662    runtime_config_ptr: &str,
1663    chain_spec_json: &mut serde_json::Value,
1664    balances_to_add: Vec<(String, u128)>,
1665) -> Result<(), GeneratorError> {
1666    let Some(val) = chain_spec_json.pointer_mut(runtime_config_ptr) else {
1667        unreachable!("pointer to runtime config should be valid!")
1668    };
1669    let Some(balances) = val.pointer("/balances/balances") else {
1670        warn!("NO 'balances' key in runtime config, skipping...");
1671        return Ok(());
1672    };
1673
1674    let mut balances_map = generate_balance_map(balances)?;
1675    for balance in balances_to_add {
1676        balances_map.insert(balance.0, balance.1);
1677    }
1678    let new_balances: Vec<(&String, &u128)> = balances_map.iter().collect();
1679    val["balances"]["balances"] = json!(new_balances);
1680    Ok(())
1681}
1682
1683/// Gets the address for a given key scheme from the node's accounts.
1684fn get_address_for_scheme(node: &NodeSpec, scheme: KeyScheme) -> String {
1685    let account_key = scheme.account_key();
1686    node.accounts
1687        .accounts
1688        .get(account_key)
1689        .expect(&format!(
1690            "'{}' account should be set at spec computation {THIS_IS_A_BUG}",
1691            account_key
1692        ))
1693        .address
1694        .clone()
1695}
1696
1697fn get_node_keys(
1698    node: &NodeSpec,
1699    session_key: SessionKeyType,
1700    asset_hub_polkadot: bool,
1701) -> GenesisNodeKey {
1702    let sr_account = node.accounts.accounts.get("sr").unwrap();
1703    let sr_stash = node.accounts.accounts.get("sr_stash").unwrap();
1704    let ed_account = node.accounts.accounts.get("ed").unwrap();
1705    let ec_account = node.accounts.accounts.get("ec").unwrap();
1706    let eth_account = node.accounts.accounts.get("eth").unwrap();
1707    let mut keys = HashMap::new();
1708    for k in [
1709        "babe",
1710        "im_online",
1711        "parachain_validator",
1712        "authority_discovery",
1713        "para_validator",
1714        "para_assignment",
1715        "aura",
1716        "nimbus",
1717        "vrf",
1718    ] {
1719        if k == "aura" && asset_hub_polkadot {
1720            keys.insert(k.to_string(), ed_account.address.clone());
1721            continue;
1722        }
1723        keys.insert(k.to_string(), sr_account.address.clone());
1724    }
1725
1726    keys.insert("grandpa".to_string(), ed_account.address.clone());
1727    keys.insert("beefy".to_string(), ec_account.address.clone());
1728    keys.insert("eth".to_string(), eth_account.public_key.clone());
1729
1730    let account_to_use = match session_key {
1731        SessionKeyType::Default => sr_account.address.clone(),
1732        SessionKeyType::Stash => sr_stash.address.clone(),
1733        SessionKeyType::Evm => format!("0x{}", eth_account.public_key),
1734    };
1735
1736    (account_to_use.clone(), account_to_use, keys)
1737}
1738
1739/// Generates session keys for a node with custom key types.
1740/// Returns (account, account, keys_map) tuple.
1741fn get_node_keys_with_custom_types(
1742    node: &NodeSpec,
1743    session_key: SessionKeyType,
1744    custom_key_types: &[ChainSpecKeyType],
1745) -> GenesisNodeKey {
1746    let sr_account = node.accounts.accounts.get("sr").unwrap();
1747    let sr_stash = node.accounts.accounts.get("sr_stash").unwrap();
1748    let eth_account = node.accounts.accounts.get("eth").unwrap();
1749
1750    // key_name -> address
1751    let mut keys = HashMap::new();
1752    for key_type in custom_key_types {
1753        let scheme = key_type.scheme;
1754        let account_key = scheme.account_key();
1755        let address = node
1756            .accounts
1757            .accounts
1758            .get(account_key)
1759            .expect(&format!(
1760                "'{}' account should be set at spec computation {THIS_IS_A_BUG}",
1761                account_key
1762            ))
1763            .address
1764            .clone();
1765        keys.insert(key_type.key_name.clone(), address);
1766    }
1767
1768    let account_to_use = match session_key {
1769        SessionKeyType::Default => sr_account.address.clone(),
1770        SessionKeyType::Stash => sr_stash.address.clone(),
1771        SessionKeyType::Evm => format!("0x{}", eth_account.public_key),
1772    };
1773
1774    (account_to_use.clone(), account_to_use, keys)
1775}
1776
1777fn add_authorities(
1778    runtime_config_ptr: &str,
1779    chain_spec_json: &mut serde_json::Value,
1780    nodes: &[&NodeSpec],
1781    session_key: SessionKeyType,
1782) {
1783    let asset_hub_polkadot = chain_spec_json
1784        .get("id")
1785        .and_then(|v| v.as_str())
1786        .map(|id| id.starts_with("asset-hub-polkadot"))
1787        .unwrap_or_default();
1788    if let Some(val) = chain_spec_json.pointer_mut(runtime_config_ptr) {
1789        if let Some(session_keys) = val.pointer_mut("/session/keys") {
1790            let keys: Vec<GenesisNodeKey> = nodes
1791                .iter()
1792                .map(|node| {
1793                    if let Some(custom_key_types) =
1794                        parse_chain_spec_key_types(&node.chain_spec_key_types, asset_hub_polkadot)
1795                    {
1796                        get_node_keys_with_custom_types(node, session_key, &custom_key_types)
1797                    } else {
1798                        get_node_keys(node, session_key, asset_hub_polkadot)
1799                    }
1800                })
1801                .collect();
1802            *session_keys = json!(keys);
1803        } else {
1804            warn!("⚠️  'session/keys' key not present in runtime config.");
1805        }
1806    } else {
1807        unreachable!("pointer to runtime config should be valid!")
1808    }
1809}
1810fn add_hrmp_channels(
1811    runtime_config_ptr: &str,
1812    chain_spec_json: &mut serde_json::Value,
1813    hrmp_channels: &[HrmpChannelConfig],
1814) {
1815    if let Some(val) = chain_spec_json.pointer_mut(runtime_config_ptr) {
1816        if let Some(preopen_hrmp_channels) = val.pointer_mut("/hrmp/preopenHrmpChannels") {
1817            let hrmp_channels = hrmp_channels
1818                .iter()
1819                .map(|c| {
1820                    (
1821                        c.sender(),
1822                        c.recipient(),
1823                        c.max_capacity(),
1824                        c.max_message_size(),
1825                    )
1826                })
1827                .collect::<Vec<_>>();
1828            *preopen_hrmp_channels = json!(hrmp_channels);
1829        } else {
1830            warn!("⚠️  'hrmp/preopenHrmpChannels' key not present in runtime config.");
1831        }
1832    } else {
1833        unreachable!("pointer to runtime config should be valid!")
1834    }
1835}
1836
1837fn add_aura_authorities(
1838    runtime_config_ptr: &str,
1839    chain_spec_json: &mut serde_json::Value,
1840    nodes: &[&NodeSpec],
1841    _key_type: KeyType,
1842) {
1843    if let Some(val) = chain_spec_json.pointer_mut(runtime_config_ptr) {
1844        if let Some(aura_authorities) = val.pointer_mut("/aura/authorities") {
1845            let keys: Vec<String> = nodes
1846                .iter()
1847                .map(|node| {
1848                    node.accounts
1849                        .accounts
1850                        .get("sr")
1851                        .expect(&format!(
1852                            "'sr' account should be set at spec computation {THIS_IS_A_BUG}"
1853                        ))
1854                        .address
1855                        .clone()
1856                })
1857                .collect();
1858            *aura_authorities = json!(keys);
1859        } else {
1860            warn!("⚠️  'aura/authorities' key not present in runtime config.");
1861        }
1862    } else {
1863        unreachable!("pointer to runtime config should be valid!")
1864    }
1865}
1866
1867fn add_grandpa_authorities(
1868    runtime_config_ptr: &str,
1869    chain_spec_json: &mut serde_json::Value,
1870    nodes: &[&NodeSpec],
1871    _key_type: KeyType,
1872) {
1873    if let Some(val) = chain_spec_json.pointer_mut(runtime_config_ptr) {
1874        if let Some(grandpa_authorities) = val.pointer_mut("/grandpa/authorities") {
1875            let keys: Vec<(String, usize)> = nodes
1876                .iter()
1877                .map(|node| {
1878                    (
1879                        node.accounts
1880                            .accounts
1881                            .get("ed")
1882                            .expect(&format!(
1883                                "'ed' account should be set at spec computation {THIS_IS_A_BUG}"
1884                            ))
1885                            .address
1886                            .clone(),
1887                        1,
1888                    )
1889                })
1890                .collect();
1891            *grandpa_authorities = json!(keys);
1892        } else {
1893            warn!("⚠️  'grandpa/authorities' key not present in runtime config.");
1894        }
1895    } else {
1896        unreachable!("pointer to runtime config should be valid!")
1897    }
1898}
1899
1900fn add_staking(
1901    runtime_config_ptr: &str,
1902    chain_spec_json: &mut serde_json::Value,
1903    nodes: &Vec<NodeSpec>,
1904    staking_min: u128,
1905) {
1906    if let Some(val) = chain_spec_json.pointer_mut(runtime_config_ptr) {
1907        let Some(_) = val.pointer("/staking") else {
1908            // should be a info log
1909            warn!("NO 'staking' key in runtime config, skipping...");
1910            return;
1911        };
1912
1913        let mut stakers = vec![];
1914        let mut invulnerables = vec![];
1915        for node in nodes {
1916            let sr_stash_addr = &node
1917                .accounts
1918                .accounts
1919                .get("sr_stash")
1920                .expect("'sr_stash account should be defined for the node. qed")
1921                .address;
1922            stakers.push(json!([
1923                sr_stash_addr,
1924                sr_stash_addr,
1925                staking_min,
1926                "Validator"
1927            ]));
1928
1929            if node.is_invulnerable {
1930                invulnerables.push(sr_stash_addr);
1931            }
1932        }
1933
1934        val["staking"]["validatorCount"] = json!(stakers.len());
1935        val["staking"]["stakers"] = json!(stakers);
1936        val["staking"]["invulnerables"] = json!(invulnerables);
1937    } else {
1938        unreachable!("pointer to runtime config should be valid!")
1939    }
1940}
1941
1942// TODO: (team)
1943// fn add_nominators() {}
1944
1945// // TODO: (team) we should think a better way to use the decorators from
1946// // current version (ts).
1947// fn para_custom() { todo!() }
1948fn override_parachain_info(
1949    runtime_config_ptr: &str,
1950    chain_spec_json: &mut serde_json::Value,
1951    para_id: u32,
1952) {
1953    if let Some(val) = chain_spec_json.pointer_mut(runtime_config_ptr) {
1954        if let Some(parachain_id) = val.pointer_mut("/parachainInfo/parachainId") {
1955            *parachain_id = json!(para_id)
1956        } else {
1957            // Add warning here!
1958        }
1959    } else {
1960        unreachable!("pointer to runtime config should be valid!")
1961    }
1962}
1963
1964fn generate_balance_to_add_from_assets_pallet(
1965    runtime_config_ptr: &str,
1966    chain_spec_json: &serde_json::Value,
1967) -> Result<Vec<(String, u128)>, GeneratorError> {
1968    let Some(val) = chain_spec_json.pointer(runtime_config_ptr) else {
1969        unreachable!("pointer to runtime config should be valid!")
1970    };
1971    // only accounts the balances list does not have yet
1972    let balances_map = match val.pointer("/balances/balances") {
1973        Some(balances) => generate_balance_map(balances)?,
1974        None => Default::default(),
1975    };
1976    let Some(assets_accounts) = val.pointer("/assets/accounts") else {
1977        return Ok(vec![]);
1978    };
1979    let accounts =
1980        Vec::<(serde_json::Value, String, u128)>::deserialize(assets_accounts).map_err(|e| {
1981            GeneratorError::ChainSpecGeneration(format!(
1982                "assets/accounts entries must be [asset_id, address, amount]: {e}"
1983            ))
1984        })?;
1985    Ok(accounts
1986        .into_iter()
1987        .filter(|(_, address, _)| !balances_map.contains_key(address))
1988        .map(|(_, address, amount)| (address, amount))
1989        .collect())
1990}
1991
1992fn add_collator_selection(
1993    runtime_config_ptr: &str,
1994    chain_spec_json: &mut serde_json::Value,
1995    nodes: &[&NodeSpec],
1996    session_key: SessionKeyType,
1997) {
1998    if let Some(val) = chain_spec_json.pointer_mut(runtime_config_ptr) {
1999        let key_type = if let SessionKeyType::Evm = session_key {
2000            "eth"
2001        } else {
2002            "sr"
2003        };
2004        let keys: Vec<String> = nodes
2005            .iter()
2006            .map(|node| {
2007                node.accounts
2008                    .accounts
2009                    .get(key_type)
2010                    .expect(&format!(
2011                        "'sr' account should be set at spec computation {THIS_IS_A_BUG}"
2012                    ))
2013                    .address
2014                    .clone()
2015            })
2016            .collect();
2017
2018        // collatorSelection.invulnerables
2019        if let Some(invulnerables) = val.pointer_mut("/collatorSelection/invulnerables") {
2020            *invulnerables = json!(keys);
2021        } else {
2022            // TODO: add a nice warning here.
2023            debug!("⚠️  'invulnerables' not present in spec, will not be customized");
2024        }
2025    } else {
2026        unreachable!("pointer to runtime config should be valid!")
2027    }
2028}
2029
2030// Helpers
2031fn generate_balance_map(
2032    balances: &serde_json::Value,
2033) -> Result<HashMap<String, u128>, GeneratorError> {
2034    let entries = Vec::<(String, u128)>::deserialize(balances).map_err(|e| {
2035        GeneratorError::ChainSpecGeneration(format!(
2036            "balances/balances entries must be [address, amount]: {e}"
2037        ))
2038    })?;
2039    Ok(entries.into_iter().collect())
2040}
2041
2042fn generate_balance_to_add_from_nodes(
2043    nodes: &[NodeSpec],
2044    staking_min: u128,
2045) -> Vec<(String, u128)> {
2046    // generate balances to add
2047    let mut balances_to_add = vec![];
2048
2049    for node in nodes {
2050        if node.initial_balance.eq(&0) {
2051            continue;
2052        };
2053
2054        // Double down the minimal stake defined
2055        let balance = std::cmp::max(node.initial_balance, staking_min * 2);
2056        for k in ["sr", "sr_stash"] {
2057            let account = node.accounts.accounts.get(k).unwrap();
2058            balances_to_add.push((account.address.clone(), balance));
2059        }
2060    }
2061    balances_to_add
2062}
2063
2064#[cfg(test)]
2065mod tests {
2066    use std::fs;
2067
2068    use configuration::HrmpChannelConfigBuilder;
2069
2070    use super::*;
2071    use crate::{generators, shared::types::NodeAccounts};
2072
2073    const ROCOCO_LOCAL_PLAIN_TESTING: &str = "./testing/rococo-local-plain.json";
2074    const ROCOCO_PENPAL_LOCAL_PLAIN_TESTING: &str = "./testing/rococo-penpal-local-plain.json";
2075
2076    fn chain_spec_test(file: &str) -> serde_json::Value {
2077        let content = fs::read_to_string(file).unwrap();
2078        serde_json::from_str(&content).unwrap()
2079    }
2080
2081    fn chain_spec_with_stake() -> serde_json::Value {
2082        json!({"genesis": {
2083            "runtimeGenesis" : {
2084                "patch": {
2085                    "staking": {
2086                        "forceEra": "NotForcing",
2087                        "invulnerables": [
2088                          "5GNJqTPyNqANBkUVMN1LPPrxXnFouWXoe2wNSmmEoLctxiZY",
2089                          "5HpG9w8EBLe5XCrbczpwq5TSXvedjrBGCwqxK1iQ7qUsSWFc"
2090                        ],
2091                        "minimumValidatorCount": 1,
2092                        "slashRewardFraction": 100000000,
2093                        "stakers": [
2094                          [
2095                            "5GNJqTPyNqANBkUVMN1LPPrxXnFouWXoe2wNSmmEoLctxiZY",
2096                            "5GNJqTPyNqANBkUVMN1LPPrxXnFouWXoe2wNSmmEoLctxiZY",
2097                            100000000000001_u128,
2098                            "Validator"
2099                          ],
2100                          [
2101                            "5HpG9w8EBLe5XCrbczpwq5TSXvedjrBGCwqxK1iQ7qUsSWFc",
2102                            "5HpG9w8EBLe5XCrbczpwq5TSXvedjrBGCwqxK1iQ7qUsSWFc",
2103                            100000000000000_u128,
2104                            "Validator"
2105                          ]
2106                        ],
2107                        "validatorCount": 2
2108                    },
2109                }
2110            }
2111        }})
2112    }
2113
2114    fn chain_spec_with_dev_stakers() -> serde_json::Value {
2115        json!({"genesis": {
2116            "runtimeGenesis" : {
2117                "patch": {
2118                    "staking": {
2119                        "activeEra": [
2120                            0,
2121                            0,
2122                            0
2123                        ],
2124                        "canceledPayout": 0,
2125                        "devStakers": [
2126                            2000,
2127                            25000
2128                        ],
2129                        "forceEra": "NotForcing",
2130                        "invulnerables": [],
2131                        "maxNominatorCount": null,
2132                        "maxValidatorCount": null,
2133                        "minNominatorBond": 0,
2134                        "minValidatorBond": 0,
2135                        "slashRewardFraction": 0,
2136                        "stakers": [],
2137                        "validatorCount": 500
2138                    },
2139                }
2140            }
2141        }})
2142    }
2143
2144    #[test]
2145    fn merge_with_empty_b_works() {
2146        let mut chain_spec_json = chain_spec_with_stake();
2147        merge(&mut chain_spec_json, &json!({}));
2148    }
2149
2150    #[test]
2151    fn genesis_patch_appends_and_replaces() {
2152        let mut chain_spec_json = chain_spec_with_stake();
2153        let pointer = get_runtime_config_pointer(&chain_spec_json).unwrap();
2154        let patch: json_patch::Patch = serde_json::from_value(json!([
2155            {"op": "add", "path": "/staking/invulnerables/-", "value": "5Extra"},
2156            {"op": "replace", "path": "/staking/minimumValidatorCount", "value": 7},
2157            {"op": "add", "path": "/balances", "value": {"balances": [["5Extra", 1]]}},
2158        ]))
2159        .unwrap();
2160
2161        edit_genesis(&pointer, &mut chain_spec_json, None, Some(&patch)).unwrap();
2162
2163        let section = chain_spec_json.pointer(&pointer).unwrap();
2164        assert_eq!(section["staking"]["invulnerables"][2], "5Extra");
2165        assert_eq!(section["staking"]["minimumValidatorCount"], 7);
2166        assert_eq!(section["balances"]["balances"][0][0], "5Extra");
2167    }
2168
2169    #[test]
2170    fn appended_balance_survives_overrides_and_add_balances() {
2171        let mut spec = chain_spec_test(ROCOCO_LOCAL_PLAIN_TESTING);
2172        let pointer = get_runtime_config_pointer(&spec).unwrap();
2173        let balances = format!("{pointer}/balances/balances");
2174
2175        // overrides replace the list, the patch appends to the result
2176        let overrides = json!({"balances": {"balances": [["5Base", 1]]}});
2177        let patch: json_patch::Patch = serde_json::from_value(json!([
2178            {"op": "add", "path": "/balances/balances/-", "value": ["5Extra", 5]},
2179        ]))
2180        .unwrap();
2181        edit_genesis(&pointer, &mut spec, Some(&overrides), Some(&patch)).unwrap();
2182        add_balances(&pointer, &mut spec, vec![("5Node".into(), 7)]).unwrap();
2183
2184        let map = generate_balance_map(spec.pointer(&balances).unwrap()).unwrap();
2185        assert_eq!(map.len(), 3);
2186        assert_eq!(map["5Base"], 1);
2187        assert_eq!(map["5Extra"], 5);
2188        assert_eq!(map["5Node"], 7);
2189    }
2190
2191    #[test]
2192    fn malformed_balance_is_an_error_not_a_panic() {
2193        let mut spec = chain_spec_test(ROCOCO_LOCAL_PLAIN_TESTING);
2194        let pointer = get_runtime_config_pointer(&spec).unwrap();
2195        let patch: json_patch::Patch = serde_json::from_value(json!([
2196            {"op": "add", "path": "/balances/balances/-", "value": ["5Extra", "5"]},
2197        ]))
2198        .unwrap();
2199        edit_genesis(&pointer, &mut spec, None, Some(&patch)).unwrap();
2200
2201        let err = add_balances(&pointer, &mut spec, vec![]).unwrap_err();
2202        assert!(err.to_string().contains("balances"), "{err}");
2203    }
2204
2205    #[test]
2206    fn genesis_patch_fails_whole_when_an_operation_fails() {
2207        let mut chain_spec_json = chain_spec_with_stake();
2208        let before = chain_spec_json.clone();
2209        let pointer = get_runtime_config_pointer(&chain_spec_json).unwrap();
2210        let patch: json_patch::Patch = serde_json::from_value(json!([
2211            {"op": "replace", "path": "/staking/minimumValidatorCount", "value": 7},
2212            {"op": "replace", "path": "/staking/missing", "value": 1},
2213        ]))
2214        .unwrap();
2215
2216        let err = edit_genesis(&pointer, &mut chain_spec_json, None, Some(&patch)).unwrap_err();
2217        assert!(err.to_string().contains("genesis_patch"), "{err}");
2218        assert_eq!(chain_spec_json, before);
2219    }
2220
2221    #[test]
2222    fn get_min_stake_works() {
2223        let mut chain_spec_json = chain_spec_with_stake();
2224
2225        let pointer = get_runtime_config_pointer(&chain_spec_json).unwrap();
2226        let min = get_staking_min(&pointer, &mut chain_spec_json);
2227
2228        assert_eq!(100000000000001, min);
2229    }
2230
2231    #[test]
2232    fn dev_stakers_not_override_count_works() {
2233        let mut chain_spec_json = chain_spec_with_dev_stakers();
2234
2235        let pointer = get_runtime_config_pointer(&chain_spec_json).unwrap();
2236        clear_authorities(&pointer, &mut chain_spec_json, &Context::Relay);
2237
2238        let validator_count = chain_spec_json
2239            .pointer(&format!("{pointer}/staking/validatorCount"))
2240            .unwrap();
2241        assert_eq!(validator_count, &json!(500));
2242    }
2243
2244    #[test]
2245    fn dev_stakers_override_count_works() {
2246        let mut chain_spec_json = chain_spec_with_stake();
2247
2248        let pointer = get_runtime_config_pointer(&chain_spec_json).unwrap();
2249        clear_authorities(&pointer, &mut chain_spec_json, &Context::Relay);
2250
2251        let validator_count = chain_spec_json
2252            .pointer(&format!("{pointer}/staking/validatorCount"))
2253            .unwrap();
2254        assert_eq!(validator_count, &json!(0));
2255    }
2256
2257    #[test]
2258    fn overrides_from_toml_works() {
2259        use serde::{Deserialize, Serialize};
2260
2261        #[derive(Debug, Serialize, Deserialize)]
2262        struct MockConfig {
2263            #[serde(rename = "genesis", skip_serializing_if = "Option::is_none")]
2264            genesis_overrides: Option<serde_json::Value>,
2265        }
2266
2267        let mut chain_spec_json = chain_spec_test(ROCOCO_LOCAL_PLAIN_TESTING);
2268        // Could also be  something like [genesis.runtimeGenesis.patch.balances]
2269        const TOML: &str = "[genesis.runtime.balances]
2270            devAccounts = [
2271            20000,
2272            1000000000000000000,
2273            \"//Sender//{}\"
2274        ]";
2275        let override_toml: MockConfig = toml::from_str(TOML).unwrap();
2276        let overrides = override_toml.genesis_overrides.unwrap();
2277        let pointer = get_runtime_config_pointer(&chain_spec_json).unwrap();
2278
2279        let percolated_overrides = percolate_overrides(&pointer, &overrides)
2280            .map_err(|e| GeneratorError::ChainSpecGeneration(e.to_string()))
2281            .unwrap();
2282        trace!("percolated_overrides: {:#?}", percolated_overrides);
2283        if let Some(genesis) = chain_spec_json.pointer_mut(&pointer) {
2284            merge(genesis, percolated_overrides);
2285        }
2286
2287        trace!("chain spec: {chain_spec_json:#?}");
2288        assert!(chain_spec_json
2289            .pointer("/genesis/runtime/balances/devAccounts")
2290            .is_some());
2291    }
2292
2293    #[test]
2294    fn add_balances_works() {
2295        let mut spec_plain = chain_spec_test(ROCOCO_LOCAL_PLAIN_TESTING);
2296        let mut name = String::from("luca");
2297        let initial_balance = 1_000_000_000_000_u128;
2298        let seed = format!("//{}{name}", name.remove(0).to_uppercase());
2299        let accounts = NodeAccounts {
2300            accounts: generators::generate_node_keys(&seed).unwrap(),
2301            seed,
2302        };
2303        let node = NodeSpec {
2304            name,
2305            accounts,
2306            initial_balance,
2307            ..Default::default()
2308        };
2309
2310        let nodes = vec![node];
2311        let balances_to_add = generate_balance_to_add_from_nodes(&nodes, 0);
2312        add_balances("/genesis/runtime", &mut spec_plain, balances_to_add).unwrap();
2313
2314        let new_balances = spec_plain
2315            .pointer("/genesis/runtime/balances/balances")
2316            .unwrap();
2317
2318        let balances_map = generate_balance_map(new_balances).unwrap();
2319
2320        // sr and sr_stash keys exists
2321        let sr = nodes[0].accounts.accounts.get("sr").unwrap();
2322        let sr_stash = nodes[0].accounts.accounts.get("sr_stash").unwrap();
2323        assert_eq!(balances_map.get(&sr.address).unwrap(), &initial_balance);
2324        assert_eq!(
2325            balances_map.get(&sr_stash.address).unwrap(),
2326            &initial_balance
2327        );
2328    }
2329
2330    #[test]
2331    fn add_balances_ensure_zombie_account() {
2332        let mut spec_plain = chain_spec_test(ROCOCO_LOCAL_PLAIN_TESTING);
2333
2334        let balances = spec_plain
2335            .pointer("/genesis/runtime/balances/balances")
2336            .unwrap();
2337        let balances_map = generate_balance_map(balances).unwrap();
2338
2339        let nodes: Vec<NodeSpec> = vec![];
2340        let mut balances_to_add = generate_balance_to_add_from_nodes(&nodes, 0);
2341        balances_to_add.push((ZOMBIE_KEY.to_string(), 1000 * 10_u128.pow(12)));
2342        add_balances("/genesis/runtime", &mut spec_plain, balances_to_add).unwrap();
2343
2344        let new_balances = spec_plain
2345            .pointer("/genesis/runtime/balances/balances")
2346            .unwrap();
2347
2348        let new_balances_map = generate_balance_map(new_balances).unwrap();
2349
2350        // sr and sr_stash keys exists
2351        assert!(new_balances_map.contains_key(ZOMBIE_KEY));
2352        assert_eq!(
2353            new_balances_map.len(),
2354            balances_map.len() + 1,
2355            "Number of balances should includes one more key (zombie key)."
2356        );
2357    }
2358
2359    #[test]
2360    fn add_balances_spec_without_balances() {
2361        let mut spec_plain = chain_spec_test(ROCOCO_LOCAL_PLAIN_TESTING);
2362
2363        {
2364            let balances = spec_plain.pointer_mut("/genesis/runtime/balances").unwrap();
2365            *balances = json!(serde_json::Value::Null);
2366        }
2367
2368        let mut name = String::from("luca");
2369        let initial_balance = 1_000_000_000_000_u128;
2370        let seed = format!("//{}{name}", name.remove(0).to_uppercase());
2371        let accounts = NodeAccounts {
2372            accounts: generators::generate_node_keys(&seed).unwrap(),
2373            seed,
2374        };
2375        let node = NodeSpec {
2376            name,
2377            accounts,
2378            initial_balance,
2379            ..Default::default()
2380        };
2381
2382        let nodes = vec![node];
2383        let balances_to_add = generate_balance_to_add_from_nodes(&nodes, 0);
2384        add_balances("/genesis/runtime", &mut spec_plain, balances_to_add).unwrap();
2385
2386        let new_balances = spec_plain.pointer("/genesis/runtime/balances/balances");
2387
2388        // assert 'balances' is not created
2389        assert_eq!(new_balances, None);
2390    }
2391
2392    #[test]
2393    fn add_staking_works() {
2394        let mut chain_spec_json = chain_spec_with_stake();
2395        let mut name = String::from("luca");
2396        let initial_balance = 1_000_000_000_000_u128;
2397        let seed = format!("//{}{name}", name.remove(0).to_uppercase());
2398        let accounts = NodeAccounts {
2399            accounts: generators::generate_node_keys(&seed).unwrap(),
2400            seed,
2401        };
2402        let node = NodeSpec {
2403            name,
2404            accounts,
2405            initial_balance,
2406            ..Default::default()
2407        };
2408
2409        let pointer = get_runtime_config_pointer(&chain_spec_json).unwrap();
2410        let min = get_staking_min(&pointer, &mut chain_spec_json);
2411
2412        let nodes = vec![node];
2413        add_staking(&pointer, &mut chain_spec_json, &nodes, min);
2414
2415        let new_staking = chain_spec_json
2416            .pointer("/genesis/runtimeGenesis/patch/staking")
2417            .unwrap();
2418
2419        // stakers should be one (with the luca sr_stash accounts)
2420        let sr_stash = nodes[0].accounts.accounts.get("sr_stash").unwrap();
2421        assert_eq!(new_staking["stakers"][0][0], json!(sr_stash.address));
2422        // with the calculated minimal bound
2423        assert_eq!(new_staking["stakers"][0][2], json!(min));
2424        // and only one
2425        assert_eq!(new_staking["stakers"].as_array().unwrap().len(), 1);
2426    }
2427
2428    #[test]
2429    fn adding_hrmp_channels_works() {
2430        let mut spec_plain = chain_spec_test(ROCOCO_LOCAL_PLAIN_TESTING);
2431
2432        {
2433            let current_hrmp_channels = spec_plain
2434                .pointer("/genesis/runtime/hrmp/preopenHrmpChannels")
2435                .unwrap();
2436            // assert should be empty
2437            assert_eq!(current_hrmp_channels, &json!([]));
2438        }
2439
2440        let para_100_101 = HrmpChannelConfigBuilder::new()
2441            .with_sender(100)
2442            .with_recipient(101)
2443            .build();
2444        let para_101_100 = HrmpChannelConfigBuilder::new()
2445            .with_sender(101)
2446            .with_recipient(100)
2447            .build();
2448        let channels = vec![para_100_101, para_101_100];
2449
2450        add_hrmp_channels("/genesis/runtime", &mut spec_plain, &channels);
2451        let new_hrmp_channels = spec_plain
2452            .pointer("/genesis/runtime/hrmp/preopenHrmpChannels")
2453            .unwrap()
2454            .as_array()
2455            .unwrap();
2456
2457        assert_eq!(new_hrmp_channels.len(), 2);
2458        assert_eq!(new_hrmp_channels.first().unwrap()[0], 100);
2459        assert_eq!(new_hrmp_channels.first().unwrap()[1], 101);
2460        assert_eq!(new_hrmp_channels.last().unwrap()[0], 101);
2461        assert_eq!(new_hrmp_channels.last().unwrap()[1], 100);
2462    }
2463
2464    #[test]
2465    fn adding_hrmp_channels_to_an_spec_without_channels() {
2466        let mut spec_plain = chain_spec_test("./testing/rococo-local-plain.json");
2467
2468        {
2469            let hrmp = spec_plain.pointer_mut("/genesis/runtime/hrmp").unwrap();
2470            *hrmp = json!(serde_json::Value::Null);
2471        }
2472
2473        let para_100_101 = HrmpChannelConfigBuilder::new()
2474            .with_sender(100)
2475            .with_recipient(101)
2476            .build();
2477        let para_101_100 = HrmpChannelConfigBuilder::new()
2478            .with_sender(101)
2479            .with_recipient(100)
2480            .build();
2481        let channels = vec![para_100_101, para_101_100];
2482
2483        add_hrmp_channels("/genesis/runtime", &mut spec_plain, &channels);
2484        let new_hrmp_channels = spec_plain.pointer("/genesis/runtime/hrmp/preopenHrmpChannels");
2485
2486        // assert 'preopenHrmpChannels' is not created
2487        assert_eq!(new_hrmp_channels, None);
2488    }
2489
2490    #[test]
2491    fn get_node_keys_works() {
2492        let mut name = String::from("luca");
2493        let seed = format!("//{}{name}", name.remove(0).to_uppercase());
2494        let accounts = NodeAccounts {
2495            accounts: generators::generate_node_keys(&seed).unwrap(),
2496            seed,
2497        };
2498        let node = NodeSpec {
2499            name,
2500            accounts,
2501            ..Default::default()
2502        };
2503
2504        let sr = &node.accounts.accounts["sr"];
2505        let keys = [
2506            ("babe".into(), sr.address.clone()),
2507            ("im_online".into(), sr.address.clone()),
2508            ("parachain_validator".into(), sr.address.clone()),
2509            ("authority_discovery".into(), sr.address.clone()),
2510            ("para_validator".into(), sr.address.clone()),
2511            ("para_assignment".into(), sr.address.clone()),
2512            ("aura".into(), sr.address.clone()),
2513            ("nimbus".into(), sr.address.clone()),
2514            ("vrf".into(), sr.address.clone()),
2515            (
2516                "grandpa".into(),
2517                node.accounts.accounts["ed"].address.clone(),
2518            ),
2519            ("beefy".into(), node.accounts.accounts["ec"].address.clone()),
2520            ("eth".into(), node.accounts.accounts["eth"].address.clone()),
2521        ]
2522        .into();
2523
2524        // Stash
2525        let sr_stash = &node.accounts.accounts["sr_stash"];
2526        let node_key = get_node_keys(&node, SessionKeyType::Stash, false);
2527        assert_eq!(node_key.0, sr_stash.address);
2528        assert_eq!(node_key.1, sr_stash.address);
2529        assert_eq!(node_key.2, keys);
2530        // Non-stash
2531        let node_key = get_node_keys(&node, SessionKeyType::Default, false);
2532        assert_eq!(node_key.0, sr.address);
2533        assert_eq!(node_key.1, sr.address);
2534        assert_eq!(node_key.2, keys);
2535    }
2536
2537    #[test]
2538    fn get_node_keys_supports_asset_hub_polkadot() {
2539        let mut name = String::from("luca");
2540        let seed = format!("//{}{name}", name.remove(0).to_uppercase());
2541        let accounts = NodeAccounts {
2542            accounts: generators::generate_node_keys(&seed).unwrap(),
2543            seed,
2544        };
2545        let node = NodeSpec {
2546            name,
2547            accounts,
2548            ..Default::default()
2549        };
2550
2551        let node_key = get_node_keys(&node, SessionKeyType::default(), false);
2552        assert_eq!(node_key.2["aura"], node.accounts.accounts["sr"].address);
2553
2554        let node_key = get_node_keys(&node, SessionKeyType::default(), true);
2555        assert_eq!(node_key.2["aura"], node.accounts.accounts["ed"].address);
2556    }
2557
2558    #[test]
2559    fn ensure_penpal_assets_works() {
2560        let mut spec_plain = chain_spec_test(ROCOCO_PENPAL_LOCAL_PLAIN_TESTING);
2561
2562        let balances = spec_plain
2563            .pointer("/genesis/runtimeGenesis/patch/balances/balances")
2564            .unwrap();
2565        let balances_map = generate_balance_map(balances).unwrap();
2566        println!("balance {balances_map:?}");
2567
2568        let nodes: Vec<NodeSpec> = vec![];
2569        let balances_to_add = generate_balance_to_add_from_nodes(&nodes, 0);
2570        add_balances(
2571            "/genesis/runtimeGenesis/patch",
2572            &mut spec_plain,
2573            balances_to_add,
2574        )
2575        .unwrap();
2576
2577        //
2578        let balances_to_add_from_assets = generate_balance_to_add_from_assets_pallet(
2579            "/genesis/runtimeGenesis/patch",
2580            &spec_plain,
2581        )
2582        .unwrap();
2583        println!("to add : {balances_to_add_from_assets:?}");
2584        add_balances(
2585            "/genesis/runtimeGenesis/patch",
2586            &mut spec_plain,
2587            balances_to_add_from_assets,
2588        )
2589        .unwrap();
2590
2591        let new_balances = spec_plain
2592            .pointer("/genesis/runtimeGenesis/patch/balances/balances")
2593            .unwrap();
2594
2595        let new_balances_map = generate_balance_map(new_balances).unwrap();
2596        println!("balance {new_balances_map:?}");
2597
2598        assert_eq!(new_balances_map.len(), balances_map.len() + 1);
2599    }
2600
2601    #[test]
2602    fn get_node_keys_with_custom_types_works() {
2603        use super::super::{chain_spec_key_types::ChainSpecKeyType, keystore_key_types::KeyScheme};
2604
2605        let mut name = String::from("alice");
2606        let seed = format!("//{}{name}", name.remove(0).to_uppercase());
2607        let accounts = NodeAccounts {
2608            accounts: generators::generate_node_keys(&seed).unwrap(),
2609            seed,
2610        };
2611        let node = NodeSpec {
2612            name,
2613            accounts,
2614            ..Default::default()
2615        };
2616
2617        let custom_key_types = vec![
2618            ChainSpecKeyType::new("aura", KeyScheme::Ed),
2619            ChainSpecKeyType::new("grandpa", KeyScheme::Sr),
2620        ];
2621
2622        let node_key =
2623            get_node_keys_with_custom_types(&node, SessionKeyType::Default, &custom_key_types);
2624
2625        // Account should be sr (default)
2626        assert_eq!(node_key.0, node.accounts.accounts["sr"].address);
2627        assert_eq!(node_key.1, node.accounts.accounts["sr"].address);
2628
2629        // Keys should use custom schemes
2630        assert_eq!(node_key.2["aura"], node.accounts.accounts["ed"].address);
2631        assert_eq!(node_key.2["grandpa"], node.accounts.accounts["sr"].address);
2632    }
2633
2634    #[test]
2635    fn get_node_keys_with_custom_types_stash_works() {
2636        use super::super::{chain_spec_key_types::ChainSpecKeyType, keystore_key_types::KeyScheme};
2637
2638        let mut name = String::from("alice");
2639        let seed = format!("//{}{name}", name.remove(0).to_uppercase());
2640        let accounts = NodeAccounts {
2641            accounts: generators::generate_node_keys(&seed).unwrap(),
2642            seed,
2643        };
2644        let node = NodeSpec {
2645            name,
2646            accounts,
2647            ..Default::default()
2648        };
2649
2650        let custom_key_types = vec![ChainSpecKeyType::new("aura", KeyScheme::Sr)];
2651
2652        let node_key =
2653            get_node_keys_with_custom_types(&node, SessionKeyType::Stash, &custom_key_types);
2654
2655        // Account should be sr_stash (stash derivation)
2656        assert_eq!(node_key.0, node.accounts.accounts["sr_stash"].address);
2657        assert_eq!(node_key.1, node.accounts.accounts["sr_stash"].address);
2658        assert_eq!(node_key.2["aura"], node.accounts.accounts["sr"].address);
2659    }
2660}