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 #[serde(default)]
36 pub(crate) files_to_inject: Vec<TransferedFile>,
37 #[serde(default)]
38 pub(crate) bootnodes_addresses: Vec<multiaddr::Multiaddr>,
39}
40
41#[derive(Debug, Deserialize)]
42pub(crate) struct RawParachain {
43 #[serde(flatten)]
44 pub(crate) inner: Parachain,
45 pub(crate) collators: serde_json::Value,
46}
47
48#[async_trait]
49impl ChainUpgrade for Parachain {
50 async fn runtime_upgrade(&self, options: RuntimeUpgradeOptions) -> Result<(), anyhow::Error> {
51 let node = if let Some(node_name) = &options.node_name {
53 if let Some(node) = self
54 .collators()
55 .into_iter()
56 .find(|node| node.name() == node_name)
57 {
58 node
59 } else {
60 return Err(anyhow!("Node: {node_name} is not part of the set of nodes"));
61 }
62 } else {
63 if let Some(node) = self.collators().first() {
65 node
66 } else {
67 return Err(anyhow!("chain doesn't have any node!"));
68 }
69 };
70
71 self.perform_runtime_upgrade(node, options).await
72 }
73}
74
75impl Parachain {
76 pub(crate) fn new(para_id: u32, unique_id: impl Into<String>) -> Self {
77 Self {
78 chain: None,
79 para_id,
80 unique_id: unique_id.into(),
81 chain_id: None,
82 chain_spec_path: None,
83 collators: Default::default(),
84 files_to_inject: Default::default(),
85 bootnodes_addresses: vec![],
86 }
87 }
88
89 pub(crate) fn with_chain_spec(
90 para_id: u32,
91 unique_id: impl Into<String>,
92 chain_id: impl Into<String>,
93 chain_spec_path: impl AsRef<Path>,
94 ) -> Self {
95 Self {
96 para_id,
97 unique_id: unique_id.into(),
98 chain: None,
99 chain_id: Some(chain_id.into()),
100 chain_spec_path: Some(chain_spec_path.as_ref().into()),
101 collators: Default::default(),
102 files_to_inject: Default::default(),
103 bootnodes_addresses: vec![],
104 }
105 }
106
107 pub(crate) async fn from_spec(
108 para: &ParachainSpec,
109 files_to_inject: &[TransferedFile],
110 scoped_fs: &ScopedFilesystem<'_, impl FileSystem>,
111 ) -> Result<Self, anyhow::Error> {
112 let mut para_files_to_inject = files_to_inject.to_owned();
113
114 let mut parachain = if let Some(chain_spec) = para.chain_spec.as_ref() {
116 let id = chain_spec.read_chain_id(scoped_fs).await?;
117
118 let spec_name = chain_spec.chain_spec_name();
120 let base = PathBuf::from_str(scoped_fs.base_dir)?;
121 para_files_to_inject.push(TransferedFile::new(
122 base.join(format!("{spec_name}.json")),
123 PathBuf::from(format!("/cfg/{}.json", para.id)),
124 ));
125
126 let raw_path = chain_spec
127 .raw_path()
128 .ok_or(anyhow::anyhow!("chain-spec path should be set by now.",))?;
129 let mut running_para =
130 Parachain::with_chain_spec(para.id, ¶.unique_id, id, raw_path);
131 if let Some(chain_name) = chain_spec.chain_name() {
132 running_para.chain = Some(chain_name.to_string());
133 }
134
135 running_para
136 } else {
137 Parachain::new(para.id, ¶.unique_id)
138 };
139
140 parachain.bootnodes_addresses = para.bootnodes_addresses().into_iter().cloned().collect();
141 parachain.files_to_inject = para_files_to_inject;
142
143 Ok(parachain)
144 }
145
146 pub async fn register(
147 options: RegisterParachainOptions,
148 scoped_fs: &ScopedFilesystem<'_, impl FileSystem>,
149 ) -> Result<(), anyhow::Error> {
150 info!("Registering parachain: {:?}", options);
151 let sudo: Keypair;
153 if let Some(possible_seed) = options.seed {
154 sudo = Keypair::from_secret_key(possible_seed)
155 .expect(&format!("seed should return a Keypair {THIS_IS_A_BUG}"));
156 } else {
157 let uri = SecretUri::from_str("//Alice")?;
158 sudo = Keypair::from_uri(&uri)?;
159 }
160
161 let genesis_state = scoped_fs
162 .read_to_string(options.state_path)
163 .await
164 .expect(&format!(
165 "State Path should be ok by this point {THIS_IS_A_BUG}"
166 ));
167 let wasm_data = scoped_fs
168 .read_to_string(options.wasm_path)
169 .await
170 .expect(&format!(
171 "Wasm Path should be ok by this point {THIS_IS_A_BUG}"
172 ));
173
174 wait_ws_ready(options.node_ws_url.as_str())
175 .await
176 .map_err(|_| {
177 anyhow::anyhow!(
178 "Error waiting for ws to be ready, at {}",
179 options.node_ws_url.as_str()
180 )
181 })?;
182
183 let api: OnlineClient<SubstrateConfig> = get_client_from_url(&options.node_ws_url).await?;
184
185 let schedule_para = subxt::dynamic::tx(
186 "ParasSudoWrapper",
187 "sudo_schedule_para_initialize",
188 vec![
189 Value::primitive(options.id.into()),
190 Value::named_composite([
191 (
192 "genesis_head",
193 Value::from_bytes(hex::decode(&genesis_state[2..])?),
194 ),
195 (
196 "validation_code",
197 Value::from_bytes(hex::decode(&wasm_data[2..])?),
198 ),
199 ("para_kind", Value::bool(options.onboard_as_para)),
200 ]),
201 ],
202 );
203
204 let sudo_call = subxt::dynamic::tx("Sudo", "sudo", vec![schedule_para.into_value()]);
205
206 let mut tx = api
209 .tx()
210 .sign_and_submit_then_watch_default(&sudo_call, &sudo)
211 .await?;
212
213 while let Some(status) = tx.next().await {
216 match status? {
217 TxStatus::InBestBlock(tx_in_block) | TxStatus::InFinalizedBlock(tx_in_block) => {
218 let _result = tx_in_block.wait_for_success().await?;
219 info!("In block: {:#?}", tx_in_block.block_hash());
220 },
221 TxStatus::Error { message }
222 | TxStatus::Invalid { message }
223 | TxStatus::Dropped { message } => {
224 return Err(anyhow::format_err!("Error submitting tx: {message}"));
225 },
226 _ => continue,
227 }
228 }
229
230 Ok(())
231 }
232
233 pub fn para_id(&self) -> u32 {
234 self.para_id
235 }
236
237 pub fn unique_id(&self) -> &str {
238 self.unique_id.as_str()
239 }
240
241 pub fn chain_id(&self) -> Option<&str> {
242 self.chain_id.as_deref()
243 }
244
245 pub fn collators(&self) -> Vec<&NetworkNode> {
246 self.collators.iter().collect()
247 }
248
249 pub fn bootnodes_addresses(&self) -> Vec<&multiaddr::Multiaddr> {
250 self.bootnodes_addresses.iter().collect()
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use std::collections::HashMap;
257
258 use super::*;
259
260 #[test]
261 fn create_with_is_works() {
262 let para = Parachain::new(100, "100");
263 assert_eq!(para.para_id, 100);
265 assert_eq!(para.unique_id, "100");
266 assert_eq!(para.chain_id, None);
267 assert_eq!(para.chain, None);
268 assert_eq!(para.chain_spec_path, None);
269 }
270
271 #[test]
272 fn create_with_chain_spec_works() {
273 let para = Parachain::with_chain_spec(100, "100", "rococo-local", "/tmp/rococo-local.json");
274 assert_eq!(para.para_id, 100);
275 assert_eq!(para.unique_id, "100");
276 assert_eq!(para.chain_id, Some("rococo-local".to_string()));
277 assert_eq!(para.chain, None);
278 assert_eq!(
279 para.chain_spec_path,
280 Some(PathBuf::from("/tmp/rococo-local.json"))
281 );
282 }
283
284 #[tokio::test]
285 async fn create_with_para_spec_works() {
286 use configuration::ParachainConfigBuilder;
287
288 use crate::network_spec::parachain::ParachainSpec;
289
290 let bootnode_addresses = vec!["/ip4/10.41.122.55/tcp/45421"];
291
292 let para_config = ParachainConfigBuilder::new(Default::default())
293 .with_id(100)
294 .cumulus_based(false)
295 .with_default_command("adder-collator")
296 .with_raw_bootnodes_addresses(bootnode_addresses.clone())
297 .with_collator(|c| c.with_name("col"))
298 .build()
299 .unwrap();
300
301 let para_spec =
302 ParachainSpec::from_config(¶_config, "rococo-local".try_into().unwrap()).unwrap();
303 let fs = support::fs::in_memory::InMemoryFileSystem::new(HashMap::default());
304 let scoped_fs = ScopedFilesystem {
305 fs: &fs,
306 base_dir: "/tmp/some",
307 };
308
309 let files = vec![TransferedFile::new(
310 PathBuf::from("/tmp/some"),
311 PathBuf::from("/tmp/some"),
312 )];
313 let para = Parachain::from_spec(¶_spec, &files, &scoped_fs)
314 .await
315 .unwrap();
316 println!("{para:#?}");
317 assert_eq!(para.para_id, 100);
318 assert_eq!(para.unique_id, "100");
319 assert_eq!(para.chain_id, None);
320 assert_eq!(para.chain, None);
321 assert_eq!(para.files_to_inject.len(), 1);
323 assert_eq!(
324 para.bootnodes_addresses()
325 .iter()
326 .map(|addr| addr.to_string())
327 .collect::<Vec<_>>(),
328 bootnode_addresses
329 );
330 }
331
332 #[test]
333 fn genesis_state_precedence_uses_path_over_generator() {
334 use configuration::ParachainConfigBuilder;
335
336 use crate::network_spec::parachain::ParachainSpec;
337
338 let para_config = ParachainConfigBuilder::new(Default::default())
339 .with_id(101)
340 .with_genesis_state_path("./path/to/genesis/state")
341 .with_genesis_state_generator("generator_state --flag")
342 .with_collator(|c| c.with_name("col").with_command("cmd"))
343 .build()
344 .unwrap();
345
346 let para_spec =
347 ParachainSpec::from_config(¶_config, "relay".try_into().unwrap()).unwrap();
348
349 let debug = format!("{:?}", para_spec.genesis_state);
351 assert!(
352 debug.contains("Path("),
353 "expected genesis_state to be Path variant, got: {debug}"
354 );
355 }
356
357 #[test]
358 fn genesis_state_generator_with_args_preserved() {
359 use configuration::ParachainConfigBuilder;
360
361 use crate::network_spec::parachain::ParachainSpec;
362
363 let para_config = ParachainConfigBuilder::new(Default::default())
364 .with_id(102)
365 .with_genesis_state_generator(
366 "undying-collator export-genesis-state --pov-size=10000 --pvf-complexity=1",
367 )
368 .with_collator(|c| c.with_name("col").with_command("cmd"))
369 .build()
370 .unwrap();
371
372 let para_spec =
373 ParachainSpec::from_config(¶_config, "relay".try_into().unwrap()).unwrap();
374 let debug = format!("{:?}", para_spec.genesis_state);
375
376 assert!(
378 debug.contains("CommandWithCustomArgs"),
379 "expected CommandWithCustomArgs in debug, got: {debug}"
380 );
381 assert!(debug.contains("export-genesis-state"));
382 assert!(debug.contains("--pov-size"));
383 }
384}