zombienet_orchestrator/network/
parachain.rs

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