zombienet_orchestrator/network/
parachain.rs1use 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 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 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 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 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 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, ¶.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, ¶.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 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 let mut tx = api
207 .tx()
208 .sign_and_submit_then_watch_default(&sudo_call, &sudo)
209 .await?;
210
211 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 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(¶_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(¶_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 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}