Skip to main content

zombienet_configuration/
relaychain.rs

1use std::{cell::RefCell, error::Error, fmt::Debug, marker::PhantomData, rc::Rc};
2
3use serde::{Deserialize, Serialize};
4use support::constants::{DEFAULT_TYPESTATE, THIS_IS_A_BUG};
5
6use crate::{
7    shared::{
8        errors::{ConfigError, FieldError},
9        helpers::{merge_errors, merge_errors_vecs},
10        macros::states,
11        node::{self, GroupNodeConfig, GroupNodeConfigBuilder, NodeConfig, NodeConfigBuilder},
12        resources::{Resources, ResourcesBuilder},
13        types::{
14            Arg, AssetLocation, Chain, ChainDefaultContext, Command, Image, ValidationContext,
15        },
16    },
17    types::{ChainSpecRuntime, JsonOverrides},
18    utils::{default_command_polkadot, default_relaychain_chain, is_false},
19};
20
21/// A relay chain configuration, composed of nodes and fine-grained configuration options.
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct RelaychainConfig {
24    #[serde(default = "default_relaychain_chain")]
25    chain: Chain,
26    #[serde(default = "default_command_polkadot")]
27    default_command: Option<Command>,
28    default_image: Option<Image>,
29    default_resources: Option<Resources>,
30    default_db_snapshot: Option<AssetLocation>,
31    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
32    default_args: Vec<Arg>,
33    /// chain-spec to use (location can be url or file path)
34    chain_spec_path: Option<AssetLocation>,
35    /// Full _template_ command, will be rendered (using custom token replacements)
36    /// and executed for generate the chain-spec.
37    /// available tokens {{chainName}} / {{disableBootnodes}}
38    chain_spec_command: Option<String>,
39    /// runtime to use for generating the chain-spec.
40    /// Location can be url or file path and an optional preset
41    chain_spec_runtime: Option<ChainSpecRuntime>,
42    #[serde(skip_serializing_if = "is_false", default)]
43    chain_spec_command_is_local: bool,
44    chain_spec_command_output_path: Option<String>,
45    random_nominators_count: Option<u32>,
46    max_nominations: Option<u8>,
47    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
48    nodes: Vec<NodeConfig>,
49    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
50    node_groups: Vec<GroupNodeConfig>,
51    #[serde(rename = "genesis", skip_serializing_if = "Option::is_none")]
52    runtime_genesis_patch: Option<serde_json::Value>,
53    // Path or url to override the runtime (:code) in the chain-spec
54    wasm_override: Option<AssetLocation>,
55    command: Option<Command>,
56    // Inline json or asset location to override raw chainspec
57    raw_spec_override: Option<JsonOverrides>,
58    /// Optional post-process script to run after chain-spec generation.
59    #[serde(skip_serializing_if = "Option::is_none")]
60    post_process_script: Option<String>,
61    #[serde(skip_serializing_if = "is_false", default)]
62    override_session_0: bool,
63}
64
65impl RelaychainConfig {
66    /// The chain name.
67    pub fn chain(&self) -> &Chain {
68        &self.chain
69    }
70
71    /// The default command used for nodes.
72    pub fn default_command(&self) -> Option<&Command> {
73        self.default_command.as_ref()
74    }
75
76    /// The default container image used for nodes.
77    pub fn default_image(&self) -> Option<&Image> {
78        self.default_image.as_ref()
79    }
80
81    /// The default resources limits used for nodes.
82    pub fn default_resources(&self) -> Option<&Resources> {
83        self.default_resources.as_ref()
84    }
85
86    /// The default database snapshot location that will be used for state.
87    pub fn default_db_snapshot(&self) -> Option<&AssetLocation> {
88        self.default_db_snapshot.as_ref()
89    }
90
91    /// The default arguments that will be used to launch the node command.
92    pub fn default_args(&self) -> Vec<&Arg> {
93        self.default_args.iter().collect::<Vec<&Arg>>()
94    }
95
96    /// The location of an pre-existing chain specification for the relay chain.
97    pub fn chain_spec_path(&self) -> Option<&AssetLocation> {
98        self.chain_spec_path.as_ref()
99    }
100
101    /// The location of a wasm runtime to override in the chain-spec.
102    pub fn wasm_override(&self) -> Option<&AssetLocation> {
103        self.wasm_override.as_ref()
104    }
105
106    /// The full _template_ command to genera the chain-spec
107    pub fn chain_spec_command(&self) -> Option<&str> {
108        self.chain_spec_command.as_deref()
109    }
110
111    /// Does the chain_spec_command needs to be run locally
112    pub fn chain_spec_command_is_local(&self) -> bool {
113        self.chain_spec_command_is_local
114    }
115
116    pub fn override_session_0(&self) -> bool {
117        self.override_session_0
118    }
119
120    /// The file where the `chain_spec_command` will write the chain-spec into.
121    /// Defaults to /dev/stdout.
122    pub fn chain_spec_command_output_path(&self) -> Option<&str> {
123        self.chain_spec_command_output_path.as_deref()
124    }
125
126    /// The non-default command used for nodes.
127    pub fn command(&self) -> Option<&Command> {
128        self.command.as_ref()
129    }
130
131    /// The number of `random nominators` to create for chains using staking, this is used in tandem with `max_nominations` to simulate the amount of nominators and nominations.
132    pub fn random_nominators_count(&self) -> Option<u32> {
133        self.random_nominators_count
134    }
135
136    /// The maximum number of nominations to create per nominator.
137    pub fn max_nominations(&self) -> Option<u8> {
138        self.max_nominations
139    }
140
141    /// The genesis overrides as a JSON value.
142    pub fn runtime_genesis_patch(&self) -> Option<&serde_json::Value> {
143        self.runtime_genesis_patch.as_ref()
144    }
145
146    /// The nodes of the relay chain.
147    pub fn nodes(&self) -> Vec<&NodeConfig> {
148        self.nodes.iter().collect::<Vec<&NodeConfig>>()
149    }
150
151    /// The group nodes of the relay chain.
152    pub fn group_node_configs(&self) -> Vec<&GroupNodeConfig> {
153        self.node_groups.iter().collect::<Vec<&GroupNodeConfig>>()
154    }
155
156    /// The location of a file or inline json to override raw chain-spec.
157    pub fn raw_spec_override(&self) -> Option<&JsonOverrides> {
158        self.raw_spec_override.as_ref()
159    }
160
161    /// Optional post-process script to run after chain-spec generation for this relaychain.
162    pub fn post_process_script(&self) -> Option<&str> {
163        self.post_process_script.as_deref()
164    }
165
166    /// Set the nodes to build
167    pub(crate) fn set_nodes(&mut self, nodes: Vec<NodeConfig>) {
168        self.nodes = nodes;
169    }
170
171    /// The location of runtime to use by chain-spec builder lib (from `sc-chain-spec` crate)
172    pub fn chain_spec_runtime(&self) -> Option<&ChainSpecRuntime> {
173        self.chain_spec_runtime.as_ref()
174    }
175}
176
177states! {
178    Initial,
179    WithChain,
180    WithAtLeastOneNode
181}
182
183/// A relay chain configuration builder, used to build a [`RelaychainConfig`] declaratively with fields validation.
184pub struct RelaychainConfigBuilder<State> {
185    config: RelaychainConfig,
186    validation_context: Rc<RefCell<ValidationContext>>,
187    errors: Vec<anyhow::Error>,
188    _state: PhantomData<State>,
189}
190
191impl Default for RelaychainConfigBuilder<Initial> {
192    fn default() -> Self {
193        Self {
194            config: RelaychainConfig {
195                chain: "default"
196                    .try_into()
197                    .expect(&format!("{DEFAULT_TYPESTATE} {THIS_IS_A_BUG}")),
198                default_command: None,
199                default_image: None,
200                default_resources: None,
201                default_db_snapshot: None,
202                default_args: vec![],
203                chain_spec_path: None,
204                chain_spec_command: None,
205                chain_spec_command_output_path: None,
206                chain_spec_runtime: None,
207                wasm_override: None,
208                chain_spec_command_is_local: false, // remote cmd by default
209                command: None,
210                random_nominators_count: None,
211                max_nominations: None,
212                runtime_genesis_patch: None,
213                nodes: vec![],
214                node_groups: vec![],
215                raw_spec_override: None,
216                post_process_script: None,
217                override_session_0: false,
218            },
219            validation_context: Default::default(),
220            errors: vec![],
221            _state: PhantomData,
222        }
223    }
224}
225
226impl<A> RelaychainConfigBuilder<A> {
227    fn transition<B>(
228        config: RelaychainConfig,
229        validation_context: Rc<RefCell<ValidationContext>>,
230        errors: Vec<anyhow::Error>,
231    ) -> RelaychainConfigBuilder<B> {
232        RelaychainConfigBuilder {
233            config,
234            validation_context,
235            errors,
236            _state: PhantomData,
237        }
238    }
239
240    fn default_chain_context(&self) -> ChainDefaultContext {
241        ChainDefaultContext {
242            default_command: self.config.default_command.clone(),
243            default_image: self.config.default_image.clone(),
244            default_resources: self.config.default_resources.clone(),
245            default_db_snapshot: self.config.default_db_snapshot.clone(),
246            default_args: self.config.default_args.clone(),
247        }
248    }
249
250    fn create_node_builder<F>(&self, f: F) -> NodeConfigBuilder<node::Buildable>
251    where
252        F: FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
253    {
254        f(NodeConfigBuilder::new(
255            self.default_chain_context(),
256            self.validation_context.clone(),
257        ))
258    }
259
260    /// Set an optional post-process script to run after chain-spec generation for this relaychain.
261    pub fn with_post_process_script(mut self, script: impl Into<String>) -> Self {
262        self.config.post_process_script = Some(script.into());
263        self
264    }
265}
266
267impl RelaychainConfigBuilder<Initial> {
268    pub fn new(
269        validation_context: Rc<RefCell<ValidationContext>>,
270    ) -> RelaychainConfigBuilder<Initial> {
271        Self {
272            validation_context,
273            ..Self::default()
274        }
275    }
276
277    /// Set the chain name (e.g. rococo-local).
278    pub fn with_chain<T>(self, chain: T) -> RelaychainConfigBuilder<WithChain>
279    where
280        T: TryInto<Chain>,
281        T::Error: Error + Send + Sync + 'static,
282    {
283        match chain.try_into() {
284            Ok(chain) => Self::transition(
285                RelaychainConfig {
286                    chain,
287                    ..self.config
288                },
289                self.validation_context,
290                self.errors,
291            ),
292            Err(error) => Self::transition(
293                self.config,
294                self.validation_context,
295                merge_errors(self.errors, FieldError::Chain(error.into()).into()),
296            ),
297        }
298    }
299}
300
301impl RelaychainConfigBuilder<WithChain> {
302    /// Set the default command used for nodes. Can be overridden.
303    pub fn with_default_command<T>(self, command: T) -> Self
304    where
305        T: TryInto<Command>,
306        T::Error: Error + Send + Sync + 'static,
307    {
308        match command.try_into() {
309            Ok(command) => Self::transition(
310                RelaychainConfig {
311                    default_command: Some(command),
312                    ..self.config
313                },
314                self.validation_context,
315                self.errors,
316            ),
317            Err(error) => Self::transition(
318                self.config,
319                self.validation_context,
320                merge_errors(self.errors, FieldError::DefaultCommand(error.into()).into()),
321            ),
322        }
323    }
324
325    /// Set the default container image used for nodes. Can be overridden.
326    pub fn with_default_image<T>(self, image: T) -> Self
327    where
328        T: TryInto<Image>,
329        T::Error: Error + Send + Sync + 'static,
330    {
331        match image.try_into() {
332            Ok(image) => Self::transition(
333                RelaychainConfig {
334                    default_image: Some(image),
335                    ..self.config
336                },
337                self.validation_context,
338                self.errors,
339            ),
340            Err(error) => Self::transition(
341                self.config,
342                self.validation_context,
343                merge_errors(self.errors, FieldError::DefaultImage(error.into()).into()),
344            ),
345        }
346    }
347
348    /// Set the default resources limits used for nodes. Can be overridden.
349    pub fn with_default_resources(
350        self,
351        f: impl FnOnce(ResourcesBuilder) -> ResourcesBuilder,
352    ) -> Self {
353        match f(ResourcesBuilder::new()).build() {
354            Ok(default_resources) => Self::transition(
355                RelaychainConfig {
356                    default_resources: Some(default_resources),
357                    ..self.config
358                },
359                self.validation_context,
360                self.errors,
361            ),
362            Err(errors) => Self::transition(
363                self.config,
364                self.validation_context,
365                merge_errors_vecs(
366                    self.errors,
367                    errors
368                        .into_iter()
369                        .map(|error| FieldError::DefaultResources(error).into())
370                        .collect::<Vec<_>>(),
371                ),
372            ),
373        }
374    }
375
376    /// Set the default database snapshot location that will be used for state. Can be overridden.
377    pub fn with_default_db_snapshot(self, location: impl Into<AssetLocation>) -> Self {
378        Self::transition(
379            RelaychainConfig {
380                default_db_snapshot: Some(location.into()),
381                ..self.config
382            },
383            self.validation_context,
384            self.errors,
385        )
386    }
387
388    /// Set the default arguments that will be used to execute the node command. Can be overridden.
389    pub fn with_default_args(self, args: Vec<Arg>) -> Self {
390        Self::transition(
391            RelaychainConfig {
392                default_args: args,
393                ..self.config
394            },
395            self.validation_context,
396            self.errors,
397        )
398    }
399
400    /// Set the location of a pre-existing chain specification for the relay chain.
401    pub fn with_chain_spec_path(self, location: impl Into<AssetLocation>) -> Self {
402        Self::transition(
403            RelaychainConfig {
404                chain_spec_path: Some(location.into()),
405                ..self.config
406            },
407            self.validation_context,
408            self.errors,
409        )
410    }
411
412    /// Set the location of a wasm to override the chain-spec.
413    pub fn with_wasm_override(self, location: impl Into<AssetLocation>) -> Self {
414        Self::transition(
415            RelaychainConfig {
416                wasm_override: Some(location.into()),
417                ..self.config
418            },
419            self.validation_context,
420            self.errors,
421        )
422    }
423
424    /// Set the chain-spec command _template_ for the relay chain.
425    pub fn with_chain_spec_command(self, cmd_template: impl Into<String>) -> Self {
426        Self::transition(
427            RelaychainConfig {
428                chain_spec_command: Some(cmd_template.into()),
429                ..self.config
430            },
431            self.validation_context,
432            self.errors,
433        )
434    }
435
436    /// Set the runtime path to use for generating the chain-spec and an optiona preset.
437    /// If the preset is not set, we will try to match [`local_testnet`, `development`, `dev`]
438    /// with the available ones and fallback to the default configuration as last option.
439    pub fn with_chain_spec_runtime(
440        self,
441        location: impl Into<AssetLocation>,
442        preset: Option<&str>,
443    ) -> Self {
444        let chain_spec_runtime = if let Some(preset) = preset {
445            ChainSpecRuntime::with_preset(location.into(), preset.to_string())
446        } else {
447            ChainSpecRuntime::new(location.into())
448        };
449        Self::transition(
450            RelaychainConfig {
451                chain_spec_runtime: Some(chain_spec_runtime),
452                ..self.config
453            },
454            self.validation_context,
455            self.errors,
456        )
457    }
458
459    /// Set if the chain-spec command needs to be run locally or not (false by default)
460    pub fn chain_spec_command_is_local(self, choice: bool) -> Self {
461        Self::transition(
462            RelaychainConfig {
463                chain_spec_command_is_local: choice,
464                ..self.config
465            },
466            self.validation_context,
467            self.errors,
468        )
469    }
470
471    /// Set to true to override session 0 and allow paras to produce
472    /// blocks from genesis.
473    pub fn with_override_session_0(self, choice: bool) -> Self {
474        Self::transition(
475            RelaychainConfig {
476                override_session_0: choice,
477                ..self.config
478            },
479            self.validation_context,
480            self.errors,
481        )
482    }
483
484    /// Set the output path for the chain-spec command.
485    pub fn with_chain_spec_command_output_path(self, output_path: &str) -> Self {
486        Self::transition(
487            RelaychainConfig {
488                chain_spec_command_output_path: Some(output_path.to_string()),
489                ..self.config
490            },
491            self.validation_context,
492            self.errors,
493        )
494    }
495
496    /// Set the number of `random nominators` to create for chains using staking, this is used in tandem with `max_nominations` to simulate the amount of nominators and nominations.
497    pub fn with_random_nominators_count(self, random_nominators_count: u32) -> Self {
498        Self::transition(
499            RelaychainConfig {
500                random_nominators_count: Some(random_nominators_count),
501                ..self.config
502            },
503            self.validation_context,
504            self.errors,
505        )
506    }
507
508    /// Set the maximum number of nominations to create per nominator.
509    pub fn with_max_nominations(self, max_nominations: u8) -> Self {
510        Self::transition(
511            RelaychainConfig {
512                max_nominations: Some(max_nominations),
513                ..self.config
514            },
515            self.validation_context,
516            self.errors,
517        )
518    }
519
520    /// Set the genesis overrides as a JSON object.
521    pub fn with_genesis_overrides(self, genesis_overrides: impl Into<serde_json::Value>) -> Self {
522        Self::transition(
523            RelaychainConfig {
524                runtime_genesis_patch: Some(genesis_overrides.into()),
525                ..self.config
526            },
527            self.validation_context,
528            self.errors,
529        )
530    }
531
532    /// Add a new validator node using a nested [`NodeConfigBuilder`].
533    /// The node will be configured as a validator (authority) with the --validator flag.
534    pub fn with_validator(
535        self,
536        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
537    ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
538        match self.create_node_builder(f).validator(true).build() {
539            Ok(node) => Self::transition(
540                RelaychainConfig {
541                    nodes: [self.config.nodes, vec![node]].concat(),
542                    ..self.config
543                },
544                self.validation_context,
545                self.errors,
546            ),
547            Err((name, errors)) => Self::transition(
548                self.config,
549                self.validation_context,
550                merge_errors_vecs(
551                    self.errors,
552                    errors
553                        .into_iter()
554                        .map(|error| ConfigError::Node(name.clone(), error).into())
555                        .collect::<Vec<_>>(),
556                ),
557            ),
558        }
559    }
560
561    /// Add a new full node using a nested [`NodeConfigBuilder`].
562    /// The node will be configured as a full node (non-validator).
563    pub fn with_fullnode(
564        self,
565        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
566    ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
567        match self.create_node_builder(f).validator(false).build() {
568            Ok(node) => Self::transition(
569                RelaychainConfig {
570                    nodes: [self.config.nodes, vec![node]].concat(),
571                    ..self.config
572                },
573                self.validation_context,
574                self.errors,
575            ),
576            Err((name, errors)) => Self::transition(
577                self.config,
578                self.validation_context,
579                merge_errors_vecs(
580                    self.errors,
581                    errors
582                        .into_iter()
583                        .map(|error| ConfigError::Node(name.clone(), error).into())
584                        .collect::<Vec<_>>(),
585                ),
586            ),
587        }
588    }
589
590    /// Add a new node using a nested [`NodeConfigBuilder`].
591    ///
592    /// **Deprecated**: Use [`with_validator`] for validator nodes or [`with_fullnode`] for full nodes instead.
593    #[deprecated(
594        since = "0.4.0",
595        note = "Use `with_validator()` for validator nodes or `with_fullnode()` for full nodes instead"
596    )]
597    pub fn with_node(
598        self,
599        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
600    ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
601        match self.create_node_builder(f).build() {
602            Ok(node) => Self::transition(
603                RelaychainConfig {
604                    nodes: vec![node],
605                    ..self.config
606                },
607                self.validation_context,
608                self.errors,
609            ),
610            Err((name, errors)) => Self::transition(
611                self.config,
612                self.validation_context,
613                merge_errors_vecs(
614                    self.errors,
615                    errors
616                        .into_iter()
617                        .map(|error| ConfigError::Node(name.clone(), error).into())
618                        .collect::<Vec<_>>(),
619                ),
620            ),
621        }
622    }
623
624    /// Add a new group node using a nested [`GroupNodeConfigBuilder`].
625    pub fn with_node_group(
626        self,
627        f: impl FnOnce(GroupNodeConfigBuilder<node::Initial>) -> GroupNodeConfigBuilder<node::Buildable>,
628    ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
629        match f(GroupNodeConfigBuilder::new(
630            self.default_chain_context(),
631            self.validation_context.clone(),
632        ))
633        .build()
634        {
635            Ok(group_node) => Self::transition(
636                RelaychainConfig {
637                    node_groups: vec![group_node],
638                    ..self.config
639                },
640                self.validation_context,
641                self.errors,
642            ),
643            Err((name, errors)) => Self::transition(
644                self.config,
645                self.validation_context,
646                merge_errors_vecs(
647                    self.errors,
648                    errors
649                        .into_iter()
650                        .map(|error| ConfigError::Node(name.clone(), error).into())
651                        .collect::<Vec<_>>(),
652                ),
653            ),
654        }
655    }
656
657    /// Set the location or inline value of a json to override the raw chain-spec.
658    pub fn with_raw_spec_override(self, overrides: impl Into<JsonOverrides>) -> Self {
659        Self::transition(
660            RelaychainConfig {
661                raw_spec_override: Some(overrides.into()),
662                ..self.config
663            },
664            self.validation_context,
665            self.errors,
666        )
667    }
668}
669
670impl RelaychainConfigBuilder<WithAtLeastOneNode> {
671    /// Add a new validator node using a nested [`NodeConfigBuilder`].
672    /// The node will be configured as a validator (authority) with the --validator flag.
673    pub fn with_validator(
674        self,
675        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
676    ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
677        match self.create_node_builder(f).validator(true).build() {
678            Ok(node) => Self::transition(
679                RelaychainConfig {
680                    nodes: [self.config.nodes, vec![node]].concat(),
681                    ..self.config
682                },
683                self.validation_context,
684                self.errors,
685            ),
686            Err((name, errors)) => Self::transition(
687                self.config,
688                self.validation_context,
689                merge_errors_vecs(
690                    self.errors,
691                    errors
692                        .into_iter()
693                        .map(|error| ConfigError::Node(name.clone(), error).into())
694                        .collect::<Vec<_>>(),
695                ),
696            ),
697        }
698    }
699
700    /// Add a new full node using a nested [`NodeConfigBuilder`].
701    /// The node will be configured as a full node (non-validator).
702    pub fn with_fullnode(
703        self,
704        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
705    ) -> Self {
706        match self.create_node_builder(f).validator(false).build() {
707            Ok(node) => Self::transition(
708                RelaychainConfig {
709                    nodes: [self.config.nodes, vec![node]].concat(),
710                    ..self.config
711                },
712                self.validation_context,
713                self.errors,
714            ),
715            Err((name, errors)) => Self::transition(
716                self.config,
717                self.validation_context,
718                merge_errors_vecs(
719                    self.errors,
720                    errors
721                        .into_iter()
722                        .map(|error| ConfigError::Node(name.clone(), error).into())
723                        .collect::<Vec<_>>(),
724                ),
725            ),
726        }
727    }
728
729    /// Add a new node using a nested [`NodeConfigBuilder`].
730    ///
731    /// **Deprecated**: Use [`with_validator`] for validator nodes or [`with_fullnode`] for full nodes instead.
732    #[deprecated(
733        since = "0.4.0",
734        note = "Use `with_validator()` for validator nodes or `with_fullnode()` for full nodes instead"
735    )]
736    pub fn with_node(
737        self,
738        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
739    ) -> Self {
740        match self.create_node_builder(f).build() {
741            Ok(node) => Self::transition(
742                RelaychainConfig {
743                    nodes: [self.config.nodes, vec![node]].concat(),
744                    ..self.config
745                },
746                self.validation_context,
747                self.errors,
748            ),
749            Err((name, errors)) => Self::transition(
750                self.config,
751                self.validation_context,
752                merge_errors_vecs(
753                    self.errors,
754                    errors
755                        .into_iter()
756                        .map(|error| ConfigError::Node(name.clone(), error).into())
757                        .collect::<Vec<_>>(),
758                ),
759            ),
760        }
761    }
762
763    /// Add a new group node using a nested [`GroupNodeConfigBuilder`].
764    pub fn with_node_group(
765        self,
766        f: impl FnOnce(GroupNodeConfigBuilder<node::Initial>) -> GroupNodeConfigBuilder<node::Buildable>,
767    ) -> Self {
768        match f(GroupNodeConfigBuilder::new(
769            self.default_chain_context(),
770            self.validation_context.clone(),
771        ))
772        .build()
773        {
774            Ok(group_node) => Self::transition(
775                RelaychainConfig {
776                    node_groups: [self.config.node_groups, vec![group_node]].concat(),
777                    ..self.config
778                },
779                self.validation_context,
780                self.errors,
781            ),
782            Err((name, errors)) => Self::transition(
783                self.config,
784                self.validation_context,
785                merge_errors_vecs(
786                    self.errors,
787                    errors
788                        .into_iter()
789                        .map(|error| ConfigError::Node(name.clone(), error).into())
790                        .collect::<Vec<_>>(),
791                ),
792            ),
793        }
794    }
795
796    /// Seals the builder and returns a [`RelaychainConfig`] if there are no validation errors, else returns errors.
797    pub fn build(self) -> Result<RelaychainConfig, Vec<anyhow::Error>> {
798        if !self.errors.is_empty() {
799            return Err(self
800                .errors
801                .into_iter()
802                .map(|error| ConfigError::Relaychain(error).into())
803                .collect::<Vec<_>>());
804        }
805
806        Ok(self.config)
807    }
808}
809
810#[cfg(test)]
811mod tests {
812    use super::*;
813
814    #[test]
815    fn relaychain_config_builder_should_succeeds_and_returns_a_relaychain_config() {
816        let relaychain_config = RelaychainConfigBuilder::new(Default::default())
817            .with_chain("polkadot")
818            .with_default_image("myrepo:myimage")
819            .with_default_command("default_command")
820            .with_default_resources(|resources| {
821                resources
822                    .with_limit_cpu("500M")
823                    .with_limit_memory("1G")
824                    .with_request_cpu("250M")
825            })
826            .with_default_db_snapshot("https://www.urltomysnapshot.com/file.tgz")
827            .with_chain_spec_path("./path/to/chain/spec.json")
828            .with_chain_spec_runtime("./path/to/runtime.wasm", Some("local_testnet"))
829            .with_wasm_override("./path/to/override/runtime.wasm")
830            .with_raw_spec_override(serde_json::json!({"some_override_key": "some_override_val"}))
831            .with_default_args(vec![("--arg1", "value1").into(), "--option2".into()])
832            .with_random_nominators_count(42)
833            .with_max_nominations(5)
834            .with_fullnode(|node| node.with_name("node1").bootnode(true))
835            .with_validator(|node| node.with_name("node2").with_command("command2"))
836            .build()
837            .unwrap();
838
839        assert_eq!(relaychain_config.chain().as_str(), "polkadot");
840        assert_eq!(relaychain_config.nodes().len(), 2);
841        let &node1 = relaychain_config.nodes().first().unwrap();
842        assert_eq!(node1.name(), "node1");
843        assert_eq!(node1.command().unwrap().as_str(), "default_command");
844        assert!(node1.is_bootnode());
845        let &node2 = relaychain_config.nodes().last().unwrap();
846        assert_eq!(node2.name(), "node2");
847        assert_eq!(node2.command().unwrap().as_str(), "command2");
848        assert!(node2.is_validator());
849        assert_eq!(
850            relaychain_config.default_command().unwrap().as_str(),
851            "default_command"
852        );
853        assert_eq!(
854            relaychain_config.default_image().unwrap().as_str(),
855            "myrepo:myimage"
856        );
857        let default_resources = relaychain_config.default_resources().unwrap();
858        assert_eq!(default_resources.limit_cpu().unwrap().as_str(), "500M");
859        assert_eq!(default_resources.limit_memory().unwrap().as_str(), "1G");
860        assert_eq!(default_resources.request_cpu().unwrap().as_str(), "250M");
861        assert!(matches!(
862            relaychain_config.default_db_snapshot().unwrap(),
863            AssetLocation::Url(value) if value.as_str() == "https://www.urltomysnapshot.com/file.tgz",
864        ));
865        assert!(matches!(
866            relaychain_config.chain_spec_path().unwrap(),
867            AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/chain/spec.json"
868        ));
869        assert!(matches!(
870            &relaychain_config.chain_spec_runtime().unwrap().location,
871            AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/runtime.wasm"
872        ));
873        assert_eq!(
874            relaychain_config
875                .chain_spec_runtime()
876                .unwrap()
877                .preset
878                .as_deref(),
879            Some("local_testnet")
880        );
881        assert!(matches!(
882            relaychain_config.wasm_override().unwrap(),
883            AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/override/runtime.wasm"
884        ));
885        let args: Vec<Arg> = vec![("--arg1", "value1").into(), "--option2".into()];
886        assert_eq!(
887            relaychain_config.default_args(),
888            args.iter().collect::<Vec<_>>()
889        );
890        assert_eq!(relaychain_config.random_nominators_count().unwrap(), 42);
891        assert_eq!(relaychain_config.max_nominations().unwrap(), 5);
892
893        assert!(matches!(
894            relaychain_config.raw_spec_override().unwrap(),
895            JsonOverrides::Json(value) if *value == serde_json::json!({"some_override_key": "some_override_val"})
896        ));
897    }
898
899    #[test]
900    fn relaychain_config_builder_should_fails_and_returns_an_error_if_chain_is_invalid() {
901        let errors = RelaychainConfigBuilder::new(Default::default())
902            .with_chain("invalid chain")
903            .with_validator(|node| node.with_name("node").with_command("command"))
904            .build()
905            .unwrap_err();
906
907        assert_eq!(errors.len(), 1);
908        assert_eq!(
909            errors.first().unwrap().to_string(),
910            "relaychain.chain: 'invalid chain' shouldn't contains whitespace"
911        );
912    }
913
914    #[test]
915    fn relaychain_config_builder_should_fails_and_returns_an_error_if_default_command_is_invalid() {
916        let errors = RelaychainConfigBuilder::new(Default::default())
917            .with_chain("chain")
918            .with_default_command("invalid command")
919            .with_validator(|node| node.with_name("node").with_command("command"))
920            .build()
921            .unwrap_err();
922
923        assert_eq!(errors.len(), 1);
924        assert_eq!(
925            errors.first().unwrap().to_string(),
926            "relaychain.default_command: 'invalid command' shouldn't contains whitespace"
927        );
928    }
929
930    #[test]
931    fn relaychain_config_builder_should_fails_and_returns_an_error_if_default_image_is_invalid() {
932        let errors = RelaychainConfigBuilder::new(Default::default())
933            .with_chain("chain")
934            .with_default_image("invalid image")
935            .with_validator(|node| node.with_name("node").with_command("command"))
936            .build()
937            .unwrap_err();
938
939        assert_eq!(errors.len(), 1);
940        assert_eq!(
941            errors.first().unwrap().to_string(),
942            r"relaychain.default_image: 'invalid image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
943        );
944    }
945
946    #[test]
947    fn relaychain_config_builder_should_fails_and_returns_an_error_if_default_resources_are_invalid(
948    ) {
949        let errors = RelaychainConfigBuilder::new(Default::default())
950            .with_chain("chain")
951            .with_default_resources(|default_resources| {
952                default_resources
953                    .with_limit_memory("100m")
954                    .with_request_cpu("invalid")
955            })
956            .with_validator(|node| node.with_name("node").with_command("command"))
957            .build()
958            .unwrap_err();
959
960        assert_eq!(errors.len(), 1);
961        assert_eq!(
962            errors.first().unwrap().to_string(),
963            r"relaychain.default_resources.request_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
964        );
965    }
966
967    #[test]
968    fn relaychain_config_builder_should_fails_and_returns_an_error_if_first_node_is_invalid() {
969        let errors = RelaychainConfigBuilder::new(Default::default())
970            .with_chain("chain")
971            .with_validator(|node| node.with_name("node").with_command("invalid command"))
972            .build()
973            .unwrap_err();
974
975        assert_eq!(errors.len(), 1);
976        assert_eq!(
977            errors.first().unwrap().to_string(),
978            "relaychain.nodes['node'].command: 'invalid command' shouldn't contains whitespace"
979        );
980    }
981
982    #[test]
983    fn relaychain_config_builder_with_at_least_one_node_should_fails_and_returns_an_error_if_second_node_is_invalid(
984    ) {
985        let errors = RelaychainConfigBuilder::new(Default::default())
986            .with_chain("chain")
987            .with_validator(|node| node.with_name("node1").with_command("command1"))
988            .with_validator(|node| node.with_name("node2").with_command("invalid command"))
989            .build()
990            .unwrap_err();
991
992        assert_eq!(errors.len(), 1);
993        assert_eq!(
994            errors.first().unwrap().to_string(),
995            "relaychain.nodes['node2'].command: 'invalid command' shouldn't contains whitespace"
996        );
997    }
998
999    #[test]
1000    fn relaychain_config_builder_should_fails_returns_multiple_errors_if_a_node_and_default_resources_are_invalid(
1001    ) {
1002        let errors = RelaychainConfigBuilder::new(Default::default())
1003            .with_chain("chain")
1004            .with_default_resources(|resources| {
1005                resources
1006                    .with_request_cpu("100Mi")
1007                    .with_limit_memory("1Gi")
1008                    .with_limit_cpu("invalid")
1009            })
1010            .with_validator(|node| node.with_name("node").with_image("invalid image"))
1011            .build()
1012            .unwrap_err();
1013
1014        assert_eq!(errors.len(), 2);
1015        assert_eq!(
1016            errors.first().unwrap().to_string(),
1017            "relaychain.default_resources.limit_cpu: 'invalid' doesn't match regex '^\\d+(.\\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1018        );
1019        assert_eq!(
1020            errors.get(1).unwrap().to_string(),
1021            "relaychain.nodes['node'].image: 'invalid image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1022        );
1023    }
1024
1025    #[test]
1026    fn relaychain_config_builder_should_works_with_chain_spec_command() {
1027        const CMD_TPL: &str = "./bin/chain-spec-generator {% raw %} {{chainName}} {% endraw %}";
1028        let config = RelaychainConfigBuilder::new(Default::default())
1029            .with_chain("polkadot")
1030            .with_default_image("myrepo:myimage")
1031            .with_default_command("default_command")
1032            .with_chain_spec_command(CMD_TPL)
1033            .with_fullnode(|node| node.with_name("node1").bootnode(true))
1034            .build()
1035            .unwrap();
1036
1037        assert_eq!(config.chain_spec_command(), Some(CMD_TPL));
1038        assert!(!config.chain_spec_command_is_local());
1039    }
1040
1041    #[test]
1042    fn relaychain_config_builder_should_works_with_chain_spec_command_locally() {
1043        const CMD_TPL: &str = "./bin/chain-spec-generator {% raw %} {{chainName}} {% endraw %}";
1044        let config = RelaychainConfigBuilder::new(Default::default())
1045            .with_chain("polkadot")
1046            .with_default_image("myrepo:myimage")
1047            .with_default_command("default_command")
1048            .with_chain_spec_command(CMD_TPL)
1049            .chain_spec_command_is_local(true)
1050            .with_fullnode(|node| node.with_name("node1").bootnode(true))
1051            .build()
1052            .unwrap();
1053
1054        assert_eq!(config.chain_spec_command(), Some(CMD_TPL));
1055        assert!(config.chain_spec_command_is_local());
1056    }
1057
1058    #[test]
1059    fn relaychain_with_group_config_should_succeeds_and_returns_a_relaychain_config() {
1060        let relaychain_config = RelaychainConfigBuilder::new(Default::default())
1061            .with_chain("chain")
1062            .with_default_command("command")
1063            .with_validator(|node| node.with_name("node").with_command("node_command"))
1064            .with_node_group(|group| {
1065                group.with_count(2).with_base_node(|base| {
1066                    base.with_name("group_node")
1067                        .with_command("some_command")
1068                        .with_image("repo:image")
1069                        .validator(true)
1070                })
1071            })
1072            .build()
1073            .unwrap();
1074
1075        assert_eq!(relaychain_config.chain().as_str(), "chain");
1076        assert_eq!(relaychain_config.nodes().len(), 1);
1077        assert_eq!(relaychain_config.group_node_configs().len(), 1);
1078        assert_eq!(
1079            relaychain_config
1080                .group_node_configs()
1081                .first()
1082                .unwrap()
1083                .count,
1084            2
1085        );
1086        let &node = relaychain_config.nodes().first().unwrap();
1087        assert_eq!(node.name(), "node");
1088        assert_eq!(node.command().unwrap().as_str(), "node_command");
1089
1090        let group_nodes = relaychain_config.group_node_configs();
1091        let group_base_node = group_nodes.first().unwrap();
1092        assert_eq!(group_base_node.base_config.name(), "group_node");
1093        assert_eq!(
1094            group_base_node.base_config.command().unwrap().as_str(),
1095            "some_command"
1096        );
1097        assert_eq!(
1098            group_base_node.base_config.image().unwrap().as_str(),
1099            "repo:image"
1100        );
1101        assert!(group_base_node.base_config.is_validator());
1102    }
1103
1104    #[test]
1105    fn relaychain_with_group_count_0_config_should_fail() {
1106        let relaychain_config = RelaychainConfigBuilder::new(Default::default())
1107            .with_chain("chain")
1108            .with_default_command("command")
1109            .with_validator(|node| node.with_name("node").with_command("node_command"))
1110            .with_node_group(|group| {
1111                group.with_count(0).with_base_node(|base| {
1112                    base.with_name("group_node")
1113                        .with_command("some_command")
1114                        .with_image("repo:image")
1115                        .validator(true)
1116                })
1117            })
1118            .build();
1119
1120        let errors: Vec<anyhow::Error> = match relaychain_config {
1121            Ok(_) => vec![],
1122            Err(errs) => errs,
1123        };
1124
1125        assert_eq!(errors.len(), 1);
1126        assert_eq!(
1127            errors.first().unwrap().to_string(),
1128            "relaychain.nodes['group_node'].Count cannot be zero"
1129        );
1130    }
1131}