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::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 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 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 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 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 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, ¶.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, ¶.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 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 let mut tx = api
196 .tx()
197 .sign_and_submit_then_watch_default(&sudo_call, &sudo)
198 .await?;
199
200 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 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(¶_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(¶_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 assert_eq!(para.files_to_inject.len(), 1);
302 }
303}