Skip to main content

zombienet_orchestrator/network/
parachain.rs

1use std::{
2    path::{Path, PathBuf},
3    str::FromStr,
4    sync::Arc,
5};
6
7use anyhow::anyhow;
8use async_trait::async_trait;
9use provider::types::TransferedFile;
10use serde::{Deserialize, Serialize};
11use subxt::{dynamic::Value, tx::TxStatus, OnlineClient, SubstrateConfig};
12use subxt_signer::{sr25519::Keypair, SecretUri};
13use support::{constants::THIS_IS_A_BUG, fs::FileSystem, net::wait_ws_ready};
14use tracing::info;
15
16use super::{chain_upgrade::ChainUpgrade, node::NetworkNode};
17use crate::{
18    network_spec::parachain::ParachainSpec,
19    shared::types::{RegisterParachainOptions, RuntimeUpgradeOptions},
20    tx_helper::client::get_client_from_url,
21    utils::default_as_empty_vec,
22    ScopedFilesystem,
23};
24
25#[derive(Debug, Serialize, Deserialize, Clone)]
26pub struct Parachain {
27    pub(crate) chain: Option<String>,
28    pub(crate) para_id: u32,
29    // unique_id is internally used to allow multiple parachains with the same id
30    // See `ParachainConfig` for more details
31    pub(crate) unique_id: String,
32    pub(crate) chain_id: Option<String>,
33    pub(crate) chain_spec_path: Option<PathBuf>,
34    #[serde(default, deserialize_with = "default_as_empty_vec")]
35    pub(crate) collators: Vec<Arc<NetworkNode>>,
36    #[serde(default)]
37    pub(crate) files_to_inject: Vec<TransferedFile>,
38    #[serde(default)]
39    pub(crate) bootnodes_addresses: Vec<multiaddr::Multiaddr>,
40}
41
42#[derive(Debug, Deserialize)]
43pub(crate) struct RawParachain {
44    #[serde(flatten)]
45    pub(crate) inner: Parachain,
46    pub(crate) collators: serde_json::Value,
47}
48
49#[async_trait]
50impl ChainUpgrade for Parachain {
51    async fn runtime_upgrade(&self, options: RuntimeUpgradeOptions) -> Result<(), anyhow::Error> {
52        // check if the node is valid first
53        let node = if let Some(node_name) = &options.node_name {
54            if let Some(node) = self
55                .collators()
56                .into_iter()
57                .find(|node| node.name() == node_name)
58            {
59                node
60            } else {
61                return Err(anyhow!("Node: {node_name} is not part of the set of nodes"));
62            }
63        } else {
64            // take the first node
65            if let Some(node) = self.collators().first() {
66                node
67            } else {
68                return Err(anyhow!("chain doesn't have any node!"));
69            }
70        };
71
72        self.perform_runtime_upgrade(node, options).await
73    }
74}
75
76impl Parachain {
77    pub(crate) fn new(para_id: u32, unique_id: impl Into<String>) -> Self {
78        Self {
79            chain: None,
80            para_id,
81            unique_id: unique_id.into(),
82            chain_id: None,
83            chain_spec_path: None,
84            collators: Default::default(),
85            files_to_inject: Default::default(),
86            bootnodes_addresses: vec![],
87        }
88    }
89
90    pub(crate) fn with_chain_spec(
91        para_id: u32,
92        unique_id: impl Into<String>,
93        chain_id: impl Into<String>,
94        chain_spec_path: impl AsRef<Path>,
95    ) -> Self {
96        Self {
97            para_id,
98            unique_id: unique_id.into(),
99            chain: None,
100            chain_id: Some(chain_id.into()),
101            chain_spec_path: Some(chain_spec_path.as_ref().into()),
102            collators: Default::default(),
103            files_to_inject: Default::default(),
104            bootnodes_addresses: vec![],
105        }
106    }
107
108    pub(crate) async fn from_spec(
109        para: &ParachainSpec,
110        files_to_inject: &[TransferedFile],
111        scoped_fs: &ScopedFilesystem<'_, impl FileSystem>,
112    ) -> Result<Self, anyhow::Error> {
113        let mut para_files_to_inject = files_to_inject.to_owned();
114
115        // parachain id is used for the keystore
116        let mut parachain = if let Some(chain_spec) = para.chain_spec.as_ref() {
117            let id = chain_spec.read_chain_id(scoped_fs).await?;
118
119            // add the spec to global files to inject
120            let spec_name = chain_spec.chain_spec_name();
121            let base = PathBuf::from_str(scoped_fs.base_dir)?;
122            para_files_to_inject.push(TransferedFile::new(
123                base.join(format!("{spec_name}.json")),
124                PathBuf::from(format!("/cfg/{}.json", para.id)),
125            ));
126
127            let raw_path = chain_spec
128                .raw_path()
129                .ok_or(anyhow::anyhow!("chain-spec path should be set by now.",))?;
130            let mut running_para =
131                Parachain::with_chain_spec(para.id, &para.unique_id, id, raw_path);
132            if let Some(chain_name) = chain_spec.chain_name() {
133                running_para.chain = Some(chain_name.to_string());
134            }
135
136            running_para
137        } else {
138            Parachain::new(para.id, &para.unique_id)
139        };
140
141        parachain.bootnodes_addresses = para.bootnodes_addresses().into_iter().cloned().collect();
142        parachain.files_to_inject = para_files_to_inject;
143
144        Ok(parachain)
145    }
146
147    pub async fn register(
148        options: RegisterParachainOptions,
149        scoped_fs: &ScopedFilesystem<'_, impl FileSystem>,
150    ) -> Result<(), anyhow::Error> {
151        info!("Registering parachain: {:?}", options);
152        // get the seed
153        let sudo: Keypair;
154        if let Some(possible_seed) = options.seed {
155            sudo = Keypair::from_secret_key(possible_seed)
156                .expect(&format!("seed should return a Keypair {THIS_IS_A_BUG}"));
157        } else {
158            let uri = SecretUri::from_str("//Alice")?;
159            sudo = Keypair::from_uri(&uri)?;
160        }
161
162        let genesis_state = scoped_fs
163            .read_to_string(options.state_path)
164            .await
165            .expect(&format!(
166                "State Path should be ok by this point {THIS_IS_A_BUG}"
167            ));
168        let wasm_data = scoped_fs
169            .read_to_string(options.wasm_path)
170            .await
171            .expect(&format!(
172                "Wasm Path should be ok by this point {THIS_IS_A_BUG}"
173            ));
174
175        wait_ws_ready(options.node_ws_url.as_str())
176            .await
177            .map_err(|_| {
178                anyhow::anyhow!(
179                    "Error waiting for ws to be ready, at {}",
180                    options.node_ws_url.as_str()
181                )
182            })?;
183
184        let api: OnlineClient<SubstrateConfig> = get_client_from_url(&options.node_ws_url).await?;
185
186        let schedule_para = subxt::dynamic::tx(
187            "ParasSudoWrapper",
188            "sudo_schedule_para_initialize",
189            vec![
190                Value::primitive(options.id.into()),
191                Value::named_composite([
192                    (
193                        "genesis_head",
194                        Value::from_bytes(hex::decode(&genesis_state[2..])?),
195                    ),
196                    (
197                        "validation_code",
198                        Value::from_bytes(hex::decode(&wasm_data[2..])?),
199                    ),
200                    ("para_kind", Value::bool(options.onboard_as_para)),
201                ]),
202            ],
203        );
204
205        let sudo_call = subxt::dynamic::tx("Sudo", "sudo", vec![schedule_para.into_value()]);
206
207        // TODO: uncomment below and fix the sign and submit (and follow afterwards until
208        // finalized block) to register the parachain
209        let mut tx = api
210            .tx()
211            .sign_and_submit_then_watch_default(&sudo_call, &sudo)
212            .await?;
213
214        // Below we use the low level API to replicate the `wait_for_in_block` behaviour
215        // which was removed in subxt 0.33.0. See https://github.com/paritytech/subxt/pull/1237.
216        while let Some(status) = tx.next().await {
217            match status? {
218                TxStatus::InBestBlock(tx_in_block) | TxStatus::InFinalizedBlock(tx_in_block) => {
219                    let _result = tx_in_block.wait_for_success().await?;
220                    info!("In block: {:#?}", tx_in_block.block_hash());
221                },
222                TxStatus::Error { message }
223                | TxStatus::Invalid { message }
224                | TxStatus::Dropped { message } => {
225                    return Err(anyhow::format_err!("Error submitting tx: {message}"));
226                },
227                _ => continue,
228            }
229        }
230
231        Ok(())
232    }
233
234    pub fn para_id(&self) -> u32 {
235        self.para_id
236    }
237
238    pub fn unique_id(&self) -> &str {
239        self.unique_id.as_str()
240    }
241
242    pub fn chain_id(&self) -> Option<&str> {
243        self.chain_id.as_deref()
244    }
245
246    pub fn collators(&self) -> Vec<&NetworkNode> {
247        self.collators.iter().map(|n| n.as_ref()).collect()
248    }
249
250    pub fn bootnodes_addresses(&self) -> Vec<&multiaddr::Multiaddr> {
251        self.bootnodes_addresses.iter().collect()
252    }
253
254    pub fn chain_spec_path(&self) -> Option<&Path> {
255        self.chain_spec_path.as_deref()
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use std::collections::HashMap;
262
263    use super::*;
264
265    #[test]
266    fn create_with_is_works() {
267        let para = Parachain::new(100, "100");
268        // only para_id and unique_id should be set
269        assert_eq!(para.para_id, 100);
270        assert_eq!(para.unique_id, "100");
271        assert_eq!(para.chain_id, None);
272        assert_eq!(para.chain, None);
273        assert_eq!(para.chain_spec_path, None);
274    }
275
276    #[test]
277    fn create_with_chain_spec_works() {
278        let para = Parachain::with_chain_spec(100, "100", "rococo-local", "/tmp/rococo-local.json");
279        assert_eq!(para.para_id, 100);
280        assert_eq!(para.unique_id, "100");
281        assert_eq!(para.chain_id, Some("rococo-local".to_string()));
282        assert_eq!(para.chain, None);
283        assert_eq!(
284            para.chain_spec_path,
285            Some(PathBuf::from("/tmp/rococo-local.json"))
286        );
287    }
288
289    #[tokio::test]
290    async fn create_with_para_spec_works() {
291        use configuration::ParachainConfigBuilder;
292
293        use crate::network_spec::parachain::ParachainSpec;
294
295        let bootnode_addresses = vec!["/ip4/10.41.122.55/tcp/45421"];
296
297        let para_config = ParachainConfigBuilder::new(Default::default())
298            .with_id(100)
299            .cumulus_based(false)
300            .with_default_command("adder-collator")
301            .with_raw_bootnodes_addresses(bootnode_addresses.clone())
302            .with_collator(|c| c.with_name("col"))
303            .build()
304            .unwrap();
305
306        let para_spec =
307            ParachainSpec::from_config(&para_config, "rococo-local".try_into().unwrap()).unwrap();
308        let fs = support::fs::in_memory::InMemoryFileSystem::new(HashMap::default());
309        let scoped_fs = ScopedFilesystem {
310            fs: &fs,
311            base_dir: "/tmp/some",
312        };
313
314        let files = vec![TransferedFile::new(
315            PathBuf::from("/tmp/some"),
316            PathBuf::from("/tmp/some"),
317        )];
318        let para = Parachain::from_spec(&para_spec, &files, &scoped_fs)
319            .await
320            .unwrap();
321        println!("{para:#?}");
322        assert_eq!(para.para_id, 100);
323        assert_eq!(para.unique_id, "100");
324        assert_eq!(para.chain_id, None);
325        assert_eq!(para.chain, None);
326        // one file should be added.
327        assert_eq!(para.files_to_inject.len(), 1);
328        assert_eq!(
329            para.bootnodes_addresses()
330                .iter()
331                .map(|addr| addr.to_string())
332                .collect::<Vec<_>>(),
333            bootnode_addresses
334        );
335    }
336
337    #[test]
338    fn genesis_state_precedence_uses_path_over_generator() {
339        use configuration::ParachainConfigBuilder;
340
341        use crate::network_spec::parachain::ParachainSpec;
342
343        let para_config = ParachainConfigBuilder::new(Default::default())
344            .with_id(101)
345            .with_genesis_state_path("./path/to/genesis/state")
346            .with_genesis_state_generator("generator_state --flag")
347            .with_collator(|c| c.with_name("col").with_command("cmd"))
348            .build()
349            .unwrap();
350
351        let para_spec =
352            ParachainSpec::from_config(&para_config, "relay".try_into().unwrap()).unwrap();
353
354        // ParaArtifact implements Debug; ensure the build option is the Path variant
355        let debug = format!("{:?}", para_spec.genesis_state);
356        assert!(
357            debug.contains("Path("),
358            "expected genesis_state to be Path variant, got: {debug}"
359        );
360    }
361
362    #[test]
363    fn genesis_state_generator_with_args_preserved() {
364        use configuration::ParachainConfigBuilder;
365
366        use crate::network_spec::parachain::ParachainSpec;
367
368        let para_config = ParachainConfigBuilder::new(Default::default())
369            .with_id(102)
370            .with_genesis_state_generator(
371                "undying-collator export-genesis-state --pov-size=10000 --pvf-complexity=1",
372            )
373            .with_collator(|c| c.with_name("col").with_command("cmd"))
374            .build()
375            .unwrap();
376
377        let para_spec =
378            ParachainSpec::from_config(&para_config, "relay".try_into().unwrap()).unwrap();
379        let debug = format!("{:?}", para_spec.genesis_state);
380
381        // Ensure CommandWithCustomArgs is used and arguments are present in debug output
382        assert!(
383            debug.contains("CommandWithCustomArgs"),
384            "expected CommandWithCustomArgs in debug, got: {debug}"
385        );
386        assert!(debug.contains("export-genesis-state"));
387        assert!(debug.contains("--pov-size"));
388    }
389}