Skip to main content

zombienet_orchestrator/
network_spec.rs

1use std::{
2    collections::{hash_map::Entry, HashMap, HashSet},
3    sync::Arc,
4};
5
6use configuration::{CustomProcess, GlobalSettings, HrmpChannelConfig, NetworkConfig};
7use futures::future::try_join_all;
8use provider::{DynNamespace, ProviderError, ProviderNamespace};
9use serde::{Deserialize, Serialize};
10use support::{
11    constants::{RELAY_NOT_NONE, THIS_IS_A_BUG},
12    fs::FileSystem,
13};
14use tracing::{debug, trace};
15
16use crate::{
17    errors::{merge_errs, OrchestratorError},
18    network_spec::jamchain::JamchainSpec,
19    ScopedFilesystem,
20};
21
22pub mod jamchain;
23pub mod jamnode;
24pub mod node;
25pub mod parachain;
26pub mod relaychain;
27
28use self::{node::NodeSpec, parachain::ParachainSpec, relaychain::RelaychainSpec};
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct NetworkSpec {
32    /// Relaychain configuration, `None` for a JAM network.
33    pub(crate) relaychain: Option<RelaychainSpec>,
34
35    /// Jamchain configuration, takes the place of the relaychain
36    pub(crate) jamchain: Option<JamchainSpec>,
37
38    /// Parachains configurations.
39    pub(crate) parachains: Vec<ParachainSpec>,
40
41    /// HRMP channels configurations.
42    pub(crate) hrmp_channels: Vec<HrmpChannelConfig>,
43
44    /// Global settings
45    pub(crate) global_settings: GlobalSettings,
46
47    /// Custom processes
48    #[serde(default)]
49    pub(crate) custom_processes: Vec<CustomProcess>,
50}
51
52impl NetworkSpec {
53    pub async fn from_config(
54        network_config: &NetworkConfig,
55    ) -> Result<NetworkSpec, OrchestratorError> {
56        let mut errs = vec![];
57        let relaychain = network_config
58            .try_relaychain()
59            .map(RelaychainSpec::from_config)
60            .transpose()?;
61        let jamchain = if let Some(jamchain_config) = network_config.jamchain() {
62            Some(JamchainSpec::from_config(jamchain_config)?)
63        } else {
64            None
65        };
66
67        // The chain the parachains are anchored to, either the relaychain or the JAM chain.
68        let root_chain = relaychain
69            .as_ref()
70            .map(|relaychain| relaychain.chain.clone())
71            .or_else(|| jamchain.as_ref().map(|jamchain| jamchain.id.clone()));
72
73        let mut parachains = vec![];
74
75        if let Some(root_chain) = root_chain {
76            // TODO: move to `fold` or map+fold
77            for para_config in network_config.parachains() {
78                match ParachainSpec::from_config(para_config, root_chain.clone()) {
79                    Ok(para) => parachains.push(para),
80                    Err(err) => errs.push(err),
81                }
82            }
83        }
84
85        // Processes and nodes share one registry on the running network. The
86        // names are final here (groups expanded, collisions between nodes
87        // resolved), so this is where a process taking one is caught.
88        let mut names: HashSet<&str> = relaychain
89            .iter()
90            .flat_map(|relay| relay.nodes.iter().map(|node| node.name.as_str()))
91            .chain(
92                jamchain
93                    .iter()
94                    .flat_map(|jam| jam.nodes.iter().map(|node| node.name.as_str())),
95            )
96            .chain(
97                parachains
98                    .iter()
99                    .flat_map(|para| para.collators.iter().map(|node| node.name.as_str())),
100            )
101            .collect();
102        for process in network_config.custom_processes() {
103            if !names.insert(process.name()) {
104                errs.push(OrchestratorError::InvalidConfig(format!(
105                    "custom_processes['{}'].name: already used by a node or custom process",
106                    process.name()
107                )));
108            }
109        }
110
111        if errs.is_empty() {
112            Ok(NetworkSpec {
113                relaychain,
114                jamchain,
115                parachains,
116                hrmp_channels: network_config
117                    .hrmp_channels()
118                    .into_iter()
119                    .cloned()
120                    .collect(),
121                global_settings: network_config.global_settings().clone(),
122                custom_processes: network_config
123                    .custom_processes()
124                    .into_iter()
125                    .cloned()
126                    .collect(),
127            })
128        } else {
129            Err(merge_errs(&errs))
130        }
131    }
132
133    pub async fn populate_nodes_available_args(
134        &mut self,
135        ns: Arc<dyn ProviderNamespace + Send + Sync>,
136    ) -> Result<(), OrchestratorError> {
137        let network_nodes = self.collect_network_nodes();
138
139        let mut image_command_to_nodes_mapping =
140            Self::create_image_command_to_nodes_mapping(network_nodes);
141
142        let available_args_outputs =
143            Self::retrieve_all_nodes_available_args_output(ns, &image_command_to_nodes_mapping)
144                .await?;
145
146        Self::update_nodes_available_args_output(
147            &mut image_command_to_nodes_mapping,
148            available_args_outputs,
149        );
150
151        Ok(())
152    }
153
154    //
155    pub async fn node_available_args_output(
156        &self,
157        node_spec: &NodeSpec,
158        ns: Arc<dyn ProviderNamespace + Send + Sync>,
159    ) -> Result<String, ProviderError> {
160        // try to find a node that use the same combination of image/cmd
161        let cmp_fn = |ad_hoc: &&NodeSpec| -> bool {
162            ad_hoc.image == node_spec.image && ad_hoc.command == node_spec.command
163        };
164
165        // check if we already had computed the args output for this cmd/[image]
166        let node = self
167            .relaychain
168            .iter()
169            .flat_map(|relaychain| relaychain.nodes.iter())
170            .find(cmp_fn);
171        let node = if let Some(node) = node {
172            Some(node)
173        } else {
174            let node = self
175                .parachains
176                .iter()
177                .find_map(|para| para.collators.iter().find(cmp_fn));
178
179            node
180        };
181
182        let output = if let Some(node) = node {
183            node.available_args_output.clone().expect(&format!(
184                "args_output should be set for running nodes {THIS_IS_A_BUG}"
185            ))
186        } else {
187            // we need to compute the args output
188            let image = node_spec
189                .image
190                .as_ref()
191                .map(|image| image.as_str().to_string());
192            let command = node_spec.command.as_str().to_string();
193
194            ns.get_node_available_args((command, image)).await?
195        };
196
197        Ok(output)
198    }
199
200    pub fn relaychain(&self) -> &RelaychainSpec {
201        self.relaychain
202            .as_ref()
203            .expect(&format!("{RELAY_NOT_NONE}, {THIS_IS_A_BUG}"))
204    }
205
206    pub fn relaychain_mut(&mut self) -> &mut RelaychainSpec {
207        self.relaychain
208            .as_mut()
209            .expect(&format!("{RELAY_NOT_NONE}, {THIS_IS_A_BUG}"))
210    }
211
212    /// The relaychain spec, `None` for a JAM network.
213    pub fn try_relaychain(&self) -> Option<&RelaychainSpec> {
214        self.relaychain.as_ref()
215    }
216
217    pub fn parachains_iter(&self) -> impl Iterator<Item = &ParachainSpec> {
218        self.parachains.iter()
219    }
220
221    pub fn parachains_iter_mut(&mut self) -> impl Iterator<Item = &mut ParachainSpec> {
222        self.parachains.iter_mut()
223    }
224
225    pub fn set_global_settings(&mut self, global_settings: GlobalSettings) {
226        self.global_settings = global_settings;
227    }
228
229    pub async fn build_parachain_artifacts<'a, T: FileSystem>(
230        &mut self,
231        ns: DynNamespace,
232        scoped_fs: &ScopedFilesystem<'a, T>,
233        relaychain_id: &str,
234        base_dir_exists: bool,
235    ) -> Result<(), anyhow::Error> {
236        for para in self.parachains.iter_mut() {
237            let chain_spec_raw_path = para
238                .build_chain_spec(
239                    relaychain_id,
240                    &ns,
241                    scoped_fs,
242                    para.post_process_script.clone().as_deref(),
243                )
244                .await?;
245
246            trace!("creating dirs for {}", &para.unique_id);
247            if base_dir_exists {
248                scoped_fs.create_dir_all(&para.unique_id).await?;
249            } else {
250                scoped_fs.create_dir(&para.unique_id).await?;
251            };
252            trace!("created dirs for {}", &para.unique_id);
253
254            // create wasm/state
255            para.genesis_state
256                .build(
257                    chain_spec_raw_path.clone(),
258                    format!("{}/genesis-state", para.unique_id),
259                    &ns,
260                    scoped_fs,
261                    None,
262                )
263                .await?;
264            debug!("parachain genesis state built!");
265            para.genesis_wasm
266                .build(
267                    chain_spec_raw_path,
268                    format!("{}/genesis-wasm", para.unique_id),
269                    &ns,
270                    scoped_fs,
271                    None,
272                )
273                .await?;
274            debug!("parachain genesis wasm built!");
275        }
276
277        Ok(())
278    }
279
280    // collect mutable references to all nodes from relaychain and parachains
281    fn collect_network_nodes(&mut self) -> Vec<&mut NodeSpec> {
282        vec![
283            self.relaychain
284                .iter_mut()
285                .flat_map(|relaychain| relaychain.nodes.iter_mut())
286                .collect::<Vec<_>>(),
287            self.parachains
288                .iter_mut()
289                .flat_map(|para| para.collators.iter_mut())
290                .collect(),
291        ]
292        .into_iter()
293        .flatten()
294        .collect::<Vec<_>>()
295    }
296
297    // initialize the mapping of all possible node image/commands to corresponding nodes
298    fn create_image_command_to_nodes_mapping(
299        network_nodes: Vec<&mut NodeSpec>,
300    ) -> HashMap<(Option<String>, String), Vec<&mut NodeSpec>> {
301        network_nodes.into_iter().fold(
302            HashMap::new(),
303            |mut acc: HashMap<(Option<String>, String), Vec<&mut node::NodeSpec>>, node| {
304                // build mapping key using image and command if image is present or command only
305                let key = node
306                    .image
307                    .as_ref()
308                    .map(|image| {
309                        (
310                            Some(image.as_str().to_string()),
311                            node.command.as_str().to_string(),
312                        )
313                    })
314                    .unwrap_or_else(|| (None, node.command.as_str().to_string()));
315
316                // append the node to the vector of nodes for this image/command tuple
317                if let Entry::Vacant(entry) = acc.entry(key.clone()) {
318                    entry.insert(vec![node]);
319                } else {
320                    acc.get_mut(&key).unwrap().push(node);
321                }
322
323                acc
324            },
325        )
326    }
327
328    async fn retrieve_all_nodes_available_args_output(
329        ns: Arc<dyn ProviderNamespace + Send + Sync>,
330        image_command_to_nodes_mapping: &HashMap<(Option<String>, String), Vec<&mut NodeSpec>>,
331    ) -> Result<Vec<(Option<String>, String, String)>, OrchestratorError> {
332        try_join_all(
333            image_command_to_nodes_mapping
334                .keys()
335                .map(|(image, command)| async {
336                    let image = image.clone();
337                    let command = command.clone();
338                    // get node available args output from image/command
339                    let available_args = ns
340                        .get_node_available_args((command.clone(), image.clone()))
341                        .await?;
342                    debug!(
343                        "retrieved available args for image: {:?}, command: {}",
344                        image, command
345                    );
346
347                    // map the result to include image and command
348                    Ok::<_, OrchestratorError>((image, command, available_args))
349                })
350                .collect::<Vec<_>>(),
351        )
352        .await
353    }
354
355    fn update_nodes_available_args_output(
356        image_command_to_nodes_mapping: &mut HashMap<(Option<String>, String), Vec<&mut NodeSpec>>,
357        available_args_outputs: Vec<(Option<String>, String, String)>,
358    ) {
359        for (image, command, available_args_output) in available_args_outputs {
360            let nodes = image_command_to_nodes_mapping
361                .get_mut(&(image, command))
362                .expect(&format!(
363                    "node image/command key should exist {THIS_IS_A_BUG}"
364                ));
365
366            for node in nodes {
367                node.available_args_output = Some(available_args_output.clone());
368            }
369        }
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use crate::generators::generate_node_port;
376
377    #[tokio::test]
378    async fn small_network_config_get_spec() {
379        use configuration::NetworkConfigBuilder;
380
381        use super::*;
382
383        let config = NetworkConfigBuilder::new()
384            .with_relaychain(|r| {
385                r.with_chain("rococo-local")
386                    .with_default_command("polkadot")
387                    .with_validator(|node| node.with_name("alice"))
388                    .with_fullnode(|node| node.with_name("bob").with_command("polkadot1"))
389            })
390            .with_parachain(|p| {
391                p.with_id(100)
392                    .with_default_command("adder-collator")
393                    .with_collator(|c| c.with_name("collator1"))
394            })
395            .with_custom_process(|c| c.with_name("eth-rpc").with_command("command"))
396            .build()
397            .unwrap();
398
399        let network_spec = NetworkSpec::from_config(&config).await.unwrap();
400        let alice = network_spec.relaychain().nodes.first().unwrap();
401        let bob = network_spec.relaychain().nodes.get(1).unwrap();
402        assert_eq!(alice.command.as_str(), "polkadot");
403        assert_eq!(bob.command.as_str(), "polkadot1");
404        assert!(alice.is_validator);
405        assert!(!bob.is_validator);
406        assert_eq!(network_spec.custom_processes.len(), 1);
407        let first_custom_process = network_spec.custom_processes.first().unwrap();
408        assert_eq!(first_custom_process.name(), "eth-rpc");
409
410        // paras
411        assert_eq!(network_spec.parachains.len(), 1);
412        let para_100 = network_spec.parachains.first().unwrap();
413        assert_eq!(para_100.id, 100);
414    }
415
416    #[tokio::test]
417    async fn jam_network_with_parachain_get_spec() {
418        use configuration::NetworkConfig;
419
420        use super::*;
421
422        let config = NetworkConfig::load_from_toml_string(
423            r#"
424[jamchain]
425id = "dev"
426default_command = "polkajam"
427
428[[jamchain.nodes]]
429name = "jam-or"
430mode = "ordinary"
431
432[[parachains]]
433id = 100
434default_command = "polkadot-omni-node"
435default_args = ["--jam-rpc-url http://{{ZOMBIE:jam-or:rpc_uri}}"]
436
437[[parachains.collators]]
438name = "collator"
439"#,
440        )
441        .unwrap();
442
443        let network_spec = NetworkSpec::from_config(&config).await.unwrap();
444
445        assert!(network_spec.try_relaychain().is_none());
446        assert_eq!(network_spec.jamchain.as_ref().unwrap().nodes.len(), 1);
447
448        // the parachain is anchored to the JAM chain
449        assert_eq!(network_spec.parachains.len(), 1);
450        let para = network_spec.parachains.first().unwrap();
451        assert_eq!(para.id, 100);
452        let collator = para.collators.first().unwrap();
453        assert_eq!(collator.command.as_str(), "polkadot-omni-node");
454    }
455
456    #[tokio::test]
457    async fn a_custom_process_may_not_take_the_name_of_any_node() {
458        use configuration::NetworkConfig;
459
460        use super::*;
461
462        // A JAM node, and a node whose final name comes from a group.
463        for (taken, toml) in [
464            (
465                "jam-or",
466                r#"
467[jamchain]
468id = "dev"
469default_command = "polkajam"
470
471[[jamchain.nodes]]
472name = "jam-or"
473mode = "ordinary"
474
475[[custom_processes]]
476name = "jam-or"
477command = "eth-rpc"
478"#,
479            ),
480            (
481                "eth-1",
482                r#"
483[relaychain]
484chain = "rococo-local"
485default_command = "polkadot"
486
487[[relaychain.node_groups]]
488name = "eth"
489count = 2
490
491[[custom_processes]]
492name = "eth-1"
493command = "eth-rpc"
494"#,
495            ),
496        ] {
497            let config = NetworkConfig::load_from_toml_string(toml).unwrap();
498            let err = NetworkSpec::from_config(&config).await.unwrap_err();
499            assert!(
500                err.to_string()
501                    .contains(&format!("custom_processes['{taken}'].name: already used")),
502                "{err}"
503            );
504        }
505    }
506
507    #[tokio::test]
508    async fn used_port_in_node_reports_err() {
509        use configuration::NetworkConfigBuilder;
510
511        use super::*;
512        let used_port = generate_node_port(None).unwrap();
513
514        let config = NetworkConfigBuilder::new()
515            .with_relaychain(|r| {
516                r.with_chain("rococo-local")
517                    .with_default_command("polkadot")
518                    .with_validator(|node| node.with_name("alice"))
519                    .with_fullnode(|node| node.with_name("bob").with_command("polkadot1"))
520            })
521            .with_parachain(|p| {
522                p.with_id(100)
523                    .with_default_command("adder-collator")
524                    .with_collator(|c| c.with_name("collator1").with_rpc_port(used_port.0))
525            })
526            .build()
527            .unwrap();
528
529        let network_spec_res = NetworkSpec::from_config(&config).await;
530        assert!(network_spec_res.is_err());
531        assert!(network_spec_res
532            .unwrap_err()
533            .to_string()
534            .contains("err Can\'t bind in socket"));
535    }
536
537    #[tokio::test]
538    async fn used_port_in_multiple_node_reports_err() {
539        use configuration::NetworkConfigBuilder;
540
541        use super::*;
542        let used_port_0 = generate_node_port(None).unwrap();
543        let used_port_1 = generate_node_port(None).unwrap();
544
545        let config = NetworkConfigBuilder::new()
546            .with_relaychain(|r| {
547                r.with_chain("rococo-local")
548                    .with_default_command("polkadot")
549                    .with_validator(|node| node.with_name("alice").with_rpc_port(used_port_0.0))
550                    .with_fullnode(|node| node.with_name("bob").with_rpc_port(used_port_1.0))
551            })
552            .with_parachain(|p| {
553                p.with_id(100)
554                    .with_default_command("adder-collator")
555                    .with_collator(|c| c.with_name("collator1"))
556            })
557            .build()
558            .unwrap();
559
560        let network_spec_res = NetworkSpec::from_config(&config).await;
561        assert!(network_spec_res.is_err());
562        // ensure we report 2 errors
563        let err_str = network_spec_res.unwrap_err().to_string();
564        let socket_errs: Vec<&str> = err_str.matches("err Can\'t bind in socket").collect();
565        assert_eq!(socket_errs.len(), 2);
566    }
567}