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