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 pub(crate) relaychain: Option<RelaychainSpec>,
34
35 pub(crate) jamchain: Option<JamchainSpec>,
37
38 pub(crate) parachains: Vec<ParachainSpec>,
40
41 pub(crate) hrmp_channels: Vec<HrmpChannelConfig>,
43
44 pub(crate) global_settings: GlobalSettings,
46
47 #[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 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 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 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 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 let cmp_fn = |ad_hoc: &&NodeSpec| -> bool {
162 ad_hoc.image == node_spec.image && ad_hoc.command == node_spec.command
163 };
164
165 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 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 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 {}", ¶.unique_id);
247 if base_dir_exists {
248 scoped_fs.create_dir_all(¶.unique_id).await?;
249 } else {
250 scoped_fs.create_dir(¶.unique_id).await?;
251 };
252 trace!("created dirs for {}", ¶.unique_id);
253
254 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 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 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 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 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 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 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 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 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 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 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}