1pub mod chain_upgrade;
2pub mod node;
3pub mod parachain;
4pub mod relaychain;
5
6use std::{collections::HashMap, path::PathBuf};
7
8use configuration::{
9 para_states::{Initial, Running},
10 shared::node::EnvVar,
11 types::{Arg, Command, Image, Port},
12 ParachainConfig, ParachainConfigBuilder, RegistrationStrategy,
13};
14use provider::{types::TransferedFile, DynNamespace, ProviderError};
15use serde::Serialize;
16use support::fs::FileSystem;
17
18use self::{node::NetworkNode, parachain::Parachain, relaychain::Relaychain};
19use crate::{
20 generators::chain_spec::ChainSpec,
21 network_spec::{self, NetworkSpec},
22 shared::{
23 macros,
24 types::{ChainDefaultContext, RegisterParachainOptions},
25 },
26 spawner::{self, SpawnNodeCtx},
27 ScopedFilesystem, ZombieRole,
28};
29
30#[derive(Serialize)]
31pub struct Network<T: FileSystem> {
32 #[serde(skip)]
33 ns: DynNamespace,
34 #[serde(skip)]
35 filesystem: T,
36 relay: Relaychain,
37 initial_spec: NetworkSpec,
38 parachains: HashMap<u32, Parachain>,
39 #[serde(skip)]
40 nodes_by_name: HashMap<String, NetworkNode>,
41}
42
43impl<T: FileSystem> std::fmt::Debug for Network<T> {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 f.debug_struct("Network")
46 .field("ns", &"ns_skipped")
47 .field("relay", &self.relay)
48 .field("initial_spec", &self.initial_spec)
49 .field("parachains", &self.parachains)
50 .field("nodes_by_name", &self.nodes_by_name)
51 .finish()
52 }
53}
54
55macros::create_add_options!(AddNodeOptions {
56 chain_spec: Option<PathBuf>
57});
58
59macros::create_add_options!(AddCollatorOptions {
60 chain_spec: Option<PathBuf>,
61 chain_spec_relay: Option<PathBuf>
62});
63
64impl<T: FileSystem> Network<T> {
65 pub(crate) fn new_with_relay(
66 relay: Relaychain,
67 ns: DynNamespace,
68 fs: T,
69 initial_spec: NetworkSpec,
70 ) -> Self {
71 Self {
72 ns,
73 filesystem: fs,
74 relay,
75 initial_spec,
76 parachains: Default::default(),
77 nodes_by_name: Default::default(),
78 }
79 }
80
81 pub fn ns_name(&self) -> String {
83 self.ns.name().to_string()
84 }
85
86 pub fn base_dir(&self) -> Option<&str> {
87 self.ns.base_dir().to_str()
88 }
89
90 pub fn relaychain(&self) -> &Relaychain {
91 &self.relay
92 }
93
94 pub async fn destroy(self) -> Result<(), ProviderError> {
96 self.ns.destroy().await
97 }
98
99 pub async fn add_node(
128 &mut self,
129 name: impl Into<String>,
130 options: AddNodeOptions,
131 ) -> Result<(), anyhow::Error> {
132 let name = name.into();
133 let relaychain = self.relaychain();
134
135 if self.nodes_iter().any(|n| n.name == name) {
136 return Err(anyhow::anyhow!("Name: {} is already used.", name));
137 }
138
139 let chain_spec_path = if let Some(chain_spec_custom_path) = &options.chain_spec {
140 chain_spec_custom_path.clone()
141 } else {
142 PathBuf::from(format!(
143 "{}/{}.json",
144 self.ns.base_dir().to_string_lossy(),
145 relaychain.chain
146 ))
147 };
148
149 let chain_context = ChainDefaultContext {
150 default_command: self.initial_spec.relaychain.default_command.as_ref(),
151 default_image: self.initial_spec.relaychain.default_image.as_ref(),
152 default_resources: self.initial_spec.relaychain.default_resources.as_ref(),
153 default_db_snapshot: self.initial_spec.relaychain.default_db_snapshot.as_ref(),
154 default_args: self.initial_spec.relaychain.default_args.iter().collect(),
155 };
156
157 let mut node_spec =
158 network_spec::node::NodeSpec::from_ad_hoc(&name, options.into(), &chain_context)?;
159
160 node_spec.available_args_output = Some(
161 self.initial_spec
162 .node_available_args_output(&node_spec, self.ns.clone())
163 .await?,
164 );
165
166 let base_dir = self.ns.base_dir().to_string_lossy();
167 let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
168
169 let ctx = SpawnNodeCtx {
170 chain_id: &relaychain.chain_id,
171 parachain_id: None,
172 chain: &relaychain.chain,
173 role: ZombieRole::Node,
174 ns: &self.ns,
175 scoped_fs: &scoped_fs,
176 parachain: None,
177 bootnodes_addr: &vec![],
178 wait_ready: true,
179 };
180
181 let global_files_to_inject = vec![TransferedFile::new(
182 chain_spec_path,
183 PathBuf::from(format!("/cfg/{}.json", relaychain.chain)),
184 )];
185
186 let node = spawner::spawn_node(&node_spec, global_files_to_inject, &ctx).await?;
187
188 self.add_running_node(node.clone(), None);
200
201 Ok(())
202 }
203
204 pub async fn add_collator(
230 &mut self,
231 name: impl Into<String>,
232 options: AddCollatorOptions,
233 para_id: u32,
234 ) -> Result<(), anyhow::Error> {
235 let spec = self
236 .initial_spec
237 .parachains
238 .iter()
239 .find(|para| para.id == para_id)
240 .ok_or(anyhow::anyhow!(format!("parachain: {para_id} not found!")))?;
241 let role = if spec.is_cumulus_based {
242 ZombieRole::CumulusCollator
243 } else {
244 ZombieRole::Collator
245 };
246 let chain_context = ChainDefaultContext {
247 default_command: spec.default_command.as_ref(),
248 default_image: spec.default_image.as_ref(),
249 default_resources: spec.default_resources.as_ref(),
250 default_db_snapshot: spec.default_db_snapshot.as_ref(),
251 default_args: spec.default_args.iter().collect(),
252 };
253 let parachain = self
254 .parachains
255 .get(¶_id)
256 .ok_or(anyhow::anyhow!(format!("parachain: {para_id} not found!")))?;
257
258 let base_dir = self.ns.base_dir().to_string_lossy();
259 let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
260
261 let ctx = SpawnNodeCtx {
263 chain_id: &self.relay.chain_id,
264 parachain_id: parachain.chain_id.as_deref(),
265 chain: &self.relay.chain,
266 role,
267 ns: &self.ns,
268 scoped_fs: &scoped_fs,
269 parachain: Some(spec),
270 bootnodes_addr: &vec![],
271 wait_ready: true,
272 };
273
274 let relaychain_spec_path = if let Some(chain_spec_custom_path) = &options.chain_spec_relay {
275 chain_spec_custom_path.clone()
276 } else {
277 PathBuf::from(format!(
278 "{}/{}.json",
279 self.ns.base_dir().to_string_lossy(),
280 self.relay.chain
281 ))
282 };
283
284 let mut global_files_to_inject = vec![TransferedFile::new(
285 relaychain_spec_path,
286 PathBuf::from(format!("/cfg/{}.json", self.relay.chain)),
287 )];
288
289 let para_chain_spec_local_path = if let Some(para_chain_spec_custom) = &options.chain_spec {
290 Some(para_chain_spec_custom.clone())
291 } else if let Some(para_spec_path) = ¶chain.chain_spec_path {
292 Some(PathBuf::from(format!(
293 "{}/{}",
294 self.ns.base_dir().to_string_lossy(),
295 para_spec_path.to_string_lossy()
296 )))
297 } else {
298 None
299 };
300
301 if let Some(para_spec_path) = para_chain_spec_local_path {
302 global_files_to_inject.push(TransferedFile::new(
303 para_spec_path,
304 PathBuf::from(format!("/cfg/{}.json", para_id)),
305 ));
306 }
307
308 let mut node_spec =
309 network_spec::node::NodeSpec::from_ad_hoc(name.into(), options.into(), &chain_context)?;
310
311 node_spec.available_args_output = Some(
312 self.initial_spec
313 .node_available_args_output(&node_spec, self.ns.clone())
314 .await?,
315 );
316
317 let node = spawner::spawn_node(&node_spec, global_files_to_inject, &ctx).await?;
318 let para = self.parachains.get_mut(¶_id).unwrap();
319 para.collators.push(node.clone());
320 self.add_running_node(node, None);
321
322 Ok(())
323 }
324
325 pub fn para_config_builder(&self) -> ParachainConfigBuilder<Initial, Running> {
330 ParachainConfigBuilder::new_with_running(Default::default())
332 }
333
334 pub async fn add_parachain(
368 &mut self,
369 para_config: &ParachainConfig,
370 custom_relaychain_spec: Option<PathBuf>,
371 custom_parchain_fs_prefix: Option<String>,
372 ) -> Result<(), anyhow::Error> {
373 let mut para_spec = network_spec::parachain::ParachainSpec::from_config(para_config)?;
375 let base_dir = self.ns.base_dir().to_string_lossy().to_string();
376 let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
377
378 let mut global_files_to_inject = vec![];
379
380 let relay_chain_id = if let Some(custom_path) = custom_relaychain_spec {
382 global_files_to_inject.push(TransferedFile::new(
384 custom_path.clone(),
385 PathBuf::from(format!("/cfg/{}.json", self.relaychain().chain)),
386 ));
387 let content = std::fs::read_to_string(custom_path)?;
388 ChainSpec::chain_id_from_spec(&content)?
389 } else {
390 global_files_to_inject.push(TransferedFile::new(
391 PathBuf::from(format!(
392 "{}/{}",
393 scoped_fs.base_dir,
394 self.relaychain().chain_spec_path.to_string_lossy()
395 )),
396 PathBuf::from(format!("/cfg/{}.json", self.relaychain().chain)),
397 ));
398 self.relay.chain_id.clone()
399 };
400
401 let chain_spec_raw_path = para_spec
402 .build_chain_spec(&relay_chain_id, &self.ns, &scoped_fs)
403 .await?;
404
405 let para_path_prefix = if let Some(custom_prefix) = custom_parchain_fs_prefix {
407 custom_prefix
408 } else {
409 para_spec.id.to_string()
410 };
411
412 scoped_fs.create_dir(¶_path_prefix).await?;
413 para_spec
415 .genesis_state
416 .build(
417 chain_spec_raw_path.as_ref(),
418 format!("{}/genesis-state", ¶_path_prefix),
419 &self.ns,
420 &scoped_fs,
421 )
422 .await?;
423 para_spec
424 .genesis_wasm
425 .build(
426 chain_spec_raw_path.as_ref(),
427 format!("{}/para_spec-wasm", ¶_path_prefix),
428 &self.ns,
429 &scoped_fs,
430 )
431 .await?;
432
433 let parachain =
434 Parachain::from_spec(¶_spec, &global_files_to_inject, &scoped_fs).await?;
435 let parachain_id = parachain.chain_id.clone();
436
437 let ctx_para = SpawnNodeCtx {
439 parachain: Some(¶_spec),
440 parachain_id: parachain_id.as_deref(),
441 role: if para_spec.is_cumulus_based {
442 ZombieRole::CumulusCollator
443 } else {
444 ZombieRole::Collator
445 },
446 bootnodes_addr: &vec![],
447 chain_id: &self.relaychain().chain_id,
448 chain: &self.relaychain().chain,
449 ns: &self.ns,
450 scoped_fs: &scoped_fs,
451 wait_ready: false,
452 };
453
454 let first_node_url = self
456 .relaychain()
457 .nodes
458 .first()
459 .ok_or(anyhow::anyhow!(
460 "At least one node of the relaychain should be running"
461 ))?
462 .ws_uri();
463
464 if para_config.registration_strategy() == Some(&RegistrationStrategy::UsingExtrinsic) {
465 let register_para_options = RegisterParachainOptions {
466 id: parachain.para_id,
467 wasm_path: para_spec
469 .genesis_wasm
470 .artifact_path()
471 .ok_or(anyhow::anyhow!(
472 "artifact path for wasm must be set at this point",
473 ))?
474 .to_path_buf(),
475 state_path: para_spec
476 .genesis_state
477 .artifact_path()
478 .ok_or(anyhow::anyhow!(
479 "artifact path for state must be set at this point",
480 ))?
481 .to_path_buf(),
482 node_ws_url: first_node_url.to_string(),
483 onboard_as_para: para_spec.onboard_as_parachain,
484 seed: None, finalization: false,
486 };
487
488 Parachain::register(register_para_options, &scoped_fs).await?;
489 }
490
491 let spawning_tasks = para_spec
493 .collators
494 .iter()
495 .map(|node| spawner::spawn_node(node, parachain.files_to_inject.clone(), &ctx_para));
496
497 let running_nodes = futures::future::try_join_all(spawning_tasks).await?;
498 let running_para_id = parachain.para_id;
499 self.add_para(parachain);
500 for node in running_nodes {
501 self.add_running_node(node, Some(running_para_id));
502 }
503
504 Ok(())
505 }
506
507 pub fn get_node(&self, name: impl Into<String>) -> Result<&NetworkNode, anyhow::Error> {
511 let name = name.into();
512 if let Some(node) = self.nodes_iter().find(|&n| n.name == name) {
513 return Ok(node);
514 }
515
516 let list = self
517 .nodes_iter()
518 .map(|n| &n.name)
519 .cloned()
520 .collect::<Vec<_>>()
521 .join(", ");
522
523 Err(anyhow::anyhow!(
524 "can't find node with name: {name:?}, should be one of {list}"
525 ))
526 }
527
528 pub fn get_node_mut(
529 &mut self,
530 name: impl Into<String>,
531 ) -> Result<&mut NetworkNode, anyhow::Error> {
532 let name = name.into();
533 self.nodes_iter_mut()
534 .find(|n| n.name == name)
535 .ok_or(anyhow::anyhow!("can't find node with name: {name:?}"))
536 }
537
538 pub fn nodes(&self) -> Vec<&NetworkNode> {
539 self.nodes_by_name.values().collect::<Vec<&NetworkNode>>()
540 }
541
542 pub async fn detach(&self) {
543 self.ns.detach().await
544 }
545
546 pub(crate) fn add_running_node(&mut self, node: NetworkNode, para_id: Option<u32>) {
548 if let Some(para_id) = para_id {
549 if let Some(para) = self.parachains.get_mut(¶_id) {
550 para.collators.push(node.clone());
551 } else {
552 unreachable!()
554 }
555 } else {
556 self.relay.nodes.push(node.clone());
557 }
558 let node_name = node.name.clone();
560 self.nodes_by_name.insert(node_name, node);
561 }
562
563 pub(crate) fn add_para(&mut self, para: Parachain) {
564 self.parachains.insert(para.para_id, para);
565 }
566
567 pub fn name(&self) -> &str {
568 self.ns.name()
569 }
570
571 pub fn parachain(&self, para_id: u32) -> Option<&Parachain> {
572 self.parachains.get(¶_id)
573 }
574
575 pub fn parachains(&self) -> Vec<&Parachain> {
576 self.parachains.values().collect()
577 }
578
579 pub(crate) fn nodes_iter(&self) -> impl Iterator<Item = &NetworkNode> {
580 self.relay
581 .nodes
582 .iter()
583 .chain(self.parachains.values().flat_map(|p| &p.collators))
584 }
585
586 pub(crate) fn nodes_iter_mut(&mut self) -> impl Iterator<Item = &mut NetworkNode> {
587 self.relay.nodes.iter_mut().chain(
588 self.parachains
589 .iter_mut()
590 .flat_map(|(_, p)| &mut p.collators),
591 )
592 }
593}