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