zombienet_orchestrator/network/
parachain.rs1use 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 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 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 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 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 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, ¶.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, ¶.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 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 let mut tx = api
210 .tx()
211 .sign_and_submit_then_watch_default(&sudo_call, &sudo)
212 .await?;
213
214 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 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(¶_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(¶_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 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(¶_config, "relay".try_into().unwrap()).unwrap();
353
354 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(¶_config, "relay".try_into().unwrap()).unwrap();
379 let debug = format!("{:?}", para_spec.genesis_state);
380
381 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}