Skip to main content

zombienet_orchestrator/
network.rs

1pub mod chain_upgrade;
2pub mod jamchain;
3pub mod node;
4pub mod parachain;
5pub mod relaychain;
6
7use std::{
8    cell::RefCell,
9    collections::HashMap,
10    path::PathBuf,
11    rc::Rc,
12    sync::Arc,
13    time::{Duration, SystemTime, UNIX_EPOCH},
14};
15
16use configuration::{
17    para_states::{Initial, Running},
18    shared::{helpers::generate_unique_node_name_from_names, node::EnvVar},
19    types::{Arg, Command, Image, ParaId, Port, ValidationContext},
20    ParachainConfig, ParachainConfigBuilder, RegistrationStrategy,
21};
22use provider::{types::TransferedFile, DynNamespace, ProviderError};
23use serde::{Deserialize, Serialize};
24use support::{
25    constants::{RELAY_NOT_NONE, THIS_IS_A_BUG},
26    fs::FileSystem,
27};
28use tokio::sync::RwLock;
29use tracing::{error, warn};
30
31use self::{
32    jamchain::Jamchain,
33    node::{JamNetworkNode, NetworkNode, NodeKind, SpawnedNode},
34    parachain::Parachain,
35    relaychain::Relaychain,
36};
37use crate::{
38    generators::{self, chain_spec::ChainSpec},
39    network_spec::{self, NetworkSpec},
40    observability::{self, ObservabilityInfo, ObservabilityState},
41    shared::{
42        constants::{NODE_MONITORING_FAILURE_THRESHOLD_SECONDS, NODE_MONITORING_INTERVAL_SECONDS},
43        macros,
44        types::{ChainDefaultContext, RegisterParachainOptions},
45    },
46    spawner::{self, SpawnNodeCtx},
47    utils::write_zombie_json,
48    ScopedFilesystem, ZombieRole,
49};
50
51/// Context where the node is running
52/// RC or Para
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54pub enum NodeContext {
55    Rc,
56    Para {
57        para_id: ParaId,
58        is_cumulus_based: bool,
59    },
60    Jam,
61}
62
63#[derive(Serialize)]
64pub struct Network<T: FileSystem> {
65    #[serde(skip)]
66    ns: DynNamespace,
67    #[serde(skip)]
68    filesystem: T,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    relay: Option<Relaychain>,
71    initial_spec: NetworkSpec,
72    parachains: HashMap<u32, Vec<Parachain>>,
73    #[serde(skip_serializing_if = "Option::is_none")]
74    jamchain: Option<Jamchain>,
75    /// Every node spawned in this network, by name, regardless of its kind.
76    ///
77    /// Holds the same `Arc`s as the typed collections above; downcast with
78    /// [`Network::get_node`] / [`Network::get_jam_node`] to get the concrete
79    /// type back.
80    #[serde(skip)]
81    nodes_by_name: HashMap<String, Arc<dyn SpawnedNode>>,
82    #[serde(skip)]
83    nodes_to_watch: Arc<RwLock<Vec<Arc<dyn SpawnedNode>>>>,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    start_time_ts: Option<String>,
86    #[serde(skip)]
87    observability: ObservabilityState,
88}
89
90impl<T: FileSystem> std::fmt::Debug for Network<T> {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        f.debug_struct("Network")
93            .field("ns", &"ns_skipped")
94            .field("relay", &self.relay)
95            .field("initial_spec", &self.initial_spec)
96            .field("parachains", &self.parachains)
97            .field("jamchain", &self.jamchain)
98            .field("nodes_by_name", &self.nodes_by_name.keys())
99            .field("observability", &self.observability)
100            .finish()
101    }
102}
103
104macros::create_add_options!(AddNodeOptions {
105    chain_spec: Option<PathBuf>,
106    override_eth_key: Option<String>
107});
108
109macros::create_add_options!(AddCollatorOptions {
110    chain_spec: Option<PathBuf>,
111    chain_spec_relay: Option<PathBuf>,
112    override_eth_key: Option<String>
113});
114
115impl<T: FileSystem> Network<T> {
116    pub(crate) fn new_with_relay(
117        relay: Relaychain,
118        ns: DynNamespace,
119        fs: T,
120        initial_spec: NetworkSpec,
121    ) -> Self {
122        Self::new(Some(relay), ns, fs, initial_spec)
123    }
124
125    /// Create a network whose root chain is a JAM chain instead of a relaychain.
126    pub(crate) fn new_without_relay(ns: DynNamespace, fs: T, initial_spec: NetworkSpec) -> Self {
127        Self::new(None, ns, fs, initial_spec)
128    }
129
130    fn new(relay: Option<Relaychain>, ns: DynNamespace, fs: T, initial_spec: NetworkSpec) -> Self {
131        Self {
132            ns,
133            filesystem: fs,
134            relay,
135            initial_spec,
136            parachains: Default::default(),
137            jamchain: Default::default(),
138            nodes_by_name: Default::default(),
139            nodes_to_watch: Default::default(),
140            start_time_ts: Default::default(),
141            observability: ObservabilityState::default(),
142        }
143    }
144
145    // Pubic API
146    pub fn ns_name(&self) -> String {
147        self.ns.name().to_string()
148    }
149
150    pub fn base_dir(&self) -> Option<&str> {
151        self.ns.base_dir().to_str()
152    }
153
154    pub fn relaychain(&self) -> &Relaychain {
155        self.relay
156            .as_ref()
157            .expect(&format!("{RELAY_NOT_NONE}, {THIS_IS_A_BUG}"))
158    }
159
160    /// The relaychain of the network, `None` for a JAM network.
161    pub fn try_relaychain(&self) -> Option<&Relaychain> {
162        self.relay.as_ref()
163    }
164
165    fn relaychain_mut(&mut self) -> &mut Relaychain {
166        self.relay
167            .as_mut()
168            .expect(&format!("{RELAY_NOT_NONE}, {THIS_IS_A_BUG}"))
169    }
170
171    // Teardown the network
172    pub async fn destroy(mut self) -> Result<(), ProviderError> {
173        if let Err(e) = self.stop_observability().await {
174            warn!("⚠️  Failed to cleanup observability stack: {e}");
175        }
176        self.ns.destroy().await
177    }
178
179    pub fn observability(&self) -> Option<&ObservabilityInfo> {
180        self.observability.as_runnnig()
181    }
182
183    pub fn observability_state(&self) -> &ObservabilityState {
184        &self.observability
185    }
186
187    /// Add a node to the relaychain
188    // The new node is added to the running network instance.
189    /// # Example:
190    /// ```rust
191    /// # use provider::NativeProvider;
192    /// # use support::{fs::local::LocalFileSystem};
193    /// # use zombienet_orchestrator::{errors, AddNodeOptions, Orchestrator};
194    /// # use configuration::NetworkConfig;
195    /// # async fn example() -> Result<(), errors::OrchestratorError> {
196    /// #   let provider = NativeProvider::new(LocalFileSystem {});
197    /// #   let orchestrator = Orchestrator::new(LocalFileSystem {}, provider);
198    /// #   let config = NetworkConfig::load_from_toml("config.toml")?;
199    /// let mut network = orchestrator.spawn(config).await?;
200    ///
201    /// // Create the options to add the new node
202    /// let opts = AddNodeOptions {
203    ///     rpc_port: Some(9444),
204    ///     is_validator: true,
205    ///     ..Default::default()
206    /// };
207    ///
208    /// network.add_node("new-node", opts).await?;
209    /// #   Ok(())
210    /// # }
211    /// ```
212    pub async fn add_node(
213        &mut self,
214        name: impl Into<String>,
215        options: AddNodeOptions,
216    ) -> Result<(), anyhow::Error> {
217        let name = generate_unique_node_name_from_names(
218            name,
219            &mut self.nodes_by_name.keys().cloned().collect(),
220        );
221
222        let relaychain = self.relaychain();
223
224        let chain_spec_path = if let Some(chain_spec_custom_path) = &options.chain_spec {
225            chain_spec_custom_path.clone()
226        } else {
227            PathBuf::from(format!(
228                "{}/{}.json",
229                self.ns.base_dir().to_string_lossy(),
230                relaychain.chain
231            ))
232        };
233
234        let chain_context = ChainDefaultContext {
235            default_command: self.initial_spec.relaychain().default_command.as_ref(),
236            default_image: self.initial_spec.relaychain().default_image.as_ref(),
237            default_resources: self.initial_spec.relaychain().default_resources.as_ref(),
238            default_db_snapshot: self.initial_spec.relaychain().default_db_snapshot.as_ref(),
239            default_args: self.initial_spec.relaychain().default_args.iter().collect(),
240        };
241
242        let mut node_spec = network_spec::node::NodeSpec::from_ad_hoc(
243            &name,
244            options.into(),
245            &chain_context,
246            false,
247            false,
248        )?;
249
250        node_spec.available_args_output = Some(
251            self.initial_spec
252                .node_available_args_output(&node_spec, self.ns.clone())
253                .await?,
254        );
255
256        let base_dir = self.ns.base_dir().to_string_lossy();
257        let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
258
259        let resolved_db_snapshots = generators::resolve_db_snapshots(
260            std::iter::once(&node_spec),
261            &self.ns,
262            &self.filesystem,
263        )
264        .await?;
265
266        let ctx = SpawnNodeCtx {
267            chain_id: &relaychain.chain_id,
268            parachain_id: None,
269            chain: &relaychain.chain,
270            role: ZombieRole::Node,
271            ns: &self.ns,
272            scoped_fs: &scoped_fs,
273            parachain: None,
274            bootnodes_addr: &vec![],
275            wait_ready: true,
276            nodes_by_name: serde_json::to_value(&self.nodes_by_name)?,
277            global_settings: &self.initial_spec.global_settings,
278            resolved_db_snapshots: &resolved_db_snapshots,
279        };
280
281        let global_files_to_inject = vec![TransferedFile::new(
282            chain_spec_path,
283            PathBuf::from(format!("/cfg/{}.json", relaychain.chain)),
284        )];
285
286        let node = spawner::spawn_node(&node_spec, global_files_to_inject, &ctx).await?;
287
288        // TODO: register the new node as validator in the relaychain
289        // STEPS:
290        //  - check balance of `stash` derivation for validator account
291        //  - call rotate_keys on the new validator
292        //  - call setKeys on the new validator
293        // if node_spec.is_validator {
294        //     let running_node = self.relay.nodes.first().unwrap();
295        //     // tx_helper::validator_actions::register(vec![&node], &running_node.ws_uri, None).await?;
296        // }
297
298        // Let's make sure node is up before adding
299        node.wait_until_is_up(self.initial_spec.global_settings.network_spawn_timeout())
300            .await?;
301
302        // Add node to relaychain data
303        self.add_running_node(node.clone(), None).await;
304
305        // Dump zombie.json
306        self.write_zombie_json().await?;
307
308        generators::cleanup_db_snapshot_cache(&resolved_db_snapshots).await;
309
310        Ok(())
311    }
312
313    /// Add a new collator to a parachain
314    ///
315    /// NOTE: if more parachains with given id available (rare corner case)
316    /// then it adds collator to the first parachain
317    ///
318    /// # Example:
319    /// ```rust
320    /// # use provider::NativeProvider;
321    /// # use support::{fs::local::LocalFileSystem};
322    /// # use zombienet_orchestrator::{errors, AddCollatorOptions, Orchestrator};
323    /// # use configuration::NetworkConfig;
324    /// # async fn example() -> Result<(), anyhow::Error> {
325    /// #   let provider = NativeProvider::new(LocalFileSystem {});
326    /// #   let orchestrator = Orchestrator::new(LocalFileSystem {}, provider);
327    /// #   let config = NetworkConfig::load_from_toml("config.toml")?;
328    /// let mut network = orchestrator.spawn(config).await?;
329    ///
330    /// let col_opts = AddCollatorOptions {
331    ///     command: Some("polkadot-parachain".try_into()?),
332    ///     ..Default::default()
333    /// };
334    ///
335    /// network.add_collator("new-col-1", col_opts, 100).await?;
336    /// #   Ok(())
337    /// # }
338    /// ```
339    pub async fn add_collator(
340        &mut self,
341        name: impl Into<String>,
342        options: AddCollatorOptions,
343        para_id: u32,
344    ) -> Result<(), anyhow::Error> {
345        let name = generate_unique_node_name_from_names(
346            name,
347            &mut self.nodes_by_name.keys().cloned().collect(),
348        );
349        let spec = self
350            .initial_spec
351            .parachains
352            .iter()
353            .find(|para| para.id == para_id)
354            .ok_or(anyhow::anyhow!(format!("parachain: {para_id} not found!")))?;
355        let role = if spec.is_cumulus_based {
356            ZombieRole::CumulusCollator
357        } else {
358            ZombieRole::Collator
359        };
360        let chain_context = ChainDefaultContext {
361            default_command: spec.default_command.as_ref(),
362            default_image: spec.default_image.as_ref(),
363            default_resources: spec.default_resources.as_ref(),
364            default_db_snapshot: spec.default_db_snapshot.as_ref(),
365            default_args: spec.default_args.iter().collect(),
366        };
367
368        let relay_chain = self.relaychain().chain.clone();
369        let relay_chain_id = self.relaychain().chain_id.clone();
370
371        let parachain = self
372            .parachains
373            .get_mut(&para_id)
374            .ok_or(anyhow::anyhow!(format!("parachain: {para_id} not found!")))?
375            .get_mut(0)
376            .ok_or(anyhow::anyhow!(format!("parachain: {para_id} not found!")))?;
377
378        let base_dir = self.ns.base_dir().to_string_lossy();
379        let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
380
381        let relaychain_spec_path = if let Some(chain_spec_custom_path) = &options.chain_spec_relay {
382            chain_spec_custom_path.clone()
383        } else {
384            PathBuf::from(format!(
385                "{}/{}.json",
386                self.ns.base_dir().to_string_lossy(),
387                relay_chain
388            ))
389        };
390
391        let mut global_files_to_inject = vec![TransferedFile::new(
392            relaychain_spec_path,
393            PathBuf::from(format!("/cfg/{}.json", relay_chain)),
394        )];
395
396        let para_chain_spec_local_path = if let Some(para_chain_spec_custom) = &options.chain_spec {
397            Some(para_chain_spec_custom.clone())
398        } else if let Some(para_spec_path) = &parachain.chain_spec_path {
399            Some(PathBuf::from(format!(
400                "{}/{}",
401                self.ns.base_dir().to_string_lossy(),
402                para_spec_path.to_string_lossy()
403            )))
404        } else {
405            None
406        };
407
408        if let Some(para_spec_path) = para_chain_spec_local_path {
409            global_files_to_inject.push(TransferedFile::new(
410                para_spec_path,
411                PathBuf::from(format!("/cfg/{para_id}.json")),
412            ));
413        }
414
415        let mut node_spec = network_spec::node::NodeSpec::from_ad_hoc(
416            name,
417            options.into(),
418            &chain_context,
419            true,
420            spec.is_evm_based,
421        )?;
422
423        node_spec.available_args_output = Some(
424            self.initial_spec
425                .node_available_args_output(&node_spec, self.ns.clone())
426                .await?,
427        );
428
429        let resolved_db_snapshots = generators::resolve_db_snapshots(
430            std::iter::once(&node_spec),
431            &self.ns,
432            &self.filesystem,
433        )
434        .await?;
435
436        // TODO: we want to still supporting spawn a dedicated bootnode??
437        let ctx = SpawnNodeCtx {
438            chain_id: &relay_chain_id,
439            parachain_id: parachain.chain_id.as_deref(),
440            chain: &relay_chain,
441            role,
442            ns: &self.ns,
443            scoped_fs: &scoped_fs,
444            parachain: Some(spec),
445            bootnodes_addr: &vec![],
446            wait_ready: true,
447            nodes_by_name: serde_json::to_value(&self.nodes_by_name)?,
448            global_settings: &self.initial_spec.global_settings,
449            resolved_db_snapshots: &resolved_db_snapshots,
450        };
451
452        let node = spawner::spawn_node(&node_spec, global_files_to_inject, &ctx).await?;
453
454        // Let's make sure node is up before adding
455        node.wait_until_is_up(self.initial_spec.global_settings.network_spawn_timeout())
456            .await?;
457
458        self.add_running_node(node, Some(para_id)).await;
459
460        // Dump zombie.json
461        self.write_zombie_json().await?;
462
463        generators::cleanup_db_snapshot_cache(&resolved_db_snapshots).await;
464
465        Ok(())
466    }
467
468    /// Get a parachain config builder from a running network
469    ///
470    /// This allow you to build a new parachain config to be deployed into
471    /// the running network.
472    pub fn para_config_builder(&self) -> ParachainConfigBuilder<Initial, Running> {
473        let used_ports = self
474            .nodes_iter()
475            .map(|node| node.spec())
476            .flat_map(|spec| {
477                [
478                    spec.ws_port.0,
479                    spec.rpc_port.0,
480                    spec.prometheus_port.0,
481                    spec.p2p_port.0,
482                ]
483            })
484            .collect();
485
486        let used_nodes_names = self.nodes_by_name.keys().cloned().collect();
487
488        // need to inverse logic of generate_unique_para_id
489        let used_para_ids = self
490            .parachains
491            .iter()
492            .map(|(id, paras)| (*id, paras.len().saturating_sub(1) as u8))
493            .collect();
494
495        let context = ValidationContext {
496            used_ports,
497            used_nodes_names,
498            used_para_ids,
499        };
500        let context = Rc::new(RefCell::new(context));
501
502        ParachainConfigBuilder::new_with_running(context)
503    }
504
505    /// Add a new parachain to the running network
506    ///
507    /// # Arguments
508    /// * `para_config` - Parachain configuration to deploy
509    /// * `custom_relaychain_spec` - Optional path to a custom relaychain spec to use
510    /// * `custom_parchain_fs_prefix` - Optional prefix to use when artifacts are created
511    ///
512    ///
513    /// # Example:
514    /// ```rust
515    /// # use anyhow::anyhow;
516    /// # use provider::NativeProvider;
517    /// # use support::{fs::local::LocalFileSystem};
518    /// # use zombienet_orchestrator::{errors, AddCollatorOptions, Orchestrator};
519    /// # use configuration::NetworkConfig;
520    /// # async fn example() -> Result<(), anyhow::Error> {
521    /// #   let provider = NativeProvider::new(LocalFileSystem {});
522    /// #   let orchestrator = Orchestrator::new(LocalFileSystem {}, provider);
523    /// #   let config = NetworkConfig::load_from_toml("config.toml")?;
524    /// let mut network = orchestrator.spawn(config).await?;
525    /// let para_config = network
526    ///     .para_config_builder()
527    ///     .with_id(100)
528    ///     .with_default_command("polkadot-parachain")
529    ///     .with_collator(|c| c.with_name("col-100-1"))
530    ///     .build()
531    ///     .map_err(|_e| anyhow!("Building config"))?;
532    ///
533    /// network.add_parachain(&para_config, None, None).await?;
534    ///
535    /// #   Ok(())
536    /// # }
537    /// ```
538    pub async fn add_parachain(
539        &mut self,
540        para_config: &ParachainConfig,
541        custom_relaychain_spec: Option<PathBuf>,
542        custom_parchain_fs_prefix: Option<String>,
543    ) -> Result<(), anyhow::Error> {
544        let base_dir = self.ns.base_dir().to_string_lossy().to_string();
545        let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
546
547        let mut global_files_to_inject = vec![];
548
549        // get relaychain id
550        let relay_chain_id = if let Some(custom_path) = custom_relaychain_spec {
551            // use this file as relaychain spec
552            global_files_to_inject.push(TransferedFile::new(
553                custom_path.clone(),
554                PathBuf::from(format!("/cfg/{}.json", self.relaychain().chain)),
555            ));
556            let content = std::fs::read_to_string(custom_path)?;
557            ChainSpec::chain_id_from_spec(&content)?
558        } else {
559            global_files_to_inject.push(TransferedFile::new(
560                PathBuf::from(format!(
561                    "{}/{}",
562                    scoped_fs.base_dir,
563                    self.relaychain().chain_spec_path.to_string_lossy()
564                )),
565                PathBuf::from(format!("/cfg/{}.json", self.relaychain().chain)),
566            ));
567            self.relaychain().chain_id.clone()
568        };
569
570        let mut para_spec = network_spec::parachain::ParachainSpec::from_config(
571            para_config,
572            relay_chain_id.as_str().try_into()?,
573        )?;
574
575        let chain_spec_raw_path = para_spec
576            .build_chain_spec(
577                &relay_chain_id,
578                &self.ns,
579                &scoped_fs,
580                para_spec.post_process_script.clone().as_deref(),
581            )
582            .await?;
583
584        // Para artifacts
585        let para_path_prefix = if let Some(custom_prefix) = custom_parchain_fs_prefix {
586            custom_prefix
587        } else {
588            para_spec.id.to_string()
589        };
590
591        scoped_fs.create_dir(&para_path_prefix).await?;
592        // create wasm/state
593        para_spec
594            .genesis_state
595            .build(
596                chain_spec_raw_path.as_ref(),
597                format!("{}/genesis-state", para_path_prefix),
598                &self.ns,
599                &scoped_fs,
600                None,
601            )
602            .await?;
603        para_spec
604            .genesis_wasm
605            .build(
606                chain_spec_raw_path.as_ref(),
607                format!("{}/para_spec-wasm", para_path_prefix),
608                &self.ns,
609                &scoped_fs,
610                None,
611            )
612            .await?;
613
614        let parachain =
615            Parachain::from_spec(&para_spec, &global_files_to_inject, &scoped_fs).await?;
616        let parachain_id = parachain.chain_id.clone();
617
618        let resolved_db_snapshots = generators::resolve_db_snapshots(
619            para_spec.collators.iter(),
620            &self.ns,
621            &self.filesystem,
622        )
623        .await?;
624
625        // Create `ctx` for spawn the nodes
626        let ctx_para = SpawnNodeCtx {
627            parachain: Some(&para_spec),
628            parachain_id: parachain_id.as_deref(),
629            role: if para_spec.is_cumulus_based {
630                ZombieRole::CumulusCollator
631            } else {
632                ZombieRole::Collator
633            },
634            bootnodes_addr: &para_config
635                .bootnodes_addresses()
636                .iter()
637                .map(|&a| a.to_string())
638                .collect(),
639            chain_id: &self.relaychain().chain_id,
640            chain: &self.relaychain().chain,
641            ns: &self.ns,
642            scoped_fs: &scoped_fs,
643            wait_ready: false,
644            nodes_by_name: serde_json::to_value(&self.nodes_by_name)?,
645            global_settings: &self.initial_spec.global_settings,
646            resolved_db_snapshots: &resolved_db_snapshots,
647        };
648
649        // Register the parachain to the running network
650        let first_node_url = self
651            .relaychain()
652            .nodes
653            .first()
654            .ok_or(anyhow::anyhow!(
655                "At least one node of the relaychain should be running"
656            ))?
657            .ws_uri();
658
659        if para_config.registration_strategy() == Some(&RegistrationStrategy::UsingExtrinsic) {
660            let register_para_options = RegisterParachainOptions {
661                id: parachain.para_id,
662                // This needs to resolve correctly
663                wasm_path: para_spec
664                    .genesis_wasm
665                    .artifact_path()
666                    .ok_or(anyhow::anyhow!(
667                        "artifact path for wasm must be set at this point",
668                    ))?
669                    .to_path_buf(),
670                state_path: para_spec
671                    .genesis_state
672                    .artifact_path()
673                    .ok_or(anyhow::anyhow!(
674                        "artifact path for state must be set at this point",
675                    ))?
676                    .to_path_buf(),
677                node_ws_url: first_node_url.to_string(),
678                onboard_as_para: para_spec.onboard_as_parachain,
679                seed: None, // TODO: Seed is passed by?
680                finalization: false,
681            };
682
683            Parachain::register(register_para_options, &scoped_fs).await?;
684        }
685
686        // Spawn the nodes
687        let spawning_tasks = para_spec
688            .collators
689            .iter()
690            .map(|node| spawner::spawn_node(node, parachain.files_to_inject.clone(), &ctx_para));
691
692        let running_nodes = futures::future::try_join_all(spawning_tasks).await?;
693
694        // Let's make sure nodes are up before adding them
695        let waiting_tasks = running_nodes.iter().map(|node| {
696            node.wait_until_is_up(self.initial_spec.global_settings.network_spawn_timeout())
697        });
698
699        let _ = futures::future::try_join_all(waiting_tasks).await?;
700
701        let running_para_id = parachain.para_id;
702        self.add_para(parachain);
703        for node in running_nodes {
704            self.add_running_node(node, Some(running_para_id)).await;
705        }
706
707        // Dump zombie.json
708        self.write_zombie_json().await?;
709
710        generators::cleanup_db_snapshot_cache(&resolved_db_snapshots).await;
711
712        Ok(())
713    }
714
715    /// Register a parachain, which has already been added to the network (with manual registration
716    /// strategy)
717    ///
718    /// # Arguments
719    /// * `para_id` - Parachain Id
720    ///
721    ///
722    /// # Example:
723    /// ```rust
724    /// # use anyhow::anyhow;
725    /// # use provider::NativeProvider;
726    /// # use support::{fs::local::LocalFileSystem};
727    /// # use zombienet_orchestrator::Orchestrator;
728    /// # use configuration::{NetworkConfig, NetworkConfigBuilder, RegistrationStrategy};
729    /// # async fn example() -> Result<(), anyhow::Error> {
730    /// #   let provider = NativeProvider::new(LocalFileSystem {});
731    /// #   let orchestrator = Orchestrator::new(LocalFileSystem {}, provider);
732    /// #   let config = NetworkConfigBuilder::new()
733    /// #     .with_relaychain(|r| {
734    /// #       r.with_chain("rococo-local")
735    /// #         .with_default_command("polkadot")
736    /// #         .with_node(|node| node.with_name("alice"))
737    /// #     })
738    /// #     .with_parachain(|p| {
739    /// #       p.with_id(100)
740    /// #         .with_registration_strategy(RegistrationStrategy::Manual)
741    /// #         .with_default_command("test-parachain")
742    /// #         .with_collator(|n| n.with_name("dave").validator(false))
743    /// #     })
744    /// #     .build()
745    /// #     .map_err(|_e| anyhow!("Building config"))?;
746    /// let mut network = orchestrator.spawn(config).await?;
747    ///
748    /// network.register_parachain(100).await?;
749    ///
750    /// #   Ok(())
751    /// # }
752    /// ```
753    pub async fn register_parachain(&mut self, para_id: u32) -> Result<(), anyhow::Error> {
754        let para = self
755            .initial_spec
756            .parachains
757            .iter()
758            .find(|p| p.id == para_id)
759            .ok_or(anyhow::anyhow!(
760                "no parachain with id = {para_id} available",
761            ))?;
762        let para_genesis_config = para.get_genesis_config()?;
763        let first_node_url = self
764            .relaychain()
765            .nodes
766            .first()
767            .ok_or(anyhow::anyhow!(
768                "At least one node of the relaychain should be running"
769            ))?
770            .ws_uri();
771        let register_para_options: RegisterParachainOptions = RegisterParachainOptions {
772            id: para_id,
773            // This needs to resolve correctly
774            wasm_path: para_genesis_config.wasm_path.clone(),
775            state_path: para_genesis_config.state_path.clone(),
776            node_ws_url: first_node_url.to_string(),
777            onboard_as_para: para_genesis_config.as_parachain,
778            seed: None, // TODO: Seed is passed by?
779            finalization: false,
780        };
781        let base_dir = self.ns.base_dir().to_string_lossy().to_string();
782        let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
783        Parachain::register(register_para_options, &scoped_fs).await?;
784
785        Ok(())
786    }
787
788    // deregister and stop the collator?
789    // remove_parachain()
790
791    /// Get a node of a specific kind by name, downcasting it from the registry.
792    fn get_node_as<'a, N: SpawnedNode>(&'a self, name: &str) -> Result<&'a N, anyhow::Error> {
793        let node = self.nodes_by_name.get(name).ok_or_else(|| {
794            anyhow::anyhow!(
795                "can't find node with name: {name:?}, should be one of {}",
796                self.node_names().join(", ")
797            )
798        })?;
799
800        // `dyn SpawnedNode` upcasts to `dyn Any` (Rust >= 1.86), no `as_any` needed.
801        let node_any: &dyn std::any::Any = node.as_ref();
802        node_any.downcast_ref::<N>().ok_or_else(|| {
803            anyhow::anyhow!(
804                "node {name:?} is a '{}' node, it can't be used as a {}",
805                node.kind(),
806                std::any::type_name::<N>()
807            )
808        })
809    }
810
811    /// Get a substrate node (relaychain node or collator) by name.
812    pub fn get_node(&self, name: impl Into<String>) -> Result<&NetworkNode, anyhow::Error> {
813        self.get_node_as::<NetworkNode>(&name.into())
814    }
815
816    /// Get a JAM node by name.
817    pub fn get_jam_node(&self, name: impl Into<String>) -> Result<&JamNetworkNode, anyhow::Error> {
818        self.get_node_as::<JamNetworkNode>(&name.into())
819    }
820
821    /// Get any node by name, without caring about its kind.
822    ///
823    /// Only the behaviour shared by every node ([`SpawnedNode`]) is available
824    /// on the returned reference.
825    pub fn get_any_node(&self, name: impl Into<String>) -> Result<&dyn SpawnedNode, anyhow::Error> {
826        let name = name.into();
827        self.nodes_by_name
828            .get(&name)
829            .map(|node| node.as_ref())
830            .ok_or_else(|| {
831                anyhow::anyhow!(
832                    "can't find node with name: {name:?}, should be one of {}",
833                    self.node_names().join(", ")
834                )
835            })
836    }
837
838    pub fn node_names(&self) -> Vec<String> {
839        self.nodes_by_name.keys().cloned().collect::<Vec<_>>()
840    }
841
842    /// All the substrate nodes (relaychain nodes and collators) of the network.
843    pub fn nodes(&self) -> Vec<&NetworkNode> {
844        self.nodes_iter().collect()
845    }
846
847    /// All the nodes of the network, of every kind.
848    pub fn all_nodes(&self) -> Vec<&dyn SpawnedNode> {
849        self.nodes_by_name.values().map(|n| n.as_ref()).collect()
850    }
851
852    /// All the nodes of the given kind.
853    pub fn nodes_of_kind(&self, kind: NodeKind) -> Vec<&dyn SpawnedNode> {
854        self.nodes_by_name
855            .values()
856            .filter(|n| n.kind() == kind)
857            .map(|n| n.as_ref())
858            .collect()
859    }
860
861    pub async fn detach(&self) {
862        self.ns.detach().await
863    }
864
865    // Internal API
866    pub(crate) async fn add_running_node(&mut self, node: NetworkNode, para_id: Option<u32>) {
867        let node = Arc::new(node);
868        if let Some(para_id) = para_id {
869            if let Some(para) = self.parachains.get_mut(&para_id).and_then(|p| p.get_mut(0)) {
870                para.collators.push(node.clone());
871            } else {
872                // is the first node of the para, let create the entry
873                unreachable!()
874            }
875        } else {
876            self.relaychain_mut().nodes.push(node.clone());
877        }
878
879        self.register_running_node(node).await;
880    }
881
882    /// Add an already spawned JAM node to the jamchain and to the registry.
883    pub(crate) async fn add_running_jam_node(&mut self, node: JamNetworkNode) {
884        let node = Arc::new(node);
885        if let Some(jamchain) = self.jamchain.as_mut() {
886            jamchain.nodes.push(node.clone());
887        } else {
888            // the jamchain should be set before adding any of its nodes
889            unreachable!()
890        }
891
892        self.register_running_node(node).await;
893    }
894
895    /// Mark a freshly spawned node as running and register it (as the very
896    /// same `Arc` the typed collection holds) for lookup and monitoring.
897    async fn register_running_node(&mut self, node: Arc<impl SpawnedNode>) {
898        node.core().set_is_running(true);
899        node.core().set_last_start_ts(
900            SystemTime::now()
901                .duration_since(UNIX_EPOCH)
902                .expect("Timestamp should be valid")
903                .as_secs(),
904        );
905
906        let node: Arc<dyn SpawnedNode> = node;
907        self.nodes_by_name
908            .insert(node.name().to_string(), node.clone());
909        self.nodes_to_watch.write().await.push(node);
910    }
911
912    pub(crate) fn add_para(&mut self, para: Parachain) {
913        self.parachains.entry(para.para_id).or_default().push(para);
914    }
915
916    pub(crate) async fn write_zombie_json(&self) -> Result<(), anyhow::Error> {
917        let base_dir = self.ns.base_dir().to_string_lossy();
918        let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
919        let ns_name = self.ns.name();
920
921        write_zombie_json(serde_json::to_value(self)?, scoped_fs, ns_name).await?;
922        Ok(())
923    }
924
925    pub fn name(&self) -> &str {
926        self.ns.name()
927    }
928
929    /// Get a first parachain from the list of the parachains with specified id.
930    /// NOTE!
931    /// Usually the list will contain only one parachain.
932    /// Multiple parachains with the same id is a corner case.
933    /// If this is the case then one can get such parachain with
934    /// `parachain_by_unique_id()` method
935    ///
936    /// # Arguments
937    /// * `para_id` - Parachain Id
938    pub fn parachain(&self, para_id: u32) -> Option<&Parachain> {
939        self.parachains.get(&para_id)?.first()
940    }
941
942    /// Get a parachain by its unique id.
943    ///
944    /// This is particularly useful if there are multiple parachains
945    /// with the same id (this is a rare corner case).
946    ///
947    /// # Arguments
948    /// * `unique_id` - unique id of the parachain
949    pub fn parachain_by_unique_id(&self, unique_id: impl AsRef<str>) -> Option<&Parachain> {
950        self.parachains
951            .values()
952            .flat_map(|p| p.iter())
953            .find(|p| p.unique_id == unique_id.as_ref())
954    }
955
956    pub fn parachains(&self) -> Vec<&Parachain> {
957        self.parachains.values().flatten().collect()
958    }
959
960    pub(crate) fn nodes_iter(&self) -> impl Iterator<Item = &NetworkNode> {
961        self.relay
962            .iter()
963            .flat_map(|relay| relay.nodes.iter())
964            .chain(
965                self.parachains
966                    .values()
967                    .flat_map(|p| p.iter())
968                    .flat_map(|p| &p.collators),
969            )
970            .map(|n| n.as_ref())
971    }
972
973    /// Waits given number of seconds until all nodes in the network report that they are
974    /// up and running.
975    ///
976    /// # Arguments
977    /// * `timeout_secs` - The number of seconds to wait.
978    ///
979    /// # Returns
980    /// * `Ok()` if the node is up before timeout occured.
981    /// * `Err(e)` if timeout or other error occurred while waiting.
982    pub async fn wait_until_is_up(&self, timeout_secs: u64) -> Result<(), anyhow::Error> {
983        let handles = self
984            .nodes_by_name
985            .values()
986            .map(|node| node.wait_until_is_up(timeout_secs));
987
988        futures::future::try_join_all(handles).await?;
989
990        Ok(())
991    }
992
993    /// Pause every node in the network (SIGSTOP). Issued in parallel.
994    ///
995    /// Note: with the native provider on an attached network, the live
996    /// network has to be running with global setting `teardown_on_failure`
997    /// disabled.
998    pub async fn pause(&self) -> Result<(), anyhow::Error> {
999        futures::future::try_join_all(self.nodes_iter().map(|n| n.pause())).await?;
1000        Ok(())
1001    }
1002
1003    /// Resume every node in the network (SIGCONT). Issued in parallel.
1004    ///
1005    /// Note: with the native provider on an attached network, the live
1006    /// network has to be running with global setting `teardown_on_failure`
1007    /// disabled.
1008    pub async fn resume(&self) -> Result<(), anyhow::Error> {
1009        futures::future::try_join_all(self.nodes_iter().map(|n| n.resume())).await?;
1010        Ok(())
1011    }
1012
1013    /// Start the observability stack (Prometheus + Grafana) as an add-on
1014    ///
1015    /// This can be called on any running network — whether freshly spawned or
1016    /// re-attached via [`Orchestrator::attach_to_live`]. If observability is
1017    /// already running, it will be stopped first
1018    ///
1019    /// # Example:
1020    /// ```rust
1021    /// # use provider::NativeProvider;
1022    /// # use support::{fs::local::LocalFileSystem};
1023    /// # use zombienet_orchestrator::{errors, Orchestrator};
1024    /// # use configuration::{NetworkConfig, ObservabilityConfigBuilder};
1025    /// # async fn example() -> Result<(), errors::OrchestratorError> {
1026    /// #   let provider = NativeProvider::new(LocalFileSystem {});
1027    /// #   let orchestrator = Orchestrator::new(LocalFileSystem {}, provider);
1028    /// #   let config = NetworkConfig::load_from_toml("config.toml")?;
1029    /// let mut network = orchestrator.spawn(config).await?;
1030    ///
1031    /// let obs_config = ObservabilityConfigBuilder::new()
1032    ///     .with_enabled(true)
1033    ///     .with_grafana_port(3000)
1034    ///     .build();
1035    ///
1036    /// let info = network.start_observability(&obs_config).await?;
1037    /// println!("Grafana: {}", info.grafana_url);
1038    /// #   Ok(())
1039    /// # }
1040    /// ```
1041    pub async fn start_observability(
1042        &mut self,
1043        config: &configuration::ObservabilityConfig,
1044    ) -> Result<&ObservabilityInfo, anyhow::Error> {
1045        if self.observability().is_some() {
1046            self.stop_observability().await?;
1047        }
1048
1049        let nodes = self.nodes();
1050        let info = observability::spawn_observability_stack(
1051            config,
1052            &nodes,
1053            self.ns.name(),
1054            self.ns.base_dir(),
1055            &self.filesystem,
1056        )
1057        .await?;
1058
1059        self.observability = ObservabilityState::Running(info);
1060        self.observability()
1061            .ok_or_else(|| anyhow::anyhow!("observability state was just set but is not running"))
1062    }
1063
1064    /// Stop the observability stack if running
1065    ///
1066    /// Removes the Prometheus and Grafana containers. This is safe to call
1067    /// even if no observability stack is running (it will be a no-op)
1068    pub async fn stop_observability(&mut self) -> Result<(), anyhow::Error> {
1069        if let ObservabilityState::Running(info) =
1070            std::mem::replace(&mut self.observability, ObservabilityState::Stopped)
1071        {
1072            observability::cleanup_observability_stack(&info).await?;
1073        }
1074        Ok(())
1075    }
1076
1077    pub(crate) fn spawn_watching_task(&self) {
1078        let nodes_to_watch = Arc::clone(&self.nodes_to_watch);
1079        let ns = Arc::clone(&self.ns);
1080        let node_bootstrap_timeout = self.initial_spec.global_settings.node_spawn_timeout();
1081
1082        tokio::spawn(async move {
1083            loop {
1084                tokio::time::sleep(Duration::from_secs(NODE_MONITORING_INTERVAL_SECONDS)).await;
1085
1086                let guard = nodes_to_watch.read().await;
1087                let nodes = guard.iter().filter(|n| n.is_running()).collect::<Vec<_>>();
1088
1089                let all_running = {
1090                    let all_running =
1091                        futures::future::try_join_all(nodes.iter().map(|n| {
1092                            n.wait_until_is_up(NODE_MONITORING_FAILURE_THRESHOLD_SECONDS)
1093                        }))
1094                        .await;
1095
1096                    // Re-check `is_running` to make sure we don't kill the network unnecessarily
1097                    if nodes.iter().any(|n| !n.is_running()) {
1098                        continue;
1099                    } else {
1100                        all_running
1101                    }
1102                };
1103
1104                if let Err(e) = all_running {
1105                    // check if the node was restarted and we need to give it more time
1106                    if let Some(node) = nodes.iter().find(|n| n.name() == e.to_string()) {
1107                        let now = SystemTime::now()
1108                            .duration_since(UNIX_EPOCH)
1109                            .expect("get current ts should work.")
1110                            .as_secs();
1111                        if node_bootstrap_timeout as u64 > (now - node.last_start_ts()) {
1112                            warn!("[{}] still in bootstrap window from last starting ({}), continue waiting...", node.name(), node.last_start_ts());
1113                            continue;
1114                        }
1115                    }
1116
1117                    warn!("\n\t🧟 One of the nodes crashed: {e}. tearing the network down...");
1118
1119                    if let Err(e) = ns.destroy().await {
1120                        error!("an error occurred during network teardown: {}", e);
1121                    }
1122
1123                    std::process::exit(1);
1124                }
1125            }
1126        });
1127    }
1128
1129    pub(crate) fn set_parachains(&mut self, parachains: HashMap<u32, Vec<Parachain>>) {
1130        self.parachains = parachains;
1131    }
1132
1133    pub(crate) fn insert_node(&mut self, node: Arc<dyn SpawnedNode>) {
1134        self.nodes_by_name.insert(node.name().to_string(), node);
1135    }
1136
1137    pub(crate) fn set_jamchain(&mut self, jamchain: Jamchain) {
1138        self.jamchain = Some(jamchain);
1139    }
1140
1141    /// The JAM chain of this network, if any.
1142    pub fn jamchain(&self) -> Option<&Jamchain> {
1143        self.jamchain.as_ref()
1144    }
1145
1146    pub(crate) fn set_start_time_ts(&mut self, start_time: SystemTime) {
1147        if let Ok(start_time_ts) = start_time.duration_since(SystemTime::UNIX_EPOCH) {
1148            self.start_time_ts = Some(start_time_ts.as_millis().to_string());
1149        } else {
1150            // Just warn, do not propagate the err (this should not happens)
1151            warn!("⚠️ Error getting start_time timestamp");
1152        }
1153    }
1154}