Skip to main content

zombienet_configuration/
jamchain.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, JamNodeConfig, JamNodeConfigBuilder},
12        resources::{Resources, ResourcesBuilder},
13        types::{
14            Arg, AssetLocation, Chain, ChainDefaultContext, Command, Image, ValidationContext,
15        },
16    },
17    types::{JamNodeMode, JamProtocolParameterType},
18    utils::{default_chain_jam, default_command_jam, default_protocol_para_type_jam},
19};
20/// A JAM chain configuration
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct JamchainConfig {
23    /// Protocol params type to use, default tiny (max_validators: 6)
24    #[serde(default = "default_protocol_para_type_jam")]
25    protocol_params_type: JamProtocolParameterType,
26    /// Id to use in config, default is `dev`
27    #[serde(default = "default_chain_jam")]
28    id: Chain,
29    /// Default command to use for nodes.
30    #[serde(default = "default_command_jam")]
31    default_command: Option<Command>,
32    /// Default image to use for nodes.
33    default_image: Option<Image>,
34    /// Default resources to use per pod.
35    default_resources: Option<Resources>,
36    /// Default arguments to pass to process (nodes).
37    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
38    default_args: Vec<Arg>,
39    /// chain-spec to use (location can be url or file path).
40    chain_spec_path: Option<AssetLocation>,
41    chain_spec_command: Option<Command>,
42    /// Command to spawn corevm monitor (if present)
43    corevm_monitor_command: Option<Command>,
44    /// Command to spawn corevm builder (if present)
45    corevm_builder_command: Option<Command>,
46    /// Genesis state beyond the validator set, merged into the config `gen-spec` reads.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    genesis_overrides: Option<serde_json::Value>,
49    /// A list of nodes to run in this chain.
50    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
51    nodes: Vec<JamNodeConfig>,
52}
53
54impl JamchainConfig {
55    /// The chain name.
56    pub fn id(&self) -> &Chain {
57        &self.id
58    }
59
60    /// The protocol parameters types used (default tiny)
61    pub fn protocol_params_type(&self) -> &JamProtocolParameterType {
62        &self.protocol_params_type
63    }
64
65    /// The default command used for nodes.
66    pub fn default_command(&self) -> Option<&Command> {
67        self.default_command.as_ref()
68    }
69
70    /// The default container image used for nodes.
71    pub fn default_image(&self) -> Option<&Image> {
72        self.default_image.as_ref()
73    }
74
75    /// The default resources limits used for nodes.
76    pub fn default_resources(&self) -> Option<&Resources> {
77        self.default_resources.as_ref()
78    }
79
80    /// The default arguments that will be used to launch the node command.
81    pub fn default_args(&self) -> Vec<&Arg> {
82        self.default_args.iter().collect::<Vec<&Arg>>()
83    }
84
85    /// The location of an pre-existing chain specification for the relay chain.
86    pub fn chain_spec_path(&self) -> Option<&AssetLocation> {
87        self.chain_spec_path.as_ref()
88    }
89
90    /// The location of an pre-existing chain specification for the relay chain.
91    pub fn chain_spec_command(&self) -> Option<&Command> {
92        self.chain_spec_command.as_ref()
93    }
94
95    /// The corevm monitor command used (if set).
96    pub fn corevm_monitor_command(&self) -> Option<&Command> {
97        self.corevm_monitor_command.as_ref()
98    }
99
100    /// The corevm builder command used (if set).
101    pub fn corevm_builder_command(&self) -> Option<&Command> {
102        self.corevm_builder_command.as_ref()
103    }
104
105    /// The genesis overrides merged into the config `gen-spec` reads (if set).
106    pub fn genesis_overrides(&self) -> Option<&serde_json::Value> {
107        self.genesis_overrides.as_ref()
108    }
109
110    /// The nodes of the relay chain.
111    pub fn nodes(&self) -> Vec<&JamNodeConfig> {
112        self.nodes.iter().collect::<Vec<&JamNodeConfig>>()
113    }
114}
115
116states! {
117    Initial,
118    WithId,
119    WithAtLeastOneNode
120}
121
122/// A relay chain configuration builder, used to build a [`JamchainConfig`] declaratively with fields validation.
123pub struct JamchainConfigBuilder<State> {
124    config: JamchainConfig,
125    validation_context: Rc<RefCell<ValidationContext>>,
126    errors: Vec<anyhow::Error>,
127    _state: PhantomData<State>,
128}
129
130impl Default for JamchainConfigBuilder<Initial> {
131    fn default() -> Self {
132        Self {
133            config: JamchainConfig {
134                protocol_params_type: JamProtocolParameterType::Tiny,
135                id: "dev"
136                    .try_into()
137                    .expect(&format!("{DEFAULT_TYPESTATE} {THIS_IS_A_BUG}")),
138                default_command: Some(
139                    "polkajam"
140                        .try_into()
141                        .expect(&format!("{DEFAULT_TYPESTATE} {THIS_IS_A_BUG}")),
142                ),
143                default_image: None,
144                default_resources: None,
145                default_args: vec![],
146                chain_spec_path: None,
147                chain_spec_command: None,
148                corevm_monitor_command: None,
149                corevm_builder_command: None,
150                genesis_overrides: None,
151                nodes: vec![],
152            },
153            validation_context: Default::default(),
154            errors: vec![],
155            _state: PhantomData,
156        }
157    }
158}
159
160impl<A> JamchainConfigBuilder<A> {
161    fn transition<B>(
162        config: JamchainConfig,
163        validation_context: Rc<RefCell<ValidationContext>>,
164        errors: Vec<anyhow::Error>,
165    ) -> JamchainConfigBuilder<B> {
166        JamchainConfigBuilder {
167            config,
168            validation_context,
169            errors,
170            _state: PhantomData,
171        }
172    }
173
174    fn default_chain_context(&self) -> ChainDefaultContext {
175        ChainDefaultContext {
176            default_command: self.config.default_command.clone(),
177            default_image: self.config.default_image.clone(),
178            default_resources: self.config.default_resources.clone(),
179            default_db_snapshot: None,
180            default_args: self.config.default_args.clone(),
181        }
182    }
183
184    fn create_node_builder<F>(&self, f: F) -> JamNodeConfigBuilder<node::Buildable>
185    where
186        F: FnOnce(JamNodeConfigBuilder<node::Initial>) -> JamNodeConfigBuilder<node::Buildable>,
187    {
188        f(JamNodeConfigBuilder::new(
189            self.default_chain_context(),
190            self.validation_context.clone(),
191        ))
192    }
193}
194
195impl JamchainConfigBuilder<Initial> {
196    pub fn new(
197        validation_context: Rc<RefCell<ValidationContext>>,
198    ) -> JamchainConfigBuilder<Initial> {
199        Self {
200            validation_context,
201            ..Self::default()
202        }
203    }
204
205    /// Set the id to use (e.g. dev).
206    pub fn with_id<T>(self, chain: T) -> JamchainConfigBuilder<WithId>
207    where
208        T: TryInto<Chain>,
209        T::Error: Error + Send + Sync + 'static,
210    {
211        match chain.try_into() {
212            Ok(id) => Self::transition(
213                JamchainConfig { id, ..self.config },
214                self.validation_context,
215                self.errors,
216            ),
217            Err(error) => Self::transition(
218                self.config,
219                self.validation_context,
220                merge_errors(self.errors, FieldError::Chain(error.into()).into()),
221            ),
222        }
223    }
224}
225
226impl JamchainConfigBuilder<WithId> {
227    /// Set the default command used for nodes. Can be overridden.
228    pub fn with_default_command<T>(self, command: T) -> Self
229    where
230        T: TryInto<Command>,
231        T::Error: Error + Send + Sync + 'static,
232    {
233        match command.try_into() {
234            Ok(command) => Self::transition(
235                JamchainConfig {
236                    default_command: Some(command),
237                    ..self.config
238                },
239                self.validation_context,
240                self.errors,
241            ),
242            Err(error) => Self::transition(
243                self.config,
244                self.validation_context,
245                merge_errors(self.errors, FieldError::DefaultCommand(error.into()).into()),
246            ),
247        }
248    }
249
250    /// Set the default container image used for nodes. Can be overridden.
251    pub fn with_default_image<T>(self, image: T) -> Self
252    where
253        T: TryInto<Image>,
254        T::Error: Error + Send + Sync + 'static,
255    {
256        match image.try_into() {
257            Ok(image) => Self::transition(
258                JamchainConfig {
259                    default_image: Some(image),
260                    ..self.config
261                },
262                self.validation_context,
263                self.errors,
264            ),
265            Err(error) => Self::transition(
266                self.config,
267                self.validation_context,
268                merge_errors(self.errors, FieldError::DefaultImage(error.into()).into()),
269            ),
270        }
271    }
272
273    /// Set the default resources limits used for nodes. Can be overridden.
274    pub fn with_default_resources(
275        self,
276        f: impl FnOnce(ResourcesBuilder) -> ResourcesBuilder,
277    ) -> Self {
278        match f(ResourcesBuilder::new()).build() {
279            Ok(default_resources) => Self::transition(
280                JamchainConfig {
281                    default_resources: Some(default_resources),
282                    ..self.config
283                },
284                self.validation_context,
285                self.errors,
286            ),
287            Err(errors) => Self::transition(
288                self.config,
289                self.validation_context,
290                merge_errors_vecs(
291                    self.errors,
292                    errors
293                        .into_iter()
294                        .map(|error| FieldError::DefaultResources(error).into())
295                        .collect::<Vec<_>>(),
296                ),
297            ),
298        }
299    }
300
301    /// Set the default arguments that will be used to execute the node command. Can be overridden.
302    pub fn with_default_args(self, args: Vec<Arg>) -> Self {
303        Self::transition(
304            JamchainConfig {
305                default_args: args,
306                ..self.config
307            },
308            self.validation_context,
309            self.errors,
310        )
311    }
312
313    /// Set the location of a pre-existing chain specification for the chain.
314    pub fn with_chain_spec_path(self, location: impl Into<AssetLocation>) -> Self {
315        Self::transition(
316            JamchainConfig {
317                chain_spec_path: Some(location.into()),
318                ..self.config
319            },
320            self.validation_context,
321            self.errors,
322        )
323    }
324
325    /// Set the command that generates the chain spec, when it is not the node binary itself.
326    ///
327    /// Only the command is used: the orchestrator appends `gen-spec <config> <spec>` itself.
328    pub fn with_chain_spec_command<T>(self, command: T) -> Self
329    where
330        T: TryInto<Command>,
331        T::Error: Error + Send + Sync + 'static,
332    {
333        match command.try_into() {
334            Ok(command) => Self::transition(
335                JamchainConfig {
336                    chain_spec_command: Some(command),
337                    ..self.config
338                },
339                self.validation_context,
340                self.errors,
341            ),
342            Err(error) => Self::transition(
343                self.config,
344                self.validation_context,
345                merge_errors(self.errors, FieldError::Command(error.into()).into()),
346            ),
347        }
348    }
349
350    /// Set the genesis overrides: a JSON object merged into the config `gen-spec` reads, on top
351    /// of the generated `id` and `genesis_validators`. Its keys are `gen-spec`'s business.
352    pub fn with_genesis_overrides(self, genesis_overrides: impl Into<serde_json::Value>) -> Self {
353        Self::transition(
354            JamchainConfig {
355                genesis_overrides: Some(genesis_overrides.into()),
356                ..self.config
357            },
358            self.validation_context,
359            self.errors,
360        )
361    }
362
363    /// Set the command used for corevm monitor.
364    pub fn with_corevm_monitor_command<T>(self, command: T) -> Self
365    where
366        T: TryInto<Command>,
367        T::Error: Error + Send + Sync + 'static,
368    {
369        match command.try_into() {
370            Ok(command) => Self::transition(
371                JamchainConfig {
372                    corevm_monitor_command: Some(command),
373                    ..self.config
374                },
375                self.validation_context,
376                self.errors,
377            ),
378            Err(error) => Self::transition(
379                self.config,
380                self.validation_context,
381                merge_errors(self.errors, FieldError::DefaultCommand(error.into()).into()),
382            ),
383        }
384    }
385
386    /// Set the command used for corevm builder.
387    pub fn with_corevm_builder_command<T>(self, command: T) -> Self
388    where
389        T: TryInto<Command>,
390        T::Error: Error + Send + Sync + 'static,
391    {
392        match command.try_into() {
393            Ok(command) => Self::transition(
394                JamchainConfig {
395                    corevm_builder_command: Some(command),
396                    ..self.config
397                },
398                self.validation_context,
399                self.errors,
400            ),
401            Err(error) => Self::transition(
402                self.config,
403                self.validation_context,
404                merge_errors(self.errors, FieldError::DefaultCommand(error.into()).into()),
405            ),
406        }
407    }
408
409    /// Add a new validator node using a nested [`JamNodeConfigBuilder`].
410    /// The node will be configured as a validator (authority).
411    pub fn with_validator(
412        self,
413        f: impl FnOnce(JamNodeConfigBuilder<node::Initial>) -> JamNodeConfigBuilder<node::Buildable>,
414    ) -> JamchainConfigBuilder<WithAtLeastOneNode> {
415        match self
416            .create_node_builder(f)
417            .with_mode(JamNodeMode::Validator)
418            .build()
419        {
420            Ok(node) => Self::transition(
421                JamchainConfig {
422                    nodes: [self.config.nodes, vec![node]].concat(),
423                    ..self.config
424                },
425                self.validation_context,
426                self.errors,
427            ),
428            Err((name, errors)) => Self::transition(
429                self.config,
430                self.validation_context,
431                merge_errors_vecs(
432                    self.errors,
433                    errors
434                        .into_iter()
435                        .map(|error| ConfigError::Node(name.clone(), error).into())
436                        .collect::<Vec<_>>(),
437                ),
438            ),
439        }
440    }
441
442    /// Add a new ordinary node using a nested [`JamNodeConfigBuilder`].
443    pub fn with_ordinary(
444        self,
445        f: impl FnOnce(JamNodeConfigBuilder<node::Initial>) -> JamNodeConfigBuilder<node::Buildable>,
446    ) -> JamchainConfigBuilder<WithAtLeastOneNode> {
447        match self
448            .create_node_builder(f)
449            .with_mode(JamNodeMode::Ordinary)
450            .build()
451        {
452            Ok(node) => Self::transition(
453                JamchainConfig {
454                    nodes: [self.config.nodes, vec![node]].concat(),
455                    ..self.config
456                },
457                self.validation_context,
458                self.errors,
459            ),
460            Err((name, errors)) => Self::transition(
461                self.config,
462                self.validation_context,
463                merge_errors_vecs(
464                    self.errors,
465                    errors
466                        .into_iter()
467                        .map(|error| ConfigError::Node(name.clone(), error).into())
468                        .collect::<Vec<_>>(),
469                ),
470            ),
471        }
472    }
473
474    /// Add a new proxy node using a nested [`JamNodeConfigBuilder`].
475    pub fn with_proxy(
476        self,
477        f: impl FnOnce(JamNodeConfigBuilder<node::Initial>) -> JamNodeConfigBuilder<node::Buildable>,
478    ) -> JamchainConfigBuilder<WithAtLeastOneNode> {
479        match self
480            .create_node_builder(f)
481            .with_mode(JamNodeMode::Proxy)
482            .build()
483        {
484            Ok(node) => Self::transition(
485                JamchainConfig {
486                    nodes: [self.config.nodes, vec![node]].concat(),
487                    ..self.config
488                },
489                self.validation_context,
490                self.errors,
491            ),
492            Err((name, errors)) => Self::transition(
493                self.config,
494                self.validation_context,
495                merge_errors_vecs(
496                    self.errors,
497                    errors
498                        .into_iter()
499                        .map(|error| ConfigError::Node(name.clone(), error).into())
500                        .collect::<Vec<_>>(),
501                ),
502            ),
503        }
504    }
505
506    // /// Helper fn to setup nodes (validators / ordinary / proxy )
507    // pub fn with_nodes_setup<T>(
508    //     self,
509    //     val_count: usize,
510    //     ordinary_count: usize,
511    //     proxy_count: usize,
512    // ) -> JamchainConfigBuilder<WithAtLeastOneNode> {
513    //     let mut nodes = vec![];
514    //     let tot = val_count + ordinary_count + proxy_count;
515    //     for i in 0..tot {}
516
517    //     Self::transition(
518    //         JamchainConfig {
519    //             nodes,
520    //             ..self.config
521    //         },
522    //         self.validation_context,
523    //         self.errors,
524    //     )
525    // }
526}
527
528impl JamchainConfigBuilder<WithAtLeastOneNode> {
529    /// Set the genesis overrides: a JSON object merged into the config `gen-spec` reads, on top
530    /// of the generated `id` and `genesis_validators`. Its keys are `gen-spec`'s business.
531    pub fn with_genesis_overrides(self, genesis_overrides: impl Into<serde_json::Value>) -> Self {
532        Self::transition(
533            JamchainConfig {
534                genesis_overrides: Some(genesis_overrides.into()),
535                ..self.config
536            },
537            self.validation_context,
538            self.errors,
539        )
540    }
541
542    /// Add a new validator node using a nested [`JamNodeConfigBuilder`].
543    /// The node will be configured as a validator (authority).
544    pub fn with_validator(
545        self,
546        f: impl FnOnce(JamNodeConfigBuilder<node::Initial>) -> JamNodeConfigBuilder<node::Buildable>,
547    ) -> JamchainConfigBuilder<WithAtLeastOneNode> {
548        match self
549            .create_node_builder(f)
550            .with_mode(JamNodeMode::Validator)
551            .build()
552        {
553            Ok(node) => Self::transition(
554                JamchainConfig {
555                    nodes: [self.config.nodes, vec![node]].concat(),
556                    ..self.config
557                },
558                self.validation_context,
559                self.errors,
560            ),
561            Err((name, errors)) => Self::transition(
562                self.config,
563                self.validation_context,
564                merge_errors_vecs(
565                    self.errors,
566                    errors
567                        .into_iter()
568                        .map(|error| ConfigError::Node(name.clone(), error).into())
569                        .collect::<Vec<_>>(),
570                ),
571            ),
572        }
573    }
574
575    /// Add a new ordinary node using a nested [`JamNodeConfigBuilder`].
576    pub fn with_ordinary(
577        self,
578        f: impl FnOnce(JamNodeConfigBuilder<node::Initial>) -> JamNodeConfigBuilder<node::Buildable>,
579    ) -> JamchainConfigBuilder<WithAtLeastOneNode> {
580        match self
581            .create_node_builder(f)
582            .with_mode(JamNodeMode::Ordinary)
583            .build()
584        {
585            Ok(node) => Self::transition(
586                JamchainConfig {
587                    nodes: [self.config.nodes, vec![node]].concat(),
588                    ..self.config
589                },
590                self.validation_context,
591                self.errors,
592            ),
593            Err((name, errors)) => Self::transition(
594                self.config,
595                self.validation_context,
596                merge_errors_vecs(
597                    self.errors,
598                    errors
599                        .into_iter()
600                        .map(|error| ConfigError::Node(name.clone(), error).into())
601                        .collect::<Vec<_>>(),
602                ),
603            ),
604        }
605    }
606
607    /// Add a new proxy node using a nested [`JamNodeConfigBuilder`].
608    pub fn with_proxy(
609        self,
610        f: impl FnOnce(JamNodeConfigBuilder<node::Initial>) -> JamNodeConfigBuilder<node::Buildable>,
611    ) -> JamchainConfigBuilder<WithAtLeastOneNode> {
612        match self
613            .create_node_builder(f)
614            .with_mode(JamNodeMode::Proxy)
615            .build()
616        {
617            Ok(node) => Self::transition(
618                JamchainConfig {
619                    nodes: [self.config.nodes, vec![node]].concat(),
620                    ..self.config
621                },
622                self.validation_context,
623                self.errors,
624            ),
625            Err((name, errors)) => Self::transition(
626                self.config,
627                self.validation_context,
628                merge_errors_vecs(
629                    self.errors,
630                    errors
631                        .into_iter()
632                        .map(|error| ConfigError::Node(name.clone(), error).into())
633                        .collect::<Vec<_>>(),
634                ),
635            ),
636        }
637    }
638
639    /// Seals the builder and returns a [`JamchainConfig`] if there are no validation errors, else returns errors.
640    pub fn build(self) -> Result<JamchainConfig, Vec<anyhow::Error>> {
641        if !self.errors.is_empty() {
642            return Err(self
643                .errors
644                .into_iter()
645                .map(|error| ConfigError::Jamchain(error).into())
646                .collect::<Vec<_>>());
647        }
648
649        Ok(self.config)
650    }
651}
652
653#[cfg(test)]
654mod tests {
655    use serde_json::json;
656
657    use super::*;
658
659    fn overrides() -> serde_json::Value {
660        json!({
661            "services": [{ "id": 5, "code": "/blobs/parasim-service.jam" }],
662            "assigners": { "0": 5 },
663        })
664    }
665
666    /// The override is the caller's business: it reaches `gen-spec` untouched, so nothing here
667    /// knows or checks what its keys mean.
668    #[test]
669    fn genesis_overrides_are_kept_as_given() {
670        let config = JamchainConfigBuilder::new(Default::default())
671            .with_id("dev")
672            .with_genesis_overrides(overrides())
673            .with_validator(|node| node.with_name("jam0"))
674            .build()
675            .expect("the chain config builds");
676
677        assert_eq!(config.genesis_overrides(), Some(&overrides()));
678    }
679
680    /// Callers set the override after the nodes as naturally as before them.
681    #[test]
682    fn genesis_overrides_can_follow_the_nodes() {
683        let config = JamchainConfigBuilder::new(Default::default())
684            .with_id("dev")
685            .with_validator(|node| node.with_name("jam0"))
686            .with_genesis_overrides(overrides())
687            .build()
688            .expect("the chain config builds");
689
690        assert_eq!(config.genesis_overrides(), Some(&overrides()));
691    }
692
693    #[test]
694    fn genesis_overrides_are_absent_by_default() {
695        let config = JamchainConfigBuilder::new(Default::default())
696            .with_id("dev")
697            .with_validator(|node| node.with_name("jam0"))
698            .build()
699            .expect("the chain config builds");
700
701        assert_eq!(config.genesis_overrides(), None);
702    }
703}