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