Skip to main content

zombienet_orchestrator/
lib.rs

1// TODO(Javier): Remove when we implement the logic in the orchestrator to spawn with the provider.
2#![allow(dead_code, clippy::expect_fun_call)]
3
4pub mod errors;
5pub mod generators;
6pub mod network;
7pub mod network_helper;
8pub mod observability;
9pub mod tx_helper;
10
11mod custom_processes;
12mod network_spec;
13pub mod shared;
14mod spawner;
15mod utils;
16
17use std::{
18    collections::{HashMap, HashSet, VecDeque},
19    env,
20    net::IpAddr,
21    path::{Path, PathBuf},
22    sync::Arc,
23    time::{Duration, SystemTime},
24};
25
26use anyhow::anyhow;
27use configuration::{types::JsonOverrides, NetworkConfig, RegistrationStrategy};
28use errors::OrchestratorError;
29use generators::{core_assignment, errors::GeneratorError, jam_config};
30use network::{
31    node::{NetworkNode, SpawnedNode},
32    parachain::Parachain,
33    relaychain::Relaychain,
34    Network,
35};
36// re-exported
37pub use network_spec::NetworkSpec;
38use network_spec::{jamchain::JamchainSpec, node::NodeSpec, parachain::ParachainSpec};
39use provider::{
40    shared::helpers::running_in_ci,
41    types::{GenerateFileCommand, NodeRole, ProviderCapabilities, TransferedFile},
42    DynNamespace, DynProvider,
43};
44use serde_json::json;
45use support::{
46    constants::{
47        GRAPH_CONTAINS_DEP, GRAPH_CONTAINS_NAME, INDEGREE_CONTAINS_NAME, QUEUE_NOT_EMPTY,
48        THIS_IS_A_BUG,
49    },
50    fs::{FileSystem, FileSystemError},
51    replacer::{get_tokens_to_replace, has_tokens},
52};
53use tokio::time::timeout;
54use tracing::{debug, info, trace, warn};
55
56use crate::{
57    network::{
58        jamchain::{Jamchain, RawJamchain},
59        node::{
60            jam::RawJamNetworkNode, process::RawCustomProcessNode, CustomProcessNode,
61            CustomProcessState, JamNetworkNode, RawNetworkNode,
62        },
63        parachain::RawParachain,
64        relaychain::RawRelaychain,
65    },
66    shared::types::RegisterParachainOptions,
67    spawner::SpawnNodeCtx,
68    utils::write_zombie_json,
69};
70pub struct Orchestrator<T>
71where
72    T: FileSystem + Sync + Send,
73{
74    filesystem: T,
75    provider: DynProvider,
76}
77
78impl<T> Orchestrator<T>
79where
80    T: FileSystem + Sync + Send + Clone,
81{
82    pub fn new(filesystem: T, provider: DynProvider) -> Self {
83        Self {
84            filesystem,
85            provider,
86        }
87    }
88
89    pub async fn spawn(
90        &self,
91        network_config: NetworkConfig,
92    ) -> Result<Network<T>, OrchestratorError> {
93        let global_timeout = network_config.global_settings().network_spawn_timeout();
94        let network_spec = NetworkSpec::from_config(&network_config).await?;
95
96        let res = timeout(
97            Duration::from_secs(global_timeout.into()),
98            self.spawn_inner(network_spec),
99        )
100        .await
101        .map_err(|_| OrchestratorError::GlobalTimeOut(global_timeout));
102        res?
103    }
104
105    pub async fn spawn_from_spec(
106        &self,
107        network_spec: NetworkSpec,
108    ) -> Result<Network<T>, OrchestratorError> {
109        let global_timeout = network_spec.global_settings.network_spawn_timeout();
110        let res = timeout(
111            Duration::from_secs(global_timeout as u64),
112            self.spawn_inner(network_spec),
113        )
114        .await
115        .map_err(|_| OrchestratorError::GlobalTimeOut(global_timeout));
116        res?
117    }
118
119    pub async fn attach_to_live(
120        &self,
121        zombie_json_path: &Path,
122    ) -> Result<Network<T>, OrchestratorError> {
123        info!("attaching to live network...");
124        info!("reading zombie.json from {:?}", zombie_json_path);
125
126        let zombie_json_content = self.filesystem.read_to_string(zombie_json_path).await?;
127        let zombie_json: serde_json::Value = serde_json::from_str(&zombie_json_content)?;
128
129        self.attach_to_live_from_json(&zombie_json).await
130    }
131
132    /// Like [`Self::attach_to_live`], but takes already-parsed `zombie.json` content.
133    /// Its `local_base_dir` is used as-is.
134    pub async fn attach_to_live_from_json(
135        &self,
136        zombie_json: &serde_json::Value,
137    ) -> Result<Network<T>, OrchestratorError> {
138        info!("recreating namespace...");
139        let ns: DynNamespace = self
140            .provider
141            .create_namespace_from_json(zombie_json)
142            .await?;
143
144        info!("recreating relaychain...");
145        let (relay, initial_spec) =
146            recreate_relaychain_from_json(zombie_json, ns.clone(), self.provider.name()).await?;
147        let relay_nodes = relay.nodes.clone();
148
149        let mut network =
150            Network::new_with_relay(relay, ns.clone(), self.filesystem.clone(), initial_spec);
151
152        for node in relay_nodes {
153            if node.is_responsive().await {
154                node.set_is_running(true);
155            }
156            network.insert_node(node);
157        }
158
159        info!("recreating parachains...");
160        let parachains_map =
161            recreate_parachains_from_json(zombie_json, ns.clone(), self.provider.name()).await?;
162        let para_nodes = parachains_map
163            .values()
164            .flat_map(|paras| paras.iter().flat_map(|para| para.collators.clone()))
165            .collect::<Vec<Arc<NetworkNode>>>();
166
167        network.set_parachains(parachains_map);
168        for node in para_nodes {
169            if node.is_responsive().await {
170                node.set_is_running(true);
171            }
172            network.insert_node(node);
173        }
174
175        if let Some(jamchain) =
176            recreate_jamchain_from_json(zombie_json, ns.clone(), self.provider.name()).await?
177        {
178            info!("recreating jamchain...");
179            let jam_nodes = jamchain.nodes.clone();
180            network.set_jamchain(jamchain);
181            for node in jam_nodes {
182                if node.is_responsive().await {
183                    node.core().set_is_running(true);
184                }
185                network.insert_node(node);
186            }
187        }
188
189        let processes =
190            recreate_custom_processes_from_json(zombie_json, ns.clone(), self.provider.name())
191                .await?;
192        if !processes.is_empty() {
193            info!("recreating custom processes...");
194            for process in &processes {
195                // On docker and k8s this reaches a forward or proxy that
196                // answers for a gone process too; a real check is #596.
197                if process.is_responsive().await {
198                    process.core().set_is_running(true);
199                }
200            }
201            network.set_custom_processes(processes);
202        }
203
204        Ok(network)
205    }
206
207    async fn spawn_inner(
208        &self,
209        mut network_spec: NetworkSpec,
210    ) -> Result<Network<T>, OrchestratorError> {
211        // main driver for spawn the network
212        debug!(network_spec = ?network_spec,"Network spec to spawn");
213
214        // TODO: move to Provider trait
215        validate_spec_with_provider_capabilities(&network_spec, self.provider.capabilities())
216            .map_err(|err| {
217                OrchestratorError::InvalidConfigForProvider(
218                    self.provider.name().into(),
219                    err.to_string(),
220                )
221            })?;
222
223        // create namespace
224        let ns = if let Some(base_dir) = network_spec.global_settings.base_dir() {
225            self.provider
226                .create_namespace_with_base_dir(base_dir)
227                .await?
228        } else {
229            self.provider.create_namespace().await?
230        };
231
232        // set the spawn_concurrency
233        let (spawn_concurrency, limited_by_tokens) = calculate_concurrency(&network_spec)?;
234
235        let start_time = SystemTime::now();
236        info!("🧰 ns: {}", ns.name());
237        info!("🧰 base_dir: {:?}", ns.base_dir());
238        info!("🕰 start time: {:?}", start_time);
239        info!("⚙️ spawn concurrency: {spawn_concurrency} (limited by tokens: {limited_by_tokens})");
240
241        network_spec
242            .populate_nodes_available_args(ns.clone())
243            .await?;
244
245        // A JAM chain takes the place of the relaychain and has its own, much simpler,
246        // spawn flow.
247        if network_spec.try_relaychain().is_none() {
248            let jam_spec =
249                network_spec
250                    .jamchain
251                    .clone()
252                    .ok_or(OrchestratorError::InvalidConfig(
253                        "A network needs either a relaychain or a jamchain.".to_string(),
254                    ))?;
255
256            return self
257                .spawn_jam(&jam_spec, &mut network_spec, ns, start_time)
258                .await;
259        }
260
261        // Resolve every node's `db_snapshot` AssetLocation into a local
262        // cache file once, serially, before any parallel spawn.
263        let all_nodes: Vec<&NodeSpec> = network_spec
264            .relaychain()
265            .nodes
266            .iter()
267            .chain(
268                network_spec
269                    .parachains_iter()
270                    .flat_map(|p| p.collators.iter()),
271            )
272            .collect();
273        let resolved_db_snapshots =
274            generators::resolve_db_snapshots(all_nodes, &ns, &self.filesystem).await?;
275
276        let base_dir = ns.base_dir().to_string_lossy();
277        let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
278
279        // The relaychain spec is moved out of the network spec while we set it up, so that
280        // it can be mutated independently of the parachains. It's put back before the nodes
281        // are spawned.
282        let mut relaychain = network_spec
283            .relaychain
284            .take()
285            .expect("checked to be `Some` above; qed");
286
287        // Create chain-spec for relaychain
288        relaychain.chain_spec.build(&ns, &scoped_fs).await?;
289
290        debug!("relaychain spec built!");
291        // Create parachain artifacts (chain-spec, wasm, state)
292        let relay_chain_id = relaychain.chain_spec.read_chain_id(&scoped_fs).await?;
293
294        let relay_chain_name = relaychain.chain.as_str().to_owned();
295        let base_dir_exists = network_spec.global_settings.base_dir().is_some();
296        network_spec
297            .build_parachain_artifacts(ns.clone(), &scoped_fs, &relay_chain_id, base_dir_exists)
298            .await?;
299
300        // Gather the parachains to register in genesis and the ones to register with extrinsic
301        let (para_to_register_in_genesis, para_to_register_with_extrinsic): (
302            Vec<&ParachainSpec>,
303            Vec<&ParachainSpec>,
304        ) = network_spec
305            .parachains
306            .iter()
307            .filter(|para| para.registration_strategy != RegistrationStrategy::Manual)
308            .partition(|para| {
309                matches!(para.registration_strategy, RegistrationStrategy::InGenesis)
310            });
311
312        let mut para_artifacts = vec![];
313        for para in &para_to_register_in_genesis {
314            let genesis_config = para.get_genesis_config()?;
315            para_artifacts.push(genesis_config)
316        }
317
318        // Customize relaychain
319        relaychain
320            .chain_spec
321            .customize_relay(
322                &relaychain,
323                &network_spec.hrmp_channels,
324                para_artifacts,
325                &scoped_fs,
326            )
327            .await?;
328
329        // Run post-process script if configured for the relaychain (run against plain spec before building raw)
330        if let Some(script_cmd) = relaychain.post_process_script.as_deref() {
331            relaychain
332                .chain_spec
333                .run_post_process_script(script_cmd, &scoped_fs)
334                .await?;
335        }
336
337        // Override cores if needed
338        let num_cores = network_spec.parachains_iter().fold(0u32, |mut acc, para| {
339            if let Some(cores) = para.num_cores {
340                acc += cores;
341            } else if &RegistrationStrategy::InGenesis == para.registration_strategy() {
342                // add 1 by default
343                acc += 1;
344            }
345
346            acc
347        });
348
349        if num_cores > para_to_register_in_genesis.len() as u32 {
350            let num_cores_to_set = num_cores - para_to_register_in_genesis.len() as u32;
351            // we should set the correct core config
352            let overrides = json!({
353                "configuration": {
354                    "config": {
355                        "scheduler_params": {
356                            "num_cores": num_cores_to_set,
357                            "max_validators_per_core": 1
358                        },
359                    }
360                }
361            });
362
363            relaychain
364                .chain_spec
365                .apply_genesis_override(&scoped_fs, &overrides)
366                .await?;
367        }
368
369        // Build raw version (after any post-processing of the plain spec)
370        relaychain
371            .chain_spec
372            .build_raw(&ns, &scoped_fs, None)
373            .await?;
374
375        // override wasm if needed
376        if let Some(ref wasm_override) = relaychain.wasm_override {
377            relaychain
378                .chain_spec
379                .override_code(&scoped_fs, wasm_override)
380                .await?;
381        }
382
383        // custom override raw spec if needed
384        if let Some(ref raw_spec_override) = relaychain.raw_spec_override {
385            relaychain
386                .chain_spec
387                .override_raw_spec(&scoped_fs, raw_spec_override)
388                .await?;
389        }
390
391        // assign extra cores if needed
392        if num_cores > para_to_register_in_genesis.len() as u32
393            || (relaychain.override_session_0 && !para_to_register_in_genesis.is_empty())
394        {
395            debug!("Raw overrides info: num_cores: {}, para_to_register_in_genesis_len: {:?}, override_session_0: {}", num_cores, para_to_register_in_genesis.len(), relaychain.override_session_0);
396            let mut core_index = 0u32;
397            // we should check with version the runtime is using
398            // could be ParaScheduler or CoretimeAssignmentProvider
399            let scheduler_key = core_assignment::get_parascheduler_storage_key();
400            let is_old = !relaychain
401                .chain_spec
402                .find_raw_key(&scoped_fs, &scheduler_key)
403                .await?;
404
405            let mut para_scheduler_value_parts: Vec<String> = vec![];
406            let mut raw_json_overrides = json!({});
407            // loop over para and assign cores from 0..
408            for para in &network_spec.parachains {
409                // cores we need to assign
410                let mut cores_for_para = 0_u32;
411                if let Some(cores) = para.num_cores {
412                    if &RegistrationStrategy::InGenesis == para.registration_strategy() {
413                        cores_for_para = cores;
414                    }
415                } else {
416                    // no num_cores set but we need to check if `override_session_0` is true
417                    // to assign the first core.
418                    if &RegistrationStrategy::InGenesis == para.registration_strategy()
419                        && relaychain.override_session_0
420                    {
421                        cores_for_para += 1;
422                    }
423                }
424
425                debug!(
426                    "Assigning {cores_for_para} cores in raw spec for para {}. Using pallet {}",
427                    para.id,
428                    if is_old {
429                        "CoretimeAssignmentProvider"
430                    } else {
431                        "ParaScheduler"
432                    }
433                );
434                for _core in 0..cores_for_para {
435                    if is_old {
436                        let (core_assign_key, core_assign_value) =
437                            core_assignment::generate_old(core_index, para.id);
438                        raw_json_overrides[core_assign_key] = json!(core_assign_value);
439                    } else {
440                        let part = core_assignment::generate(core_index, para.id);
441                        para_scheduler_value_parts.push(part);
442                    }
443                    core_index += 1;
444                }
445            }
446
447            // if not old we need to store the k/v to override
448            if !is_old {
449                let count_prefix = format!("{:02x}", para_scheduler_value_parts.len() * 4);
450                let core_assign_value =
451                    format!("{count_prefix}{}", para_scheduler_value_parts.join(""));
452                raw_json_overrides[scheduler_key] = json!(core_assign_value);
453            }
454
455            // extra check to ensure we need to override session 0
456            if relaychain.override_session_0 {
457                trace!("Overriding pallet ParaSessionInfo.session (0) to allow paras to produce blocks at first session.");
458                let raw_spec = relaychain.chain_spec.read_raw_spec(&scoped_fs).await?;
459                let overrides = generators::generate_session_0_overrides(&raw_spec, num_cores)?;
460
461                for (k, v) in overrides.as_object().ok_or(anyhow!(
462                    "'generate_session_0_overrides' should be a valid json Object."
463                ))? {
464                    raw_json_overrides[k] = v.clone();
465                }
466            }
467
468            debug!("Raw overrides keys: {:?}", raw_json_overrides);
469
470            relaychain
471                .chain_spec
472                .override_raw_spec(
473                    &scoped_fs,
474                    &JsonOverrides::Json(json!({
475                        "genesis": {
476                            "raw": {
477                                "top": raw_json_overrides
478                            }
479                        }
480                    })),
481                )
482                .await?;
483        }
484
485        // The relaychain is fully set up now, put it back into the network spec.
486        network_spec.relaychain = Some(relaychain);
487
488        let (bootnodes, relaynodes) =
489            split_nodes_by_bootnodes(&network_spec.relaychain().nodes, false);
490
491        // TODO: we want to still supporting spawn a dedicated bootnode??
492        let mut ctx = SpawnNodeCtx {
493            chain_id: &relay_chain_id,
494            parachain_id: None,
495            chain: relay_chain_name.as_str(),
496            role: ZombieRole::Node,
497            ns: &ns,
498            scoped_fs: &scoped_fs,
499            parachain: None,
500            bootnodes_addr: &vec![],
501            wait_ready: false,
502            nodes_by_name: json!({}),
503            global_settings: &network_spec.global_settings,
504            resolved_db_snapshots: &resolved_db_snapshots,
505        };
506
507        let global_files_to_inject = vec![TransferedFile::new(
508            PathBuf::from(format!(
509                "{}/{relay_chain_name}.json",
510                ns.base_dir().to_string_lossy()
511            )),
512            PathBuf::from(format!("/cfg/{relay_chain_name}.json")),
513        )];
514
515        let r = Relaychain::new(
516            relay_chain_name.to_string(),
517            relay_chain_id.clone(),
518            PathBuf::from(network_spec.relaychain().chain_spec.raw_path().ok_or(
519                OrchestratorError::InvariantError("chain-spec raw path should be set now"),
520            )?),
521        );
522        let mut network =
523            Network::new_with_relay(r, ns.clone(), self.filesystem.clone(), network_spec.clone());
524
525        // Initiate the node_ws_url which will be later used in the Parachain_with_extrinsic config
526        let mut node_ws_url: String = "".to_string();
527
528        // Calculate the bootnodes addr from the running nodes
529        let mut bootnodes_addr: Vec<String> = vec![];
530
531        for level in dependency_levels_among(&bootnodes)? {
532            let mut running_nodes_per_level = vec![];
533            for chunk in level.chunks(spawn_concurrency) {
534                let spawning_tasks = chunk
535                    .iter()
536                    .map(|node| spawner::spawn_node(node, global_files_to_inject.clone(), &ctx));
537
538                for node in futures::future::try_join_all(spawning_tasks).await? {
539                    let bootnode_multiaddr = node.multiaddr();
540
541                    bootnodes_addr.push(bootnode_multiaddr.to_string());
542
543                    // Is used in the register_para_options (We need to get this from the relay and not the collators)
544                    if node_ws_url.is_empty() {
545                        node_ws_url.clone_from(&node.ws_uri)
546                    }
547
548                    running_nodes_per_level.push(node);
549                }
550            }
551            info!(
552                "🕰  waiting for level: {:?} to be up...",
553                level.iter().map(|n| n.name.clone()).collect::<Vec<_>>()
554            );
555
556            // Wait for all nodes in the current level to be up
557            let waiting_tasks = running_nodes_per_level.iter().map(|node| {
558                node.wait_until_is_up(network_spec.global_settings.node_spawn_timeout())
559            });
560
561            let _ = futures::future::try_join_all(waiting_tasks).await?;
562
563            for node in running_nodes_per_level {
564                // Add the node to the  context and `Network` instance
565                ctx.nodes_by_name[node.name().to_owned()] = serde_json::to_value(&node)?;
566                network.add_running_node(node, None).await;
567            }
568        }
569
570        // Add the bootnodes to the relaychain spec file and ctx
571        network_spec
572            .relaychain()
573            .chain_spec
574            .add_bootnodes(&scoped_fs, &bootnodes_addr)
575            .await?;
576
577        ctx.bootnodes_addr = &bootnodes_addr;
578
579        // Upload the raw relaychain spec (with bootnodes) to the provider's file server, if any
580        upload_raw_spec(&ns, &network_spec.relaychain().chain_spec).await?;
581
582        for level in dependency_levels_among(&relaynodes)? {
583            let mut running_nodes_per_level = vec![];
584            for chunk in level.chunks(spawn_concurrency) {
585                let spawning_tasks = chunk
586                    .iter()
587                    .map(|node| spawner::spawn_node(node, global_files_to_inject.clone(), &ctx));
588
589                for node in futures::future::try_join_all(spawning_tasks).await? {
590                    running_nodes_per_level.push(node);
591                }
592            }
593            info!(
594                "🕰 waiting for level: {:?} to be up...",
595                level.iter().map(|n| n.name.clone()).collect::<Vec<_>>()
596            );
597
598            // Wait for all nodes in the current level to be up
599            let waiting_tasks = running_nodes_per_level.iter().map(|node| {
600                node.wait_until_is_up(network_spec.global_settings.network_spawn_timeout())
601            });
602
603            let _ = futures::future::try_join_all(waiting_tasks).await?;
604
605            for node in running_nodes_per_level {
606                ctx.nodes_by_name[node.name().to_owned()] = serde_json::to_value(&node)?;
607                network.add_running_node(node, None).await;
608            }
609        }
610
611        // spawn paras
612        self.spawn_parachains(
613            &network_spec.parachains,
614            &ctx,
615            &global_files_to_inject,
616            spawn_concurrency,
617            &mut network,
618        )
619        .await?;
620
621        // Now we need to register the paras with extrinsic from the Vec collected before;
622        for para in para_to_register_with_extrinsic {
623            let register_para_options: RegisterParachainOptions = RegisterParachainOptions {
624                id: para.id,
625                // This needs to resolve correctly
626                wasm_path: para
627                    .genesis_wasm
628                    .artifact_path()
629                    .ok_or(OrchestratorError::InvariantError(
630                        "artifact path for wasm must be set at this point",
631                    ))?
632                    .to_path_buf(),
633                state_path: para
634                    .genesis_state
635                    .artifact_path()
636                    .ok_or(OrchestratorError::InvariantError(
637                        "artifact path for state must be set at this point",
638                    ))?
639                    .to_path_buf(),
640                node_ws_url: node_ws_url.clone(),
641                onboard_as_para: para.onboard_as_parachain,
642                seed: None, // TODO: Seed is passed by?
643                finalization: false,
644            };
645
646            Parachain::register(register_para_options, &scoped_fs).await?;
647        }
648
649        if network_spec.global_settings.observability().enabled() {
650            match network
651                .start_observability(network_spec.global_settings.observability())
652                .await
653            {
654                Ok(obs) => {
655                    info!("📊 Prometheus URL: {}", obs.prometheus_url);
656                    info!("📊 Grafana URL: {}", obs.grafana_url);
657                },
658                Err(e) => {
659                    warn!("⚠️  Failed to spawn observability stack: {e}");
660                },
661            }
662        }
663
664        custom_processes::spawn_custom_processes(
665            &mut network,
666            &network_spec,
667            ns.clone(),
668            configured_concurrency(&network_spec),
669        )
670        .await?;
671
672        network.set_start_time_ts(start_time);
673
674        write_zombie_json(serde_json::to_value(&network)?, scoped_fs, ns.name()).await?;
675
676        if network_spec.global_settings.tear_down_on_failure() {
677            network.spawn_watching_task();
678        }
679
680        generators::cleanup_db_snapshot_cache(&resolved_db_snapshots).await;
681
682        Ok(network)
683    }
684
685    /// Spawn the collators of every parachain, in the context of an already running network.
686    ///
687    /// `ctx` is the context of the chain the parachains are anchored to (relaychain or JAM
688    /// chain); the per-parachain context is derived from it.
689    async fn spawn_parachains<'a>(
690        &self,
691        parachains: &[ParachainSpec],
692        ctx: &SpawnNodeCtx<'a, T>,
693        files_to_inject: &[TransferedFile],
694        spawn_concurrency: usize,
695        network: &mut Network<T>,
696    ) -> Result<(), OrchestratorError> {
697        let scoped_fs = ctx.scoped_fs;
698
699        for para in parachains {
700            // Create parachain (in the context of the running network)
701            let parachain = Parachain::from_spec(para, files_to_inject, scoped_fs).await?;
702            let parachain_id = parachain.chain_id.clone();
703
704            let (bootnodes, collators) =
705                split_nodes_by_bootnodes(&para.collators, para.no_default_bootnodes);
706
707            // Create `ctx` for spawn parachain nodes
708            let mut ctx_para = SpawnNodeCtx {
709                parachain: Some(para),
710                parachain_id: parachain_id.as_deref(),
711                role: if para.is_cumulus_based {
712                    ZombieRole::CumulusCollator
713                } else {
714                    ZombieRole::Collator
715                },
716                bootnodes_addr: &vec![],
717                ..ctx.clone()
718            };
719
720            // Calculate the bootnodes addr from the running nodes
721            let mut bootnodes_addr: Vec<String> = vec![];
722            let mut running_nodes: Vec<NetworkNode> = vec![];
723
724            for level in dependency_levels_among(&bootnodes)? {
725                for node in self
726                    .spawn_parachain_level(&level, &parachain, &ctx_para, spawn_concurrency)
727                    .await?
728                {
729                    bootnodes_addr.push(node.multiaddr().to_string());
730                    ctx_para.nodes_by_name[node.name().to_owned()] = serde_json::to_value(&node)?;
731                    running_nodes.push(node);
732                }
733            }
734
735            if let Some(para_chain_spec) = para.chain_spec.as_ref() {
736                para_chain_spec
737                    .add_bootnodes(scoped_fs, &bootnodes_addr)
738                    .await?;
739
740                // Upload the raw parachain spec (with bootnodes) to the provider's file server, if any
741                upload_raw_spec(ctx.ns, para_chain_spec).await?;
742            }
743
744            ctx_para.bootnodes_addr = &bootnodes_addr;
745
746            // Spawn the rest of the nodes
747            for level in dependency_levels_among(&collators)? {
748                for node in self
749                    .spawn_parachain_level(&level, &parachain, &ctx_para, spawn_concurrency)
750                    .await?
751                {
752                    ctx_para.nodes_by_name[node.name().to_owned()] = serde_json::to_value(&node)?;
753                    running_nodes.push(node);
754                }
755            }
756
757            let running_para_id = parachain.para_id;
758            network.add_para(parachain);
759            for node in running_nodes {
760                network.add_running_node(node, Some(running_para_id)).await;
761            }
762        }
763
764        Ok(())
765    }
766
767    /// Spawn one dependency level of collators, concurrently, and wait until they are up.
768    async fn spawn_parachain_level<'a>(
769        &self,
770        level: &[&NodeSpec],
771        parachain: &Parachain,
772        ctx: &SpawnNodeCtx<'a, T>,
773        spawn_concurrency: usize,
774    ) -> Result<Vec<NetworkNode>, OrchestratorError> {
775        let mut running_nodes = vec![];
776        for chunk in level.chunks(spawn_concurrency) {
777            let spawning_tasks = chunk
778                .iter()
779                .map(|node| spawner::spawn_node(node, parachain.files_to_inject.clone(), ctx));
780
781            running_nodes.extend(futures::future::try_join_all(spawning_tasks).await?);
782        }
783
784        info!(
785            "🕰  waiting for level: {:?} to be up...",
786            level.iter().map(|n| n.name.clone()).collect::<Vec<_>>()
787        );
788
789        // Wait for all nodes in the current level to be up
790        let waiting_tasks = running_nodes
791            .iter()
792            .map(|node| node.wait_until_is_up(ctx.global_settings.network_spawn_timeout()));
793
794        let _ = futures::future::try_join_all(waiting_tasks).await?;
795
796        Ok(running_nodes)
797    }
798
799    /// Spawn a network whose root chain is a JAM chain instead of a relaychain.
800    async fn spawn_jam(
801        &self,
802        jam_spec: &JamchainSpec,
803        network_spec: &mut NetworkSpec,
804        ns: DynNamespace,
805        start_time: SystemTime,
806    ) -> Result<Network<T>, OrchestratorError> {
807        let base_dir = ns.base_dir().to_string_lossy().to_string();
808        let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
809
810        // generate config
811        let jam_config = jam_config::generate(jam_spec)?;
812        // store the config file
813        scoped_fs
814            .write(
815                "jam_config.json",
816                serde_json::to_string_pretty(&jam_config)?,
817            )
818            .await?;
819        // generate spec
820        let cmd_parts: Vec<&str> = jam_spec.chain_spec_command.split(" ").collect();
821        let cmd = cmd_parts
822            .first()
823            .expect("jam chain-spec generator cmd should be valid");
824        let jam_config_full_path = format!("{}/{}", base_dir, "jam_config.json");
825        let jam_spec_full_path = format!("{}/{}", base_dir, "jam_spec.json");
826        let args = vec![
827            "gen-spec",
828            jam_config_full_path.as_str(),
829            jam_spec_full_path.as_str(),
830        ];
831        let generate_command = GenerateFileCommand::new(cmd, jam_spec_full_path.clone()).args(args);
832        generators::chain_spec::build_locally(
833            generate_command,
834            &scoped_fs,
835            Some(&PathBuf::from(&jam_spec_full_path)),
836        )
837        .await?;
838
839        // Create parachain artifacts (chain-spec, wasm, state). The parachains are anchored
840        // to the JAM chain, so its id is the one written into their chain-spec.
841        let base_dir_exists = network_spec.global_settings.base_dir().is_some();
842        network_spec
843            .build_parachain_artifacts(
844                ns.clone(),
845                &scoped_fs,
846                jam_spec.id.as_str(),
847                base_dir_exists,
848            )
849            .await?;
850
851        // Resolve every collator's `db_snapshot` AssetLocation into a local cache file once,
852        // serially, before any parallel spawn.
853        let collators: Vec<&NodeSpec> = network_spec
854            .parachains_iter()
855            .flat_map(|para| para.collators.iter())
856            .collect();
857        let resolved_db_snapshots =
858            generators::resolve_db_snapshots(collators, &ns, &self.filesystem).await?;
859
860        let (spawn_concurrency, _) = calculate_concurrency(network_spec)?;
861        let global_settings = &network_spec.global_settings;
862
863        // context setup
864        let jam_ctx = SpawnNodeCtx {
865            chain_id: jam_spec.id.as_str(),
866            parachain_id: None,
867            chain: jam_spec.id.as_str(),
868            role: ZombieRole::Node,
869            ns: &ns,
870            scoped_fs: &scoped_fs,
871            parachain: None,
872            bootnodes_addr: &vec![],
873            wait_ready: false,
874            nodes_by_name: json!({}),
875            global_settings,
876            resolved_db_snapshots: &resolved_db_snapshots,
877        };
878
879        let jam_global_files_to_inject = vec![TransferedFile::new(
880            PathBuf::from(format!("{base_dir}/jam_spec.json")),
881            PathBuf::from("/cfg/jam_spec.json"),
882        )];
883
884        let mut network =
885            Network::new_without_relay(ns.clone(), self.filesystem.clone(), network_spec.clone());
886
887        network.set_jamchain(Jamchain::new(
888            jam_spec.id.as_str(),
889            PathBuf::from(&jam_spec_full_path),
890        ));
891
892        let mut bootnodes_addr = vec![];
893        // Nodes spawned so far, so the args of the next ones can reference them.
894        let mut nodes_by_name = json!({});
895        for jam_node in &jam_spec.nodes {
896            let jam_ctx = SpawnNodeCtx {
897                bootnodes_addr: &bootnodes_addr,
898                nodes_by_name: nodes_by_name.clone(),
899                ..jam_ctx.clone()
900            };
901
902            let running_node =
903                spawner::spawn_jam_node(jam_node, jam_global_files_to_inject.clone(), &jam_ctx)
904                    .await?;
905
906            running_node
907                .wait_until_is_up(global_settings.node_spawn_timeout() as u64)
908                .await
909                .map_err(|e| OrchestratorError::InvalidConfig(e.to_string()))?;
910
911            bootnodes_addr.push(running_node.peer_addr().to_string());
912            nodes_by_name[running_node.name().to_owned()] = serde_json::to_value(&running_node)?;
913            network.add_running_jam_node(running_node).await;
914        }
915
916        // spawn paras
917        for para in network_spec.parachains.iter() {
918            if para.registration_strategy() != &RegistrationStrategy::Manual {
919                warn!(
920                    "⚠️  Parachain {} can't be registered automatically on a JAM chain, it needs to be registered manually.",
921                    para.id
922                );
923            }
924        }
925
926        // the JAM nodes are already running, so the collators can reference them
927        let jam_ctx = SpawnNodeCtx {
928            nodes_by_name,
929            ..jam_ctx
930        };
931
932        self.spawn_parachains(
933            &network_spec.parachains,
934            &jam_ctx,
935            &jam_global_files_to_inject,
936            spawn_concurrency,
937            &mut network,
938        )
939        .await?;
940
941        custom_processes::spawn_custom_processes(
942            &mut network,
943            network_spec,
944            ns.clone(),
945            configured_concurrency(network_spec),
946        )
947        .await?;
948
949        network.set_start_time_ts(start_time);
950
951        write_zombie_json(serde_json::to_value(&network)?, scoped_fs, ns.name()).await?;
952
953        if network_spec.global_settings.tear_down_on_failure() {
954            network.spawn_watching_task();
955        }
956
957        generators::cleanup_db_snapshot_cache(&resolved_db_snapshots).await;
958
959        Ok(network)
960    }
961}
962
963// Helpers
964
965/// Make sure a node persisted in `zombie.json` was spawned by the provider we
966/// are attaching with.
967fn validate_provider_tag(
968    inner: &serde_json::Value,
969    node_name: &str,
970    provider_name: &str,
971) -> Result<(), OrchestratorError> {
972    let provider_tag = inner
973        .get("provider_tag")
974        .and_then(|v| v.as_str())
975        .ok_or_else(|| {
976            OrchestratorError::InvalidConfig(format!(
977                "Node '{node_name}' is missing `provider_tag` in inner node JSON"
978            ))
979        })?;
980
981    if provider_tag != provider_name {
982        return Err(OrchestratorError::InvalidConfigForProvider(
983            provider_name.to_string(),
984            provider_tag.to_string(),
985        ));
986    }
987
988    Ok(())
989}
990
991async fn recreate_network_nodes_from_json(
992    nodes_json: &serde_json::Value,
993    ns: DynNamespace,
994    provider_name: &str,
995) -> Result<Vec<Arc<NetworkNode>>, OrchestratorError> {
996    let raw_nodes: Vec<RawNetworkNode> = serde_json::from_value(nodes_json.clone())?;
997
998    let mut nodes = Vec::with_capacity(raw_nodes.len());
999    for raw in raw_nodes {
1000        validate_provider_tag(&raw.inner, &raw.name, provider_name)?;
1001
1002        let inner = ns.spawn_node_from_json(&raw.inner).await?;
1003        let relay_node = NetworkNode::new(
1004            raw.name,
1005            raw.ws_uri,
1006            raw.internal_ws_uri,
1007            raw.prometheus_uri,
1008            raw.multiaddr,
1009            raw.spec,
1010            inner,
1011            raw.cmd_generator_opts,
1012            raw.context,
1013        );
1014        nodes.push(Arc::new(relay_node));
1015    }
1016
1017    Ok(nodes)
1018}
1019
1020async fn recreate_relaychain_from_json(
1021    zombie_json: &serde_json::Value,
1022    ns: DynNamespace,
1023    provider_name: &str,
1024) -> Result<(Relaychain, NetworkSpec), OrchestratorError> {
1025    let relay_json = zombie_json
1026        .get("relay")
1027        .ok_or(OrchestratorError::InvalidConfig(
1028            "Missing `relay` field in zombie.json".into(),
1029        ))?
1030        .clone();
1031
1032    let mut relay_raw: RawRelaychain = serde_json::from_value(relay_json)?;
1033
1034    let initial_spec: NetworkSpec = serde_json::from_value(
1035        zombie_json
1036            .get("initial_spec")
1037            .ok_or(OrchestratorError::InvalidConfig(
1038                "Missing `initial_spec` field in zombie.json".into(),
1039            ))?
1040            .clone(),
1041    )?;
1042
1043    // Populate relay nodes
1044    let nodes =
1045        recreate_network_nodes_from_json(&relay_raw.nodes, ns.clone(), provider_name).await?;
1046    relay_raw.inner.nodes = nodes;
1047
1048    Ok((relay_raw.inner, initial_spec))
1049}
1050
1051/// Rebuild the `jamchain` section of a `zombie.json`, if the network had one.
1052async fn recreate_jamchain_from_json(
1053    zombie_json: &serde_json::Value,
1054    ns: DynNamespace,
1055    provider_name: &str,
1056) -> Result<Option<Jamchain>, OrchestratorError> {
1057    let Some(jamchain_json) = zombie_json.get("jamchain") else {
1058        return Ok(None);
1059    };
1060
1061    let mut jamchain_raw: RawJamchain = serde_json::from_value(jamchain_json.clone())?;
1062    let raw_nodes: Vec<RawJamNetworkNode> = serde_json::from_value(jamchain_raw.nodes.clone())?;
1063
1064    let mut nodes = Vec::with_capacity(raw_nodes.len());
1065    for raw in raw_nodes {
1066        validate_provider_tag(&raw.inner, &raw.name, provider_name)?;
1067
1068        let inner = ns.spawn_node_from_json(&raw.inner).await?;
1069        nodes.push(Arc::new(JamNetworkNode::new(
1070            raw.name,
1071            inner,
1072            raw.spec,
1073            raw.ip,
1074            raw.cmd_generator_opts,
1075        )));
1076    }
1077
1078    jamchain_raw.inner.nodes = nodes;
1079
1080    Ok(Some(jamchain_raw.inner))
1081}
1082
1083/// Rebuild the `custom_processes` section of a `zombie.json`; empty when the
1084/// network had none (or predates custom processes being tracked).
1085async fn recreate_custom_processes_from_json(
1086    zombie_json: &serde_json::Value,
1087    ns: DynNamespace,
1088    provider_name: &str,
1089) -> Result<Vec<Arc<CustomProcessNode>>, OrchestratorError> {
1090    let Some(processes_json) = zombie_json.get("custom_processes") else {
1091        return Ok(vec![]);
1092    };
1093
1094    let raw_processes: Vec<RawCustomProcessNode> = serde_json::from_value(processes_json.clone())?;
1095
1096    let mut processes = Vec::with_capacity(raw_processes.len());
1097    for raw in raw_processes {
1098        validate_provider_tag(&raw.inner, &raw.name, provider_name)?;
1099
1100        // Attached as recorded, like a node, with the state it had; `state()`
1101        // and `wait_ready()` say more afterwards. A record the provider cannot
1102        // take back is skipped with a warning, the rest of the network attaches.
1103        let inner = match ns.spawn_node_from_json(&raw.inner).await {
1104            Ok(inner) => inner,
1105            Err(err) => {
1106                warn!(
1107                    "⚠️  {}: could not attach to the custom process, skipping it: {err}",
1108                    raw.name
1109                );
1110                continue;
1111            },
1112        };
1113
1114        // The `external` addresses saved on k8s were port-forwards of the
1115        // process that spawned the network, gone with it. Open new ones, and
1116        // keep the saved address where there is none to open.
1117        let mut ports = raw.ports;
1118        let state = raw.state.unwrap_or(CustomProcessState::Ready);
1119        // a one-shot is done by now, nothing to forward to; anything else may
1120        // still be up, whatever its recorded state
1121        if !running_in_ci() && !raw.spec.one_shot() {
1122            for (port_name, port) in ports.iter_mut() {
1123                let saved = std::mem::take(&mut port.external);
1124                port.external =
1125                    spawner::forward_or_keep(&inner, &raw.name, port_name, port.port, saved).await;
1126            }
1127        }
1128
1129        processes.push(Arc::new(CustomProcessNode::new(
1130            raw.name, inner, raw.spec, raw.ip, ports, state,
1131        )));
1132    }
1133
1134    Ok(processes)
1135}
1136
1137async fn recreate_parachains_from_json(
1138    zombie_json: &serde_json::Value,
1139    ns: DynNamespace,
1140    provider_name: &str,
1141) -> Result<HashMap<u32, Vec<Parachain>>, OrchestratorError> {
1142    let paras_json = zombie_json
1143        .get("parachains")
1144        .ok_or(OrchestratorError::InvalidConfig(
1145            "Missing `parachains` field in zombie.json".into(),
1146        ))?
1147        .clone();
1148
1149    let raw_paras: HashMap<u32, Vec<RawParachain>> = serde_json::from_value(paras_json)?;
1150
1151    let mut parachains_map = HashMap::new();
1152
1153    for (id, parachain_entries) in raw_paras {
1154        let mut parsed_vec = Vec::with_capacity(parachain_entries.len());
1155
1156        for raw_para in parachain_entries {
1157            let mut para = raw_para.inner;
1158            para.collators =
1159                recreate_network_nodes_from_json(&raw_para.collators, ns.clone(), provider_name)
1160                    .await?;
1161            parsed_vec.push(para);
1162        }
1163
1164        parachains_map.insert(id, parsed_vec);
1165    }
1166
1167    Ok(parachains_map)
1168}
1169
1170// Split the node list depending if it's bootnode or not
1171// NOTE: if there isn't a bootnode declared we use the first one
1172fn split_nodes_by_bootnodes(
1173    nodes: &[NodeSpec],
1174    no_default_bootnodes: bool,
1175) -> (Vec<&NodeSpec>, Vec<&NodeSpec>) {
1176    // get the bootnodes to spawn first and calculate the bootnode string for use later
1177    let mut bootnodes = vec![];
1178    let mut other_nodes = vec![];
1179    nodes.iter().for_each(|node| {
1180        if node.is_bootnode {
1181            bootnodes.push(node)
1182        } else {
1183            other_nodes.push(node)
1184        }
1185    });
1186
1187    if bootnodes.is_empty() && !no_default_bootnodes {
1188        bootnodes.push(other_nodes.remove(0))
1189    }
1190
1191    (bootnodes, other_nodes)
1192}
1193
1194// Generate a bootnode multiaddress and return as string
1195fn generate_bootnode_addr(
1196    node: &NetworkNode,
1197    ip: &IpAddr,
1198    port: u16,
1199) -> Result<String, GeneratorError> {
1200    generators::generate_node_bootnode_addr(
1201        &node.spec.peer_id,
1202        ip,
1203        port,
1204        node.args().as_ref(),
1205        &node.spec.p2p_cert_hash,
1206    )
1207}
1208
1209// Upload the raw chain-spec to the provider's file server (a no-op for providers without one),
1210// so the nodes spawned after it find the spec already there instead of each uploading it.
1211// The path is the one the nodes inject (`<base_dir>/<chain_spec_name>.json`), so their upload
1212// resolves to the same `<hash>__<file name>` entry.
1213async fn upload_raw_spec(
1214    ns: &DynNamespace,
1215    chain_spec: &generators::chain_spec::ChainSpec,
1216) -> Result<(), OrchestratorError> {
1217    if let Some(location) = chain_spec.raw_path() {
1218        ns.upload_to_fileserver(&ns.base_dir().join(location))
1219            .await?;
1220    }
1221
1222    Ok(())
1223}
1224
1225// Validate that the config fulfill all the requirements of the provider
1226fn validate_spec_with_provider_capabilities(
1227    network_spec: &NetworkSpec,
1228    capabilities: &ProviderCapabilities,
1229) -> Result<(), anyhow::Error> {
1230    let mut errs: Vec<String> = vec![];
1231
1232    if capabilities.requires_image {
1233        // Relaychain
1234        if let Some(relaychain) = network_spec.try_relaychain() {
1235            if relaychain.default_image.is_none() {
1236                // we should check if each node have an image
1237                let nodes = &relaychain.nodes;
1238                if nodes.iter().any(|node| node.image.is_none()) {
1239                    errs.push(String::from(
1240                        "Missing image for node, and not default is set at relaychain",
1241                    ));
1242                }
1243            }
1244        };
1245
1246        // Paras
1247        for para in &network_spec.parachains {
1248            if para.default_image.is_none() {
1249                let nodes = &para.collators;
1250                if nodes.iter().any(|node| node.image.is_none()) {
1251                    errs.push(format!(
1252                        "Missing image for node, and not default is set at parachain {}",
1253                        para.id
1254                    ));
1255                }
1256            }
1257        }
1258    } else {
1259        // native
1260        // We need to get all the `cmds` and verify if are part of the path
1261        let mut cmds: HashSet<&str> = Default::default();
1262        if let Some(relaychain) = network_spec.try_relaychain() {
1263            if let Some(cmd) = relaychain.default_command.as_ref() {
1264                cmds.insert(cmd.as_str());
1265            }
1266            for node in relaychain.nodes.iter() {
1267                cmds.insert(node.command());
1268            }
1269        }
1270
1271        if let Some(jamchain) = network_spec.jamchain.as_ref() {
1272            for node in jamchain.nodes.iter() {
1273                cmds.insert(node.command());
1274            }
1275        }
1276
1277        // Paras
1278        for para in &network_spec.parachains {
1279            if let Some(cmd) = para.default_command.as_ref() {
1280                cmds.insert(cmd.as_str());
1281            }
1282
1283            for node in para.collators.iter() {
1284                cmds.insert(node.command());
1285            }
1286        }
1287
1288        // now check the binaries
1289        let path = std::env::var("PATH").unwrap_or_default(); // path should always be set
1290        trace!("current PATH: {path}");
1291        let parts: Vec<_> = path.split(":").collect();
1292        for cmd in cmds {
1293            let missing = if cmd.contains('/') {
1294                trace!("checking {cmd}");
1295                if std::fs::metadata(cmd).is_err() {
1296                    true
1297                } else {
1298                    info!("🔎  We will use the full path {cmd} to spawn nodes.");
1299                    false
1300                }
1301            } else {
1302                // should be in the PATH
1303                !parts.iter().any(|part| {
1304                    let path_to = format!("{part}/{cmd}");
1305                    trace!("checking {path_to}");
1306                    let check_result = std::fs::metadata(&path_to);
1307                    trace!("result {:?}", check_result);
1308                    if check_result.is_ok() {
1309                        info!("🔎  We will use the cmd: '{cmd}' at path {path_to} to spawn nodes.");
1310                        true
1311                    } else {
1312                        false
1313                    }
1314                })
1315            };
1316
1317            if missing {
1318                errs.push(help_msg(cmd));
1319            }
1320        }
1321    }
1322
1323    if !errs.is_empty() {
1324        let msg = errs.join("\n");
1325        return Err(anyhow::anyhow!(format!("Invalid configuration: \n {msg}")));
1326    }
1327
1328    Ok(())
1329}
1330
1331fn help_msg(cmd: &str) -> String {
1332    match cmd {
1333        "parachain-template-node" | "solochain-template-node" | "minimal-template-node" => {
1334            format!("Missing binary {cmd}, compile by running: \n\tcargo build --package {cmd} --release")
1335        },
1336        "polkadot" => {
1337            format!("Missing binary {cmd}, compile by running (in the polkadot-sdk repo): \n\t cargo build --locked --release --features fast-runtime --bin {cmd} --bin polkadot-prepare-worker --bin polkadot-execute-worker")
1338        },
1339        "polkadot-parachain" => {
1340            format!("Missing binary {cmd}, compile by running (in the polkadot-sdk repo): \n\t cargo build --release --locked -p {cmd}-bin --bin {cmd}")
1341        },
1342        _ => {
1343            format!("Missing binary {cmd}, please compile it.")
1344        },
1345    }
1346}
1347
1348/// Allow to set the default concurrency through env var `ZOMBIE_SPAWN_CONCURRENCY`
1349fn spawn_concurrency_from_env() -> Option<usize> {
1350    if let Ok(concurrency) = env::var("ZOMBIE_SPAWN_CONCURRENCY") {
1351        concurrency.parse::<usize>().ok()
1352    } else {
1353        None
1354    }
1355}
1356
1357/// The spawn concurrency as set (env over settings, 20 when neither), before
1358/// any token limit: what custom processes spawn with, since their placeholders
1359/// resolve from nodes that are all up and ports that are all reserved before
1360/// any starts. At least 1: with 0 nothing would ever start.
1361fn configured_concurrency(spec: &NetworkSpec) -> usize {
1362    spawn_concurrency_from_env()
1363        .or(spec.global_settings.spawn_concurrency())
1364        .unwrap_or(20)
1365        .max(1)
1366}
1367
1368/// Whether a chain's spec has a `{{ZOMBIE:..}}` placeholder, which makes the
1369/// nodes spawn one at a time. Custom processes are left out: theirs resolve
1370/// from nodes that are all up by then.
1371fn chains_have_tokens(spec: &NetworkSpec) -> Result<bool, anyhow::Error> {
1372    let chains = (&spec.relaychain, &spec.jamchain, &spec.parachains);
1373    Ok(has_tokens(&serde_json::to_string(&chains)?))
1374}
1375
1376/// The concurrency nodes spawn with, and whether tokens limited it to 1.
1377fn calculate_concurrency(spec: &NetworkSpec) -> Result<(usize, bool), anyhow::Error> {
1378    let concurrency = configured_concurrency(spec);
1379    if concurrency > 1 && chains_have_tokens(spec)? {
1380        Ok((1, true))
1381    } else {
1382        Ok((concurrency, false))
1383    }
1384}
1385
1386/// Build deterministic dependency **levels** among the given nodes.
1387/// - Only dependencies **between nodes in `nodes`** are considered.
1388/// - Unknown/out-of-scope references are ignored.
1389/// - Self-dependencies are ignored.
1390fn dependency_levels_among<'a>(
1391    nodes: &'a [&'a NodeSpec],
1392) -> Result<Vec<Vec<&'a NodeSpec>>, OrchestratorError> {
1393    let by_name = nodes
1394        .iter()
1395        .map(|n| (n.name.as_str(), *n))
1396        .collect::<HashMap<_, _>>();
1397
1398    let mut graph = HashMap::with_capacity(nodes.len());
1399    let mut indegree = HashMap::with_capacity(nodes.len());
1400
1401    for node in nodes {
1402        graph.insert(node.name.as_str(), Vec::new());
1403        indegree.insert(node.name.as_str(), 0);
1404    }
1405
1406    // build dependency graph
1407    for &node in nodes {
1408        if let Ok(args_json) = serde_json::to_string(&node.args) {
1409            // collect dependencies
1410            let unique_deps = get_tokens_to_replace(&args_json)
1411                .into_iter()
1412                .filter(|dep| dep != &node.name)
1413                .filter_map(|dep| by_name.get(dep.as_str()))
1414                .map(|&dep_node| dep_node.name.as_str())
1415                .collect::<HashSet<_>>();
1416
1417            for dep_name in unique_deps {
1418                graph
1419                    .get_mut(dep_name)
1420                    .expect(&format!("{GRAPH_CONTAINS_DEP} {THIS_IS_A_BUG}"))
1421                    .push(node);
1422                *indegree
1423                    .get_mut(node.name.as_str())
1424                    .expect(&format!("{INDEGREE_CONTAINS_NAME} {THIS_IS_A_BUG}")) += 1;
1425            }
1426        }
1427    }
1428
1429    // find all nodes with no dependencies
1430    let mut queue = nodes
1431        .iter()
1432        .filter(|n| {
1433            *indegree
1434                .get(n.name.as_str())
1435                .expect(&format!("{INDEGREE_CONTAINS_NAME} {THIS_IS_A_BUG}"))
1436                == 0
1437        })
1438        .copied()
1439        .collect::<VecDeque<_>>();
1440
1441    let mut processed_count = 0;
1442    let mut levels = Vec::new();
1443
1444    // Kahn's algorithm
1445    while !queue.is_empty() {
1446        let level_size = queue.len();
1447        let mut current_level = Vec::with_capacity(level_size);
1448
1449        for _ in 0..level_size {
1450            let n = queue
1451                .pop_front()
1452                .expect(&format!("{QUEUE_NOT_EMPTY} {THIS_IS_A_BUG}"));
1453            current_level.push(n);
1454            processed_count += 1;
1455
1456            for &neighbour in graph
1457                .get(n.name.as_str())
1458                .expect(&format!("{GRAPH_CONTAINS_NAME} {THIS_IS_A_BUG}"))
1459            {
1460                let neighbour_indegree = indegree
1461                    .get_mut(neighbour.name.as_str())
1462                    .expect(&format!("{INDEGREE_CONTAINS_NAME} {THIS_IS_A_BUG}"));
1463                *neighbour_indegree -= 1;
1464
1465                if *neighbour_indegree == 0 {
1466                    queue.push_back(neighbour);
1467                }
1468            }
1469        }
1470
1471        current_level.sort_by_key(|n| &n.name);
1472        levels.push(current_level);
1473    }
1474
1475    // cycles detected, e.g A -> B -> A
1476    if processed_count != nodes.len() {
1477        return Err(OrchestratorError::InvalidConfig(
1478            "Tokens have cyclical dependencies".to_string(),
1479        ));
1480    }
1481
1482    Ok(levels)
1483}
1484
1485// TODO: get the fs from `DynNamespace` will make this not needed
1486// but the FileSystem trait isn't object-safe so we can't pass around
1487// as `dyn FileSystem`. We can refactor or using some `erase` techniques
1488// to resolve this and remove this struct
1489// TODO (Loris): Probably we could have a .scoped(base_dir) method on the
1490// filesystem itself (the trait), so it will return this and we can move this
1491// directly to the support crate, it can be useful in the future
1492#[derive(Clone, Debug)]
1493pub struct ScopedFilesystem<'a, FS: FileSystem> {
1494    fs: &'a FS,
1495    base_dir: &'a str,
1496}
1497
1498impl<'a, FS: FileSystem> ScopedFilesystem<'a, FS> {
1499    pub fn new(fs: &'a FS, base_dir: &'a str) -> Self {
1500        Self { fs, base_dir }
1501    }
1502
1503    async fn copy_files(&self, files: Vec<&TransferedFile>) -> Result<(), FileSystemError> {
1504        for file in files {
1505            let full_remote_path = PathBuf::from(format!(
1506                "{}/{}",
1507                self.base_dir,
1508                file.remote_path.to_string_lossy()
1509            ));
1510            trace!("coping file: {file}");
1511            self.fs
1512                .copy(file.local_path.as_path(), full_remote_path)
1513                .await?;
1514        }
1515        Ok(())
1516    }
1517
1518    async fn read(&self, file: impl AsRef<Path>) -> Result<Vec<u8>, FileSystemError> {
1519        let file = file.as_ref();
1520
1521        let full_path = if file.is_absolute() {
1522            file.to_owned()
1523        } else {
1524            PathBuf::from(format!("{}/{}", self.base_dir, file.to_string_lossy()))
1525        };
1526        let content = self.fs.read(full_path).await?;
1527        Ok(content)
1528    }
1529
1530    async fn read_to_string(&self, file: impl AsRef<Path>) -> Result<String, FileSystemError> {
1531        let file = file.as_ref();
1532
1533        let full_path = if file.is_absolute() {
1534            file.to_owned()
1535        } else {
1536            PathBuf::from(format!("{}/{}", self.base_dir, file.to_string_lossy()))
1537        };
1538        let content = self.fs.read_to_string(full_path).await?;
1539        Ok(content)
1540    }
1541
1542    async fn create_dir(&self, path: impl AsRef<Path>) -> Result<(), FileSystemError> {
1543        let path = PathBuf::from(format!(
1544            "{}/{}",
1545            self.base_dir,
1546            path.as_ref().to_string_lossy()
1547        ));
1548        self.fs.create_dir(path).await
1549    }
1550
1551    async fn create_dir_all(&self, path: impl AsRef<Path>) -> Result<(), FileSystemError> {
1552        let path = PathBuf::from(format!(
1553            "{}/{}",
1554            self.base_dir,
1555            path.as_ref().to_string_lossy()
1556        ));
1557        self.fs.create_dir_all(path).await
1558    }
1559
1560    async fn write(
1561        &self,
1562        path: impl AsRef<Path>,
1563        contents: impl AsRef<[u8]> + Send,
1564    ) -> Result<(), FileSystemError> {
1565        let path = path.as_ref();
1566
1567        let full_path = if path.is_absolute() {
1568            path.to_owned()
1569        } else {
1570            PathBuf::from(format!("{}/{}", self.base_dir, path.to_string_lossy()))
1571        };
1572
1573        self.fs.write(full_path, contents).await
1574    }
1575
1576    /// Get the full_path in the scoped FS
1577    fn full_path(&self, path: impl AsRef<Path>) -> PathBuf {
1578        let path = path.as_ref();
1579
1580        let full_path = if path.is_absolute() {
1581            path.to_owned()
1582        } else {
1583            PathBuf::from(format!("{}/{}", self.base_dir, path.to_string_lossy()))
1584        };
1585
1586        full_path
1587    }
1588
1589    /// Get the base_dir in the scoped FS
1590    fn base_dir(&self) -> &str {
1591        self.base_dir
1592    }
1593}
1594
1595#[derive(Clone, Debug)]
1596pub enum ZombieRole {
1597    Temp,
1598    Node,
1599    Bootnode,
1600    Collator,
1601    CumulusCollator,
1602}
1603
1604impl ZombieRole {
1605    pub(crate) fn node_role(&self) -> NodeRole {
1606        match self {
1607            ZombieRole::Node => NodeRole::Node,
1608            ZombieRole::Bootnode => NodeRole::Bootnode,
1609            ZombieRole::Collator => NodeRole::Collator,
1610            ZombieRole::CumulusCollator => NodeRole::CumulusCollator,
1611            ZombieRole::Temp => NodeRole::Temp,
1612        }
1613    }
1614}
1615
1616// re-exports
1617pub use network::{AddCollatorOptions, AddNodeOptions};
1618pub use network_helper::metrics;
1619pub use sc_chain_spec;
1620
1621#[cfg(test)]
1622mod tests {
1623    use configuration::{GlobalSettingsBuilder, NetworkConfigBuilder};
1624    use lazy_static::lazy_static;
1625    use tokio::sync::Mutex;
1626
1627    use super::*;
1628
1629    const ENV_KEY: &str = "ZOMBIE_SPAWN_CONCURRENCY";
1630    // mutex for test that use env
1631    lazy_static! {
1632        static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
1633    }
1634
1635    fn set_env(concurrency: Option<u32>) {
1636        if let Some(value) = concurrency {
1637            env::set_var(ENV_KEY, value.to_string());
1638        } else {
1639            env::remove_var(ENV_KEY);
1640        }
1641    }
1642
1643    fn generate(
1644        with_image: bool,
1645        with_cmd: Option<&'static str>,
1646    ) -> Result<NetworkConfig, Vec<anyhow::Error>> {
1647        NetworkConfigBuilder::new()
1648            .with_relaychain(|r| {
1649                let mut relay = r
1650                    .with_chain("rococo-local")
1651                    .with_default_command(with_cmd.unwrap_or("polkadot"));
1652                if with_image {
1653                    relay = relay.with_default_image("docker.io/parity/polkadot")
1654                }
1655
1656                relay
1657                    .with_validator(|node| node.with_name("alice"))
1658                    .with_validator(|node| node.with_name("bob"))
1659            })
1660            .with_parachain(|p| {
1661                p.with_id(2000).cumulus_based(true).with_collator(|n| {
1662                    let node = n
1663                        .with_name("collator")
1664                        .with_command(with_cmd.unwrap_or("polkadot-parachain"));
1665                    if with_image {
1666                        node.with_image("docker.io/paritypr/test-parachain")
1667                    } else {
1668                        node
1669                    }
1670                })
1671            })
1672            .build()
1673    }
1674
1675    fn get_node_with_dependencies(name: &str, dependencies: Option<Vec<&NodeSpec>>) -> NodeSpec {
1676        let mut spec = NodeSpec {
1677            name: name.to_string(),
1678            ..Default::default()
1679        };
1680        if let Some(dependencies) = dependencies {
1681            for node in dependencies {
1682                spec.args.push(
1683                    format!("{{{{ZOMBIE:{}:someField}}}}", node.name)
1684                        .as_str()
1685                        .into(),
1686                );
1687            }
1688        }
1689        spec
1690    }
1691
1692    fn verify_levels(actual_levels: Vec<Vec<&NodeSpec>>, expected_levels: Vec<Vec<&str>>) {
1693        actual_levels
1694            .iter()
1695            .zip(expected_levels)
1696            .for_each(|(actual_level, expected_level)| {
1697                assert_eq!(actual_level.len(), expected_level.len());
1698                actual_level
1699                    .iter()
1700                    .zip(expected_level.iter())
1701                    .for_each(|(node, expected_name)| assert_eq!(node.name, *expected_name));
1702            });
1703    }
1704
1705    #[tokio::test]
1706    async fn valid_config_with_image() {
1707        let network_config = generate(true, None).unwrap();
1708        let spec = NetworkSpec::from_config(&network_config).await.unwrap();
1709        let caps = ProviderCapabilities {
1710            requires_image: true,
1711            has_resources: false,
1712            prefix_with_full_path: false,
1713            use_default_ports_in_cmd: false,
1714        };
1715
1716        let valid = validate_spec_with_provider_capabilities(&spec, &caps);
1717        assert!(valid.is_ok())
1718    }
1719
1720    #[tokio::test]
1721    async fn invalid_config_without_image() {
1722        let network_config = generate(false, None).unwrap();
1723        let spec = NetworkSpec::from_config(&network_config).await.unwrap();
1724        let caps = ProviderCapabilities {
1725            requires_image: true,
1726            has_resources: false,
1727            prefix_with_full_path: false,
1728            use_default_ports_in_cmd: false,
1729        };
1730
1731        let valid = validate_spec_with_provider_capabilities(&spec, &caps);
1732        assert!(valid.is_err())
1733    }
1734
1735    #[tokio::test]
1736    async fn invalid_config_missing_cmd() {
1737        let network_config = generate(false, Some("other")).unwrap();
1738        let spec = NetworkSpec::from_config(&network_config).await.unwrap();
1739        let caps = ProviderCapabilities {
1740            requires_image: false,
1741            has_resources: false,
1742            prefix_with_full_path: false,
1743            use_default_ports_in_cmd: false,
1744        };
1745
1746        let valid = validate_spec_with_provider_capabilities(&spec, &caps);
1747        assert!(valid.is_err())
1748    }
1749
1750    #[tokio::test]
1751    async fn valid_config_present_cmd() {
1752        let network_config = generate(false, Some("cargo")).unwrap();
1753        let spec = NetworkSpec::from_config(&network_config).await.unwrap();
1754        let caps = ProviderCapabilities {
1755            requires_image: false,
1756            has_resources: false,
1757            prefix_with_full_path: false,
1758            use_default_ports_in_cmd: false,
1759        };
1760
1761        let valid = validate_spec_with_provider_capabilities(&spec, &caps);
1762        println!("{valid:?}");
1763        assert!(valid.is_ok())
1764    }
1765
1766    #[tokio::test]
1767    async fn default_spawn_concurrency() {
1768        let _g = ENV_MUTEX.lock().await;
1769        set_env(None);
1770        let network_config = generate(false, Some("cargo")).unwrap();
1771        let spec = NetworkSpec::from_config(&network_config).await.unwrap();
1772        let (concurrency, _) = calculate_concurrency(&spec).unwrap();
1773        assert_eq!(concurrency, 20);
1774    }
1775
1776    #[tokio::test]
1777    async fn set_spawn_concurrency() {
1778        let _g = ENV_MUTEX.lock().await;
1779        set_env(None);
1780
1781        let network_config = generate(false, Some("cargo")).unwrap();
1782        let mut spec = NetworkSpec::from_config(&network_config).await.unwrap();
1783
1784        let global_settings = GlobalSettingsBuilder::new()
1785            .with_spawn_concurrency(4)
1786            .build()
1787            .unwrap();
1788
1789        spec.set_global_settings(global_settings);
1790        let (concurrency, limited) = calculate_concurrency(&spec).unwrap();
1791        assert_eq!(concurrency, 4);
1792        assert!(!limited);
1793    }
1794
1795    #[tokio::test]
1796    async fn set_spawn_concurrency_but_limited() {
1797        let _g = ENV_MUTEX.lock().await;
1798        set_env(None);
1799
1800        let network_config = generate(false, Some("cargo")).unwrap();
1801        let mut spec = NetworkSpec::from_config(&network_config).await.unwrap();
1802
1803        let global_settings = GlobalSettingsBuilder::new()
1804            .with_spawn_concurrency(4)
1805            .build()
1806            .unwrap();
1807
1808        spec.set_global_settings(global_settings);
1809        let node = spec.relaychain_mut().nodes.first_mut().unwrap();
1810        node.args
1811            .push("--bootnodes {{ZOMBIE:bob:multiAddress')}}".into());
1812        let (concurrency, limited) = calculate_concurrency(&spec).unwrap();
1813        assert_eq!(concurrency, 1);
1814        assert!(limited);
1815    }
1816
1817    #[tokio::test]
1818    async fn custom_process_placeholders_do_not_limit_spawn_concurrency() {
1819        let _g = ENV_MUTEX.lock().await;
1820        set_env(None);
1821
1822        let network_config = generate(false, Some("cargo")).unwrap();
1823        let mut spec = NetworkSpec::from_config(&network_config).await.unwrap();
1824
1825        let global_settings = GlobalSettingsBuilder::new()
1826            .with_spawn_concurrency(4)
1827            .build()
1828            .unwrap();
1829
1830        spec.set_global_settings(global_settings);
1831        spec.custom_processes.push(
1832            configuration::CustomProcessBuilder::new()
1833                .with_name("api")
1834                .with_command("api")
1835                .with_args(vec!["{{ZOMBIE:alice:ws_uri}}".into()])
1836                .build()
1837                .unwrap(),
1838        );
1839        let (concurrency, limited) = calculate_concurrency(&spec).unwrap();
1840        assert_eq!(concurrency, 4);
1841        assert!(!limited);
1842    }
1843
1844    #[tokio::test]
1845    async fn set_spawn_concurrency_from_env() {
1846        let _g = ENV_MUTEX.lock().await;
1847        set_env(Some(10));
1848
1849        let network_config = generate(false, Some("cargo")).unwrap();
1850        let spec = NetworkSpec::from_config(&network_config).await.unwrap();
1851        let (concurrency, limited) = calculate_concurrency(&spec).unwrap();
1852        assert_eq!(concurrency, 10);
1853        assert!(!limited);
1854    }
1855
1856    #[tokio::test]
1857    async fn set_spawn_concurrency_from_env_but_limited() {
1858        let _g = ENV_MUTEX.lock().await;
1859        set_env(Some(12));
1860
1861        let network_config = generate(false, Some("cargo")).unwrap();
1862        let mut spec = NetworkSpec::from_config(&network_config).await.unwrap();
1863        let node = spec.relaychain_mut().nodes.first_mut().unwrap();
1864        node.args
1865            .push("--bootnodes {{ZOMBIE:bob:multiAddress')}}".into());
1866        let (concurrency, limited) = calculate_concurrency(&spec).unwrap();
1867        assert_eq!(concurrency, 1);
1868        assert!(limited);
1869    }
1870
1871    #[test]
1872    fn dependency_levels_among_should_work() {
1873        // no nodes
1874        assert!(dependency_levels_among(&[]).unwrap().is_empty());
1875
1876        // one node
1877        let alice = get_node_with_dependencies("alice", None);
1878        let nodes = [&alice];
1879
1880        let levels = dependency_levels_among(&nodes).unwrap();
1881        let expected = vec![vec!["alice"]];
1882
1883        verify_levels(levels, expected);
1884
1885        // two independent nodes
1886        let alice = get_node_with_dependencies("alice", None);
1887        let bob = get_node_with_dependencies("bob", None);
1888        let nodes = [&alice, &bob];
1889
1890        let levels = dependency_levels_among(&nodes).unwrap();
1891        let expected = vec![vec!["alice", "bob"]];
1892
1893        verify_levels(levels, expected);
1894
1895        // alice -> bob -> charlie
1896        let alice = get_node_with_dependencies("alice", None);
1897        let bob = get_node_with_dependencies("bob", Some(vec![&alice]));
1898        let charlie = get_node_with_dependencies("charlie", Some(vec![&bob]));
1899        let nodes = [&alice, &bob, &charlie];
1900
1901        let levels = dependency_levels_among(&nodes).unwrap();
1902        let expected = vec![vec!["alice"], vec!["bob"], vec!["charlie"]];
1903
1904        verify_levels(levels, expected);
1905
1906        //         ┌─> bob
1907        // alice ──|
1908        //         └─> charlie
1909        let alice = get_node_with_dependencies("alice", None);
1910        let bob = get_node_with_dependencies("bob", Some(vec![&alice]));
1911        let charlie = get_node_with_dependencies("charlie", Some(vec![&alice]));
1912        let nodes = [&alice, &bob, &charlie];
1913
1914        let levels = dependency_levels_among(&nodes).unwrap();
1915        let expected = vec![vec!["alice"], vec!["bob", "charlie"]];
1916
1917        verify_levels(levels, expected);
1918
1919        //         ┌─>   bob  ──┐
1920        // alice ──|            ├─> dave
1921        //         └─> charlie  ┘
1922        let alice = get_node_with_dependencies("alice", None);
1923        let bob = get_node_with_dependencies("bob", Some(vec![&alice]));
1924        let charlie = get_node_with_dependencies("charlie", Some(vec![&alice]));
1925        let dave = get_node_with_dependencies("dave", Some(vec![&charlie, &bob]));
1926        let nodes = [&alice, &bob, &charlie, &dave];
1927
1928        let levels = dependency_levels_among(&nodes).unwrap();
1929        let expected = vec![vec!["alice"], vec!["bob", "charlie"], vec!["dave"]];
1930
1931        verify_levels(levels, expected);
1932    }
1933
1934    #[test]
1935    fn dependency_levels_among_should_detect_cycles() {
1936        let mut alice = get_node_with_dependencies("alice", None);
1937        let bob = get_node_with_dependencies("bob", Some(vec![&alice]));
1938        alice.args.push("{{ZOMBIE:bob:someField}}".into());
1939
1940        assert!(dependency_levels_among(&[&alice, &bob]).is_err())
1941    }
1942}