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    /// Like [`Self::with_default_db_snapshot`], but a no-op when `location`
389    /// is `None`. Lets a caller parametrise one network builder over both
390    /// "fresh" (`None`) and "from snapshot" (`Some`) without branching.
391    pub fn with_optional_default_db_snapshot(
392        self,
393        location: Option<impl Into<AssetLocation>>,
394    ) -> Self {
395        match location {
396            Some(location) => self.with_default_db_snapshot(location),
397            None => self,
398        }
399    }
400
401    /// Set the default arguments that will be used to execute the node command. Can be overridden.
402    pub fn with_default_args(self, args: Vec<Arg>) -> Self {
403        Self::transition(
404            RelaychainConfig {
405                default_args: args,
406                ..self.config
407            },
408            self.validation_context,
409            self.errors,
410        )
411    }
412
413    /// Set the location of a pre-existing chain specification for the relay chain.
414    pub fn with_chain_spec_path(self, location: impl Into<AssetLocation>) -> Self {
415        Self::transition(
416            RelaychainConfig {
417                chain_spec_path: Some(location.into()),
418                ..self.config
419            },
420            self.validation_context,
421            self.errors,
422        )
423    }
424
425    /// Set the location of a wasm to override the chain-spec.
426    pub fn with_wasm_override(self, location: impl Into<AssetLocation>) -> Self {
427        Self::transition(
428            RelaychainConfig {
429                wasm_override: Some(location.into()),
430                ..self.config
431            },
432            self.validation_context,
433            self.errors,
434        )
435    }
436
437    /// Set the chain-spec command _template_ for the relay chain.
438    pub fn with_chain_spec_command(self, cmd_template: impl Into<String>) -> Self {
439        Self::transition(
440            RelaychainConfig {
441                chain_spec_command: Some(cmd_template.into()),
442                ..self.config
443            },
444            self.validation_context,
445            self.errors,
446        )
447    }
448
449    /// Set the runtime path to use for generating the chain-spec and an optiona preset.
450    /// If the preset is not set, we will try to match [`local_testnet`, `development`, `dev`]
451    /// with the available ones and fallback to the default configuration as last option.
452    pub fn with_chain_spec_runtime(
453        self,
454        location: impl Into<AssetLocation>,
455        preset: Option<&str>,
456    ) -> Self {
457        let chain_spec_runtime = if let Some(preset) = preset {
458            ChainSpecRuntime::with_preset(location.into(), preset.to_string())
459        } else {
460            ChainSpecRuntime::new(location.into())
461        };
462        Self::transition(
463            RelaychainConfig {
464                chain_spec_runtime: Some(chain_spec_runtime),
465                ..self.config
466            },
467            self.validation_context,
468            self.errors,
469        )
470    }
471
472    /// Set if the chain-spec command needs to be run locally or not (false by default)
473    pub fn chain_spec_command_is_local(self, choice: bool) -> Self {
474        Self::transition(
475            RelaychainConfig {
476                chain_spec_command_is_local: choice,
477                ..self.config
478            },
479            self.validation_context,
480            self.errors,
481        )
482    }
483
484    /// Set to true to override session 0 and allow paras to produce
485    /// blocks from genesis.
486    pub fn with_override_session_0(self, choice: bool) -> Self {
487        Self::transition(
488            RelaychainConfig {
489                override_session_0: choice,
490                ..self.config
491            },
492            self.validation_context,
493            self.errors,
494        )
495    }
496
497    /// Set the output path for the chain-spec command.
498    pub fn with_chain_spec_command_output_path(self, output_path: &str) -> Self {
499        Self::transition(
500            RelaychainConfig {
501                chain_spec_command_output_path: Some(output_path.to_string()),
502                ..self.config
503            },
504            self.validation_context,
505            self.errors,
506        )
507    }
508
509    /// 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.
510    pub fn with_random_nominators_count(self, random_nominators_count: u32) -> Self {
511        Self::transition(
512            RelaychainConfig {
513                random_nominators_count: Some(random_nominators_count),
514                ..self.config
515            },
516            self.validation_context,
517            self.errors,
518        )
519    }
520
521    /// Set the maximum number of nominations to create per nominator.
522    pub fn with_max_nominations(self, max_nominations: u8) -> Self {
523        Self::transition(
524            RelaychainConfig {
525                max_nominations: Some(max_nominations),
526                ..self.config
527            },
528            self.validation_context,
529            self.errors,
530        )
531    }
532
533    /// Set the genesis overrides as a JSON object.
534    pub fn with_genesis_overrides(self, genesis_overrides: impl Into<serde_json::Value>) -> Self {
535        Self::transition(
536            RelaychainConfig {
537                runtime_genesis_patch: Some(genesis_overrides.into()),
538                ..self.config
539            },
540            self.validation_context,
541            self.errors,
542        )
543    }
544
545    /// Add a new validator node using a nested [`NodeConfigBuilder`].
546    /// The node will be configured as a validator (authority) with the --validator flag.
547    pub fn with_validator(
548        self,
549        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
550    ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
551        match self.create_node_builder(f).validator(true).build() {
552            Ok(node) => Self::transition(
553                RelaychainConfig {
554                    nodes: [self.config.nodes, vec![node]].concat(),
555                    ..self.config
556                },
557                self.validation_context,
558                self.errors,
559            ),
560            Err((name, errors)) => Self::transition(
561                self.config,
562                self.validation_context,
563                merge_errors_vecs(
564                    self.errors,
565                    errors
566                        .into_iter()
567                        .map(|error| ConfigError::Node(name.clone(), error).into())
568                        .collect::<Vec<_>>(),
569                ),
570            ),
571        }
572    }
573
574    /// Add a new full node using a nested [`NodeConfigBuilder`].
575    /// The node will be configured as a full node (non-validator).
576    pub fn with_fullnode(
577        self,
578        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
579    ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
580        match self.create_node_builder(f).validator(false).build() {
581            Ok(node) => Self::transition(
582                RelaychainConfig {
583                    nodes: [self.config.nodes, vec![node]].concat(),
584                    ..self.config
585                },
586                self.validation_context,
587                self.errors,
588            ),
589            Err((name, errors)) => Self::transition(
590                self.config,
591                self.validation_context,
592                merge_errors_vecs(
593                    self.errors,
594                    errors
595                        .into_iter()
596                        .map(|error| ConfigError::Node(name.clone(), error).into())
597                        .collect::<Vec<_>>(),
598                ),
599            ),
600        }
601    }
602
603    /// Add a new node using a nested [`NodeConfigBuilder`].
604    ///
605    /// **Deprecated**: Use [`with_validator`] for validator nodes or [`with_fullnode`] for full nodes instead.
606    #[deprecated(
607        since = "0.4.0",
608        note = "Use `with_validator()` for validator nodes or `with_fullnode()` for full nodes instead"
609    )]
610    pub fn with_node(
611        self,
612        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
613    ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
614        match self.create_node_builder(f).build() {
615            Ok(node) => Self::transition(
616                RelaychainConfig {
617                    nodes: vec![node],
618                    ..self.config
619                },
620                self.validation_context,
621                self.errors,
622            ),
623            Err((name, errors)) => Self::transition(
624                self.config,
625                self.validation_context,
626                merge_errors_vecs(
627                    self.errors,
628                    errors
629                        .into_iter()
630                        .map(|error| ConfigError::Node(name.clone(), error).into())
631                        .collect::<Vec<_>>(),
632                ),
633            ),
634        }
635    }
636
637    /// Add a new group node using a nested [`GroupNodeConfigBuilder`].
638    pub fn with_node_group(
639        self,
640        f: impl FnOnce(GroupNodeConfigBuilder<node::Initial>) -> GroupNodeConfigBuilder<node::Buildable>,
641    ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
642        match f(GroupNodeConfigBuilder::new(
643            self.default_chain_context(),
644            self.validation_context.clone(),
645        ))
646        .build()
647        {
648            Ok(group_node) => Self::transition(
649                RelaychainConfig {
650                    node_groups: vec![group_node],
651                    ..self.config
652                },
653                self.validation_context,
654                self.errors,
655            ),
656            Err((name, errors)) => Self::transition(
657                self.config,
658                self.validation_context,
659                merge_errors_vecs(
660                    self.errors,
661                    errors
662                        .into_iter()
663                        .map(|error| ConfigError::Node(name.clone(), error).into())
664                        .collect::<Vec<_>>(),
665                ),
666            ),
667        }
668    }
669
670    /// Set the location or inline value of a json to override the raw chain-spec.
671    pub fn with_raw_spec_override(self, overrides: impl Into<JsonOverrides>) -> Self {
672        Self::transition(
673            RelaychainConfig {
674                raw_spec_override: Some(overrides.into()),
675                ..self.config
676            },
677            self.validation_context,
678            self.errors,
679        )
680    }
681}
682
683impl RelaychainConfigBuilder<WithAtLeastOneNode> {
684    /// Add a new validator node using a nested [`NodeConfigBuilder`].
685    /// The node will be configured as a validator (authority) with the --validator flag.
686    pub fn with_validator(
687        self,
688        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
689    ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
690        match self.create_node_builder(f).validator(true).build() {
691            Ok(node) => Self::transition(
692                RelaychainConfig {
693                    nodes: [self.config.nodes, vec![node]].concat(),
694                    ..self.config
695                },
696                self.validation_context,
697                self.errors,
698            ),
699            Err((name, errors)) => Self::transition(
700                self.config,
701                self.validation_context,
702                merge_errors_vecs(
703                    self.errors,
704                    errors
705                        .into_iter()
706                        .map(|error| ConfigError::Node(name.clone(), error).into())
707                        .collect::<Vec<_>>(),
708                ),
709            ),
710        }
711    }
712
713    /// Add a new full node using a nested [`NodeConfigBuilder`].
714    /// The node will be configured as a full node (non-validator).
715    pub fn with_fullnode(
716        self,
717        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
718    ) -> Self {
719        match self.create_node_builder(f).validator(false).build() {
720            Ok(node) => Self::transition(
721                RelaychainConfig {
722                    nodes: [self.config.nodes, vec![node]].concat(),
723                    ..self.config
724                },
725                self.validation_context,
726                self.errors,
727            ),
728            Err((name, errors)) => Self::transition(
729                self.config,
730                self.validation_context,
731                merge_errors_vecs(
732                    self.errors,
733                    errors
734                        .into_iter()
735                        .map(|error| ConfigError::Node(name.clone(), error).into())
736                        .collect::<Vec<_>>(),
737                ),
738            ),
739        }
740    }
741
742    /// Add a new node using a nested [`NodeConfigBuilder`].
743    ///
744    /// **Deprecated**: Use [`with_validator`] for validator nodes or [`with_fullnode`] for full nodes instead.
745    #[deprecated(
746        since = "0.4.0",
747        note = "Use `with_validator()` for validator nodes or `with_fullnode()` for full nodes instead"
748    )]
749    pub fn with_node(
750        self,
751        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
752    ) -> Self {
753        match self.create_node_builder(f).build() {
754            Ok(node) => Self::transition(
755                RelaychainConfig {
756                    nodes: [self.config.nodes, vec![node]].concat(),
757                    ..self.config
758                },
759                self.validation_context,
760                self.errors,
761            ),
762            Err((name, errors)) => Self::transition(
763                self.config,
764                self.validation_context,
765                merge_errors_vecs(
766                    self.errors,
767                    errors
768                        .into_iter()
769                        .map(|error| ConfigError::Node(name.clone(), error).into())
770                        .collect::<Vec<_>>(),
771                ),
772            ),
773        }
774    }
775
776    /// Add a new group node using a nested [`GroupNodeConfigBuilder`].
777    pub fn with_node_group(
778        self,
779        f: impl FnOnce(GroupNodeConfigBuilder<node::Initial>) -> GroupNodeConfigBuilder<node::Buildable>,
780    ) -> Self {
781        match f(GroupNodeConfigBuilder::new(
782            self.default_chain_context(),
783            self.validation_context.clone(),
784        ))
785        .build()
786        {
787            Ok(group_node) => Self::transition(
788                RelaychainConfig {
789                    node_groups: [self.config.node_groups, vec![group_node]].concat(),
790                    ..self.config
791                },
792                self.validation_context,
793                self.errors,
794            ),
795            Err((name, errors)) => Self::transition(
796                self.config,
797                self.validation_context,
798                merge_errors_vecs(
799                    self.errors,
800                    errors
801                        .into_iter()
802                        .map(|error| ConfigError::Node(name.clone(), error).into())
803                        .collect::<Vec<_>>(),
804                ),
805            ),
806        }
807    }
808
809    /// Seals the builder and returns a [`RelaychainConfig`] if there are no validation errors, else returns errors.
810    pub fn build(self) -> Result<RelaychainConfig, Vec<anyhow::Error>> {
811        if !self.errors.is_empty() {
812            return Err(self
813                .errors
814                .into_iter()
815                .map(|error| ConfigError::Relaychain(error).into())
816                .collect::<Vec<_>>());
817        }
818
819        Ok(self.config)
820    }
821}
822
823#[cfg(test)]
824mod tests {
825    use super::*;
826
827    #[test]
828    fn relaychain_config_builder_should_succeeds_and_returns_a_relaychain_config() {
829        let relaychain_config = RelaychainConfigBuilder::new(Default::default())
830            .with_chain("polkadot")
831            .with_default_image("myrepo:myimage")
832            .with_default_command("default_command")
833            .with_default_resources(|resources| {
834                resources
835                    .with_limit_cpu("500M")
836                    .with_limit_memory("1G")
837                    .with_request_cpu("250M")
838            })
839            .with_default_db_snapshot("https://www.urltomysnapshot.com/file.tgz")
840            .with_chain_spec_path("./path/to/chain/spec.json")
841            .with_chain_spec_runtime("./path/to/runtime.wasm", Some("local_testnet"))
842            .with_wasm_override("./path/to/override/runtime.wasm")
843            .with_raw_spec_override(serde_json::json!({"some_override_key": "some_override_val"}))
844            .with_default_args(vec![("--arg1", "value1").into(), "--option2".into()])
845            .with_random_nominators_count(42)
846            .with_max_nominations(5)
847            .with_fullnode(|node| node.with_name("node1").bootnode(true))
848            .with_validator(|node| node.with_name("node2").with_command("command2"))
849            .build()
850            .unwrap();
851
852        assert_eq!(relaychain_config.chain().as_str(), "polkadot");
853        assert_eq!(relaychain_config.nodes().len(), 2);
854        let &node1 = relaychain_config.nodes().first().unwrap();
855        assert_eq!(node1.name(), "node1");
856        assert_eq!(node1.command().unwrap().as_str(), "default_command");
857        assert!(node1.is_bootnode());
858        let &node2 = relaychain_config.nodes().last().unwrap();
859        assert_eq!(node2.name(), "node2");
860        assert_eq!(node2.command().unwrap().as_str(), "command2");
861        assert!(node2.is_validator());
862        assert_eq!(
863            relaychain_config.default_command().unwrap().as_str(),
864            "default_command"
865        );
866        assert_eq!(
867            relaychain_config.default_image().unwrap().as_str(),
868            "myrepo:myimage"
869        );
870        let default_resources = relaychain_config.default_resources().unwrap();
871        assert_eq!(default_resources.limit_cpu().unwrap().as_str(), "500M");
872        assert_eq!(default_resources.limit_memory().unwrap().as_str(), "1G");
873        assert_eq!(default_resources.request_cpu().unwrap().as_str(), "250M");
874        assert!(matches!(
875            relaychain_config.default_db_snapshot().unwrap(),
876            AssetLocation::Url(value) if value.as_str() == "https://www.urltomysnapshot.com/file.tgz",
877        ));
878        assert!(matches!(
879            relaychain_config.chain_spec_path().unwrap(),
880            AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/chain/spec.json"
881        ));
882        assert!(matches!(
883            &relaychain_config.chain_spec_runtime().unwrap().location,
884            AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/runtime.wasm"
885        ));
886        assert_eq!(
887            relaychain_config
888                .chain_spec_runtime()
889                .unwrap()
890                .preset
891                .as_deref(),
892            Some("local_testnet")
893        );
894        assert!(matches!(
895            relaychain_config.wasm_override().unwrap(),
896            AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/override/runtime.wasm"
897        ));
898        let args: Vec<Arg> = vec![("--arg1", "value1").into(), "--option2".into()];
899        assert_eq!(
900            relaychain_config.default_args(),
901            args.iter().collect::<Vec<_>>()
902        );
903        assert_eq!(relaychain_config.random_nominators_count().unwrap(), 42);
904        assert_eq!(relaychain_config.max_nominations().unwrap(), 5);
905
906        assert!(matches!(
907            relaychain_config.raw_spec_override().unwrap(),
908            JsonOverrides::Json(value) if *value == serde_json::json!({"some_override_key": "some_override_val"})
909        ));
910    }
911
912    #[test]
913    fn with_optional_default_db_snapshot_applies_when_some() {
914        let config = RelaychainConfigBuilder::new(Default::default())
915            .with_chain("polkadot")
916            .with_default_command("cmd")
917            .with_optional_default_db_snapshot(Some("https://example.com/snap.tgz"))
918            .with_validator(|node| node.with_name("alice"))
919            .build()
920            .unwrap();
921        assert!(matches!(
922            config.default_db_snapshot().unwrap(),
923            AssetLocation::Url(value) if value.as_str() == "https://example.com/snap.tgz"
924        ));
925    }
926
927    #[test]
928    fn with_optional_default_db_snapshot_is_noop_when_none() {
929        let config = RelaychainConfigBuilder::new(Default::default())
930            .with_chain("polkadot")
931            .with_default_command("cmd")
932            .with_optional_default_db_snapshot(None::<&str>)
933            .with_validator(|node| node.with_name("alice"))
934            .build()
935            .unwrap();
936        assert!(config.default_db_snapshot().is_none());
937    }
938
939    #[test]
940    fn relaychain_config_builder_should_fails_and_returns_an_error_if_chain_is_invalid() {
941        let errors = RelaychainConfigBuilder::new(Default::default())
942            .with_chain("invalid chain")
943            .with_validator(|node| node.with_name("node").with_command("command"))
944            .build()
945            .unwrap_err();
946
947        assert_eq!(errors.len(), 1);
948        assert_eq!(
949            errors.first().unwrap().to_string(),
950            "relaychain.chain: 'invalid chain' shouldn't contains whitespace"
951        );
952    }
953
954    #[test]
955    fn relaychain_config_builder_should_fails_and_returns_an_error_if_default_command_is_invalid() {
956        let errors = RelaychainConfigBuilder::new(Default::default())
957            .with_chain("chain")
958            .with_default_command("invalid command")
959            .with_validator(|node| node.with_name("node").with_command("command"))
960            .build()
961            .unwrap_err();
962
963        assert_eq!(errors.len(), 1);
964        assert_eq!(
965            errors.first().unwrap().to_string(),
966            "relaychain.default_command: 'invalid command' shouldn't contains whitespace"
967        );
968    }
969
970    #[test]
971    fn relaychain_config_builder_should_fails_and_returns_an_error_if_default_image_is_invalid() {
972        let errors = RelaychainConfigBuilder::new(Default::default())
973            .with_chain("chain")
974            .with_default_image("invalid image")
975            .with_validator(|node| node.with_name("node").with_command("command"))
976            .build()
977            .unwrap_err();
978
979        assert_eq!(errors.len(), 1);
980        assert_eq!(
981            errors.first().unwrap().to_string(),
982            r"relaychain.default_image: 'invalid image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
983        );
984    }
985
986    #[test]
987    fn relaychain_config_builder_should_fails_and_returns_an_error_if_default_resources_are_invalid(
988    ) {
989        let errors = RelaychainConfigBuilder::new(Default::default())
990            .with_chain("chain")
991            .with_default_resources(|default_resources| {
992                default_resources
993                    .with_limit_memory("100m")
994                    .with_request_cpu("invalid")
995            })
996            .with_validator(|node| node.with_name("node").with_command("command"))
997            .build()
998            .unwrap_err();
999
1000        assert_eq!(errors.len(), 1);
1001        assert_eq!(
1002            errors.first().unwrap().to_string(),
1003            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)?$'"
1004        );
1005    }
1006
1007    #[test]
1008    fn relaychain_config_builder_should_fails_and_returns_an_error_if_first_node_is_invalid() {
1009        let errors = RelaychainConfigBuilder::new(Default::default())
1010            .with_chain("chain")
1011            .with_validator(|node| node.with_name("node").with_command("invalid command"))
1012            .build()
1013            .unwrap_err();
1014
1015        assert_eq!(errors.len(), 1);
1016        assert_eq!(
1017            errors.first().unwrap().to_string(),
1018            "relaychain.nodes['node'].command: 'invalid command' shouldn't contains whitespace"
1019        );
1020    }
1021
1022    #[test]
1023    fn relaychain_config_builder_with_at_least_one_node_should_fails_and_returns_an_error_if_second_node_is_invalid(
1024    ) {
1025        let errors = RelaychainConfigBuilder::new(Default::default())
1026            .with_chain("chain")
1027            .with_validator(|node| node.with_name("node1").with_command("command1"))
1028            .with_validator(|node| node.with_name("node2").with_command("invalid command"))
1029            .build()
1030            .unwrap_err();
1031
1032        assert_eq!(errors.len(), 1);
1033        assert_eq!(
1034            errors.first().unwrap().to_string(),
1035            "relaychain.nodes['node2'].command: 'invalid command' shouldn't contains whitespace"
1036        );
1037    }
1038
1039    #[test]
1040    fn relaychain_config_builder_should_fails_returns_multiple_errors_if_a_node_and_default_resources_are_invalid(
1041    ) {
1042        let errors = RelaychainConfigBuilder::new(Default::default())
1043            .with_chain("chain")
1044            .with_default_resources(|resources| {
1045                resources
1046                    .with_request_cpu("100Mi")
1047                    .with_limit_memory("1Gi")
1048                    .with_limit_cpu("invalid")
1049            })
1050            .with_validator(|node| node.with_name("node").with_image("invalid image"))
1051            .build()
1052            .unwrap_err();
1053
1054        assert_eq!(errors.len(), 2);
1055        assert_eq!(
1056            errors.first().unwrap().to_string(),
1057            "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)?$'"
1058        );
1059        assert_eq!(
1060            errors.get(1).unwrap().to_string(),
1061            "relaychain.nodes['node'].image: 'invalid image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1062        );
1063    }
1064
1065    #[test]
1066    fn relaychain_config_builder_should_works_with_chain_spec_command() {
1067        const CMD_TPL: &str = "./bin/chain-spec-generator {% raw %} {{chainName}} {% endraw %}";
1068        let config = RelaychainConfigBuilder::new(Default::default())
1069            .with_chain("polkadot")
1070            .with_default_image("myrepo:myimage")
1071            .with_default_command("default_command")
1072            .with_chain_spec_command(CMD_TPL)
1073            .with_fullnode(|node| node.with_name("node1").bootnode(true))
1074            .build()
1075            .unwrap();
1076
1077        assert_eq!(config.chain_spec_command(), Some(CMD_TPL));
1078        assert!(!config.chain_spec_command_is_local());
1079    }
1080
1081    #[test]
1082    fn relaychain_config_builder_should_works_with_chain_spec_command_locally() {
1083        const CMD_TPL: &str = "./bin/chain-spec-generator {% raw %} {{chainName}} {% endraw %}";
1084        let config = RelaychainConfigBuilder::new(Default::default())
1085            .with_chain("polkadot")
1086            .with_default_image("myrepo:myimage")
1087            .with_default_command("default_command")
1088            .with_chain_spec_command(CMD_TPL)
1089            .chain_spec_command_is_local(true)
1090            .with_fullnode(|node| node.with_name("node1").bootnode(true))
1091            .build()
1092            .unwrap();
1093
1094        assert_eq!(config.chain_spec_command(), Some(CMD_TPL));
1095        assert!(config.chain_spec_command_is_local());
1096    }
1097
1098    #[test]
1099    fn relaychain_with_group_config_should_succeeds_and_returns_a_relaychain_config() {
1100        let relaychain_config = RelaychainConfigBuilder::new(Default::default())
1101            .with_chain("chain")
1102            .with_default_command("command")
1103            .with_validator(|node| node.with_name("node").with_command("node_command"))
1104            .with_node_group(|group| {
1105                group.with_count(2).with_base_node(|base| {
1106                    base.with_name("group_node")
1107                        .with_command("some_command")
1108                        .with_image("repo:image")
1109                        .validator(true)
1110                })
1111            })
1112            .build()
1113            .unwrap();
1114
1115        assert_eq!(relaychain_config.chain().as_str(), "chain");
1116        assert_eq!(relaychain_config.nodes().len(), 1);
1117        assert_eq!(relaychain_config.group_node_configs().len(), 1);
1118        assert_eq!(
1119            relaychain_config
1120                .group_node_configs()
1121                .first()
1122                .unwrap()
1123                .count,
1124            2
1125        );
1126        let &node = relaychain_config.nodes().first().unwrap();
1127        assert_eq!(node.name(), "node");
1128        assert_eq!(node.command().unwrap().as_str(), "node_command");
1129
1130        let group_nodes = relaychain_config.group_node_configs();
1131        let group_base_node = group_nodes.first().unwrap();
1132        assert_eq!(group_base_node.base_config.name(), "group_node");
1133        assert_eq!(
1134            group_base_node.base_config.command().unwrap().as_str(),
1135            "some_command"
1136        );
1137        assert_eq!(
1138            group_base_node.base_config.image().unwrap().as_str(),
1139            "repo:image"
1140        );
1141        assert!(group_base_node.base_config.is_validator());
1142    }
1143
1144    #[test]
1145    fn relaychain_with_group_count_0_config_should_fail() {
1146        let relaychain_config = RelaychainConfigBuilder::new(Default::default())
1147            .with_chain("chain")
1148            .with_default_command("command")
1149            .with_validator(|node| node.with_name("node").with_command("node_command"))
1150            .with_node_group(|group| {
1151                group.with_count(0).with_base_node(|base| {
1152                    base.with_name("group_node")
1153                        .with_command("some_command")
1154                        .with_image("repo:image")
1155                        .validator(true)
1156                })
1157            })
1158            .build();
1159
1160        let errors: Vec<anyhow::Error> = match relaychain_config {
1161            Ok(_) => vec![],
1162            Err(errs) => errs,
1163        };
1164
1165        assert_eq!(errors.len(), 1);
1166        assert_eq!(
1167            errors.first().unwrap().to_string(),
1168            "relaychain.nodes['group_node'].Count cannot be zero"
1169        );
1170    }
1171}