diff --git a/Cargo.lock b/Cargo.lock index f581a91d5..ac67e52da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9469,6 +9469,7 @@ dependencies = [ "strum 0.26.3", "strum_macros 0.26.4", "subxt", + "subxt-signer", "symlink", "tar", "tempfile", diff --git a/crates/pop-cli/src/commands/call/chain.rs b/crates/pop-cli/src/commands/call/chain.rs index 41348826f..18f61fe77 100644 --- a/crates/pop-cli/src/commands/call/chain.rs +++ b/crates/pop-cli/src/commands/call/chain.rs @@ -403,7 +403,7 @@ impl Call { }, }; // If sudo is required, wrap the call in a sudo call. - let xt = if self.sudo { construct_sudo_extrinsic(xt)? } else { xt }; + let xt = if self.sudo { construct_sudo_extrinsic(xt) } else { xt }; let encoded_data = encode_call_data(client, &xt)?; // If the encoded call data is too long, don't display it all. if encoded_data.len() < ENCODED_CALL_DATA_MAX_LEN { diff --git a/crates/pop-cli/src/commands/up/parachain.rs b/crates/pop-cli/src/commands/up/parachain.rs index 4d59646d0..30fc6fce0 100644 --- a/crates/pop-cli/src/commands/up/parachain.rs +++ b/crates/pop-cli/src/commands/up/parachain.rs @@ -3,13 +3,13 @@ use crate::style::{style, Theme}; use clap::Args; use cliclack::{ - clear_screen, confirm, intro, log, multi_progress, outro, outro_cancel, set_theme, ProgressBar, - Theme as _, ThemeState, + clear_screen, confirm, intro, log, multi_progress, outro, outro_cancel, set_theme, spinner, + ProgressBar, Theme as _, ThemeState, }; use console::{Emoji, Style, Term}; use duct::cmd; use pop_common::Status; -use pop_parachains::{Error, IndexSet, NetworkNode, Zombienet}; +use pop_parachains::{clear_dmpq, Error, IndexSet, NetworkNode, RelayChain, Zombienet}; use std::{path::Path, time::Duration}; use tokio::time::sleep; @@ -91,8 +91,8 @@ impl ZombienetCommand { } // Finally spawn network and wait for signal to terminate - let spinner = cliclack::spinner(); - spinner.start("🚀 Launching local network..."); + let progress = spinner(); + progress.start("🚀 Launching local network..."); match zombienet.spawn().await { Ok(network) => { let mut result = @@ -143,10 +143,39 @@ impl ZombienetCommand { } if let Some(command) = &self.command { - run_custom_command(&spinner, command).await?; + run_custom_command(&progress, command).await?; + } + + progress.stop(result); + + // Check for any specified channels + if zombienet.hrmp_channels() { + let relay_chain = zombienet.relay_chain(); + match RelayChain::from(relay_chain) { + None => { + log::error(format!("🚫 Using `{relay_chain}` with HRMP channels is currently unsupported. Please use `paseo-local` or `westend-local`."))?; + }, + Some(_) => { + let progress = spinner(); + progress.start("Connecting to relay chain to prepare channels..."); + // Allow relay node time to start + sleep(Duration::from_secs(10)).await; + progress.set_message("Preparing channels..."); + let relay_endpoint = network.relaychain().nodes()[0].client().await?; + let para_ids: Vec<_> = + network.parachains().iter().map(|p| p.para_id()).collect(); + tokio::spawn(async move { + if let Err(e) = clear_dmpq(relay_endpoint, ¶_ids).await { + progress.stop(format!("🚫 Could not prepare channels: {e}")); + return Ok::<(), Error>(()); + } + progress.stop("Channels successfully prepared for initialization."); + Ok::<(), Error>(()) + }); + }, + } } - spinner.stop(result); tokio::signal::ctrl_c().await?; outro("Done")?; }, diff --git a/crates/pop-parachains/Cargo.toml b/crates/pop-parachains/Cargo.toml index 6380fbd10..89c1df6d9 100644 --- a/crates/pop-parachains/Cargo.toml +++ b/crates/pop-parachains/Cargo.toml @@ -17,6 +17,8 @@ glob.workspace = true serde_json.workspace = true strum.workspace = true strum_macros.workspace = true +subxt-signer.workspace = true +subxt.workspace = true tar.workspace = true tempfile.workspace = true thiserror.workspace = true @@ -29,7 +31,6 @@ reqwest.workspace = true scale-info.workspace = true scale-value.workspace = true sp-core.workspace = true -subxt.workspace = true symlink.workspace = true toml_edit.workspace = true walkdir.workspace = true diff --git a/crates/pop-parachains/src/call/mod.rs b/crates/pop-parachains/src/call/mod.rs index f5bb05b71..4569687ec 100644 --- a/crates/pop-parachains/src/call/mod.rs +++ b/crates/pop-parachains/src/call/mod.rs @@ -41,8 +41,8 @@ pub fn construct_extrinsic( /// # Arguments /// * `xt`: The extrinsic representing the dispatchable function call to be dispatched with `Root` /// privileges. -pub fn construct_sudo_extrinsic(xt: DynamicPayload) -> Result { - Ok(subxt::dynamic::tx("Sudo", "sudo", [xt.into_value()].to_vec())) +pub fn construct_sudo_extrinsic(xt: DynamicPayload) -> DynamicPayload { + subxt::dynamic::tx("Sudo", "sudo", [xt.into_value()].to_vec()) } /// Signs and submits a given extrinsic. @@ -255,7 +255,7 @@ mod tests { "100".to_string(), ], )?; - let xt = construct_sudo_extrinsic(xt)?; + let xt = construct_sudo_extrinsic(xt); assert_eq!(xt.call_name(), "sudo"); assert_eq!(xt.pallet_name(), "Sudo"); Ok(()) diff --git a/crates/pop-parachains/src/errors.rs b/crates/pop-parachains/src/errors.rs index 8bd97434a..2dd8805ac 100644 --- a/crates/pop-parachains/src/errors.rs +++ b/crates/pop-parachains/src/errors.rs @@ -62,6 +62,9 @@ pub enum Error { RustfmtError(std::io::Error), #[error("Template error: {0}")] SourcingError(#[from] pop_common::sourcing::Error), + /// An error occurred whilst interacting with a chain using `subxt`. + #[error("Subxt error: {0}")] + SubXtError(#[from] subxt::Error), #[error("Toml error: {0}")] TomlError(#[from] toml_edit::de::Error), #[error("Unsupported command: {0}")] diff --git a/crates/pop-parachains/src/lib.rs b/crates/pop-parachains/src/lib.rs index 7e70c2149..50bb6af5b 100644 --- a/crates/pop-parachains/src/lib.rs +++ b/crates/pop-parachains/src/lib.rs @@ -8,6 +8,7 @@ mod errors; mod generator; mod new_pallet; mod new_parachain; +mod relay; mod templates; mod up; mod utils; @@ -30,6 +31,7 @@ pub use errors::Error; pub use indexmap::IndexSet; pub use new_pallet::{create_pallet_template, new_pallet_options::*, TemplatePalletConfig}; pub use new_parachain::instantiate_template_dir; +pub use relay::{clear_dmpq, RelayChain}; // External export from subxt. pub use subxt::{ tx::{DynamicPayload, Payload}, diff --git a/crates/pop-parachains/src/relay.rs b/crates/pop-parachains/src/relay.rs new file mode 100644 index 000000000..6ded8c497 --- /dev/null +++ b/crates/pop-parachains/src/relay.rs @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-3.0 + +use crate::{call, DynamicPayload, Error}; +use sp_core::twox_128; +use subxt::{ + config::BlockHash, + dynamic::{self, Value}, + ext::sp_core, + OnlineClient, PolkadotConfig, +}; + +/// Clears the DMPQ state for the given parachain IDs. +/// +/// # Arguments +/// * `client` - Client for the network which state is to be modified. +/// * `para_ids` - List of ids to build the keys that will be mutated. +pub async fn clear_dmpq( + client: OnlineClient, + para_ids: &[u32], +) -> Result { + // Wait for blocks to be produced. + let mut sub = client.blocks().subscribe_finalized().await?; + for _ in 0..2 { + sub.next().await; + } + + // Generate storage keys to be removed + let clear_dmq_keys = generate_storage_keys(para_ids); + + // Submit calls to remove specified keys + let kill_storage = construct_kill_storage_call(clear_dmq_keys); + let sudo = subxt_signer::sr25519::dev::alice(); + let sudo_call = call::construct_sudo_extrinsic(kill_storage); + Ok(client.tx().sign_and_submit_default(&sudo_call, &sudo).await?) +} + +fn construct_kill_storage_call(keys: Vec>) -> DynamicPayload { + dynamic::tx( + "System", + "kill_storage", + vec![Value::unnamed_composite(keys.into_iter().map(Value::from_bytes))], + ) +} + +fn generate_storage_keys(para_ids: &[u32]) -> Vec> { + let dmp = twox_128("Dmp".as_bytes()); + let dmp_queue_heads = twox_128("DownwardMessageQueueHeads".as_bytes()); + let dmp_queues = twox_128("DownwardMessageQueues".as_bytes()); + let mut clear_dmq_keys = Vec::>::new(); + for id in para_ids { + let id = id.to_le_bytes(); + // DMP Queue Head + let mut key = dmp.to_vec(); + key.extend(&dmp_queue_heads); + key.extend(sp_core::twox_64(&id)); + key.extend(id); + clear_dmq_keys.push(key); + // DMP Queue + let mut key = dmp.to_vec(); + key.extend(&dmp_queues); + key.extend(sp_core::twox_64(&id)); + key.extend(id); + clear_dmq_keys.push(key); + } + clear_dmq_keys +} + +/// A supported relay chain. +#[derive(Debug, PartialEq)] +pub enum RelayChain { + /// Paseo. + PaseoLocal, + /// Westend. + WestendLocal, +} + +impl RelayChain { + /// Attempts to convert a chain identifier into a supported `RelayChain` variant. + /// + /// # Arguments + /// * `id` - The relay chain identifier. + pub fn from(id: &str) -> Option { + match id { + "paseo-local" => Some(RelayChain::PaseoLocal), + "westend-local" => Some(RelayChain::WestendLocal), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use subxt::ext::sp_core::twox_64; + use RelayChain::*; + + #[test] + fn construct_kill_storage_call_works() { + let keys = vec!["key".as_bytes().to_vec()]; + assert_eq!( + construct_kill_storage_call(keys.clone()), + dynamic::tx( + "System", + "kill_storage", + vec![Value::unnamed_composite(keys.into_iter().map(Value::from_bytes))], + ) + ) + } + + #[test] + fn generate_storage_keys_works() { + let para_ids = vec![1_000, 4_385]; + let dmp = twox_128("Dmp".as_bytes()); + let dmp_queue_heads = [dmp, twox_128("DownwardMessageQueueHeads".as_bytes())].concat(); + let dmp_queues = [dmp, twox_128("DownwardMessageQueues".as_bytes())].concat(); + + assert_eq!( + generate_storage_keys(¶_ids), + para_ids + .iter() + .flat_map(|id| { + let id = id.to_le_bytes().to_vec(); + [ + // DMP Queue Head + [dmp_queue_heads.clone(), twox_64(&id).to_vec(), id.clone()].concat(), + // DMP Queue + [dmp_queues.clone(), twox_64(&id).to_vec(), id].concat(), + ] + }) + .collect::>() + ) + } + + #[test] + fn supported_relay_chains() { + for (s, e) in [ + // Only chains with sudo supported + ("paseo-local", Some(PaseoLocal)), + ("westend-local", Some(WestendLocal)), + ("kusama-local", None), + ("polkadot-local", None), + ] { + assert_eq!(RelayChain::from(s), e) + } + } +} diff --git a/crates/pop-parachains/src/up/mod.rs b/crates/pop-parachains/src/up/mod.rs index 036ae889d..91c1678af 100644 --- a/crates/pop-parachains/src/up/mod.rs +++ b/crates/pop-parachains/src/up/mod.rs @@ -30,6 +30,8 @@ pub struct Zombienet { relay_chain: RelayChain, /// The configuration required to launch parachains. parachains: IndexMap, + /// Whether any HRMP channels are to be pre-opened. + hrmp_channels: bool, } impl Zombienet { @@ -59,7 +61,7 @@ impl Zombienet { // Parse network config let network_config = NetworkConfiguration::from(network_config)?; // Determine relay and parachain requirements based on arguments and config - let relay_chain = Self::relay_chain( + let relay_chain = Self::init_relay_chain( relay_chain_version, relay_chain_runtime_version, &network_config, @@ -84,7 +86,9 @@ impl Zombienet { cache, ) .await?; - Ok(Self { network_config, relay_chain, parachains }) + let hrmp_channels = + network_config.hrmp_channels().map(|c| !c.is_empty()).unwrap_or_default(); + Ok(Self { network_config, relay_chain, parachains, hrmp_channels }) } /// The binaries required to launch the network. @@ -216,7 +220,7 @@ impl Zombienet { /// will use the latest available version). /// * `network_config` - The network configuration to be used to launch a network. /// * `cache` - The location used for caching binaries. - async fn relay_chain( + async fn init_relay_chain( version: Option<&str>, runtime_version: Option<&str>, network_config: &NetworkConfiguration, @@ -276,6 +280,16 @@ impl Zombienet { Ok(relay::default(version, runtime_version, chain, cache).await?) } + /// The name of the relay chain. + pub fn relay_chain(&self) -> &str { + &self.relay_chain.chain + } + + /// Whether any HRMP channels are to be pre-opened. + pub fn hrmp_channels(&self) -> bool { + self.hrmp_channels + } + /// Launches the local network. pub async fn spawn(&mut self) -> Result, Error> { // Symlink polkadot workers @@ -354,6 +368,11 @@ impl NetworkConfiguration { self.0.get_mut("parachains").and_then(|p| p.as_array_of_tables_mut()) } + /// Returns the `hrmp_channels` configuration. + fn hrmp_channels(&self) -> Option<&ArrayOfTables> { + self.0.get("hrmp_channels").and_then(|p| p.as_array_of_tables()) + } + /// Returns the `command` configuration. fn command(config: &Table) -> Option<&Item> { config.get("command") @@ -706,6 +725,8 @@ chain = "paseo-local" if *tag == Some(version.to_string()) )); assert!(zombienet.parachains.is_empty()); + assert_eq!(zombienet.relay_chain(), "paseo-local"); + assert!(!zombienet.hrmp_channels()); Ok(()) } @@ -1186,6 +1207,48 @@ default_command = "moonbeam" Ok(()) } + #[tokio::test] + async fn new_with_hrmp_channels_works() -> Result<()> { + let temp_dir = tempdir()?; + let cache = PathBuf::from(temp_dir.path()); + let config = Builder::new().suffix(".toml").tempfile()?; + writeln!( + config.as_file(), + r#" +[relaychain] +chain = "paseo-local" + +[[parachains]] +id = 1000 +chain = "asset-hub-paseo-local" + +[[parachains]] +id = 4385 +default_command = "pop-node" + +[[hrmp_channels]] +sender = 4385 +recipient = 1000 +max_capacity = 1000 +max_message_size = 8000 +"# + )?; + + let zombienet = Zombienet::new( + &cache, + config.path().to_str().unwrap(), + None, + None, + None, + None, + None, + ) + .await?; + + assert!(zombienet.hrmp_channels()); + Ok(()) + } + #[tokio::test] async fn new_ensures_parachain_id_exists() -> Result<()> { let temp_dir = tempdir()?; diff --git a/crates/pop-parachains/src/utils/helpers.rs b/crates/pop-parachains/src/utils/helpers.rs index bd39eec9e..fd8e91d68 100644 --- a/crates/pop-parachains/src/utils/helpers.rs +++ b/crates/pop-parachains/src/utils/helpers.rs @@ -33,7 +33,8 @@ pub fn is_initial_endowment_valid(initial_endowment: &str) -> bool { initial_endowment.parse::().is_ok() || is_valid_bitwise_left_shift(initial_endowment).is_ok() } -// Auxiliar method to check if the endowment input with a shift left (1u64 << 60) format is valid. + +// Auxiliary method to check if the endowment input with a shift left (1u64 << 60) format is valid. // Parse the self << rhs format and check the shift left operation is valid. fn is_valid_bitwise_left_shift(initial_endowment: &str) -> Result { let v: Vec<&str> = initial_endowment.split(" << ").collect(); @@ -87,6 +88,7 @@ mod tests { use super::*; use crate::generator::parachain::ChainSpec; use askama::Template; + use std::env::var; use tempfile::tempdir; #[test] diff --git a/tests/networks/kusama+asset-hub.toml b/tests/networks/kusama+asset-hub.toml new file mode 100644 index 000000000..c652bd841 --- /dev/null +++ b/tests/networks/kusama+asset-hub.toml @@ -0,0 +1,19 @@ +# pop up parachain -f ./tests/networks/kusama+asset-hub.toml + +[relaychain] +chain = "kusama-local" + +[[relaychain.nodes]] +name = "alice" +validator = true + +[[relaychain.nodes]] +name = "bob" +validator = true + +[[parachains]] +id = 1000 +chain = "asset-hub-kusama-local" + +[[parachains.collators]] +name = "asset-hub" \ No newline at end of file diff --git a/tests/networks/paseo+asset-hub.toml b/tests/networks/paseo+asset-hub.toml new file mode 100644 index 000000000..a75719376 --- /dev/null +++ b/tests/networks/paseo+asset-hub.toml @@ -0,0 +1,19 @@ +# pop up parachain -f ./tests/networks/paseo+asset-hub.toml + +[relaychain] +chain = "paseo-local" + +[[relaychain.nodes]] +name = "alice" +validator = true + +[[relaychain.nodes]] +name = "bob" +validator = true + +[[parachains]] +id = 1000 +chain = "asset-hub-paseo-local" + +[[parachains.collators]] +name = "asset-hub" \ No newline at end of file diff --git a/tests/networks/polkadot+asset-hub.toml b/tests/networks/polkadot+asset-hub.toml new file mode 100644 index 000000000..9cf6735b2 --- /dev/null +++ b/tests/networks/polkadot+asset-hub.toml @@ -0,0 +1,19 @@ +# pop up parachain -f ./tests/networks/polkadot+asset-hub.toml + +[relaychain] +chain = "polkadot-local" + +[[relaychain.nodes]] +name = "alice" +validator = true + +[[relaychain.nodes]] +name = "bob" +validator = true + +[[parachains]] +id = 1000 +chain = "asset-hub-polkadot-local" + +[[parachains.collators]] +name = "asset-hub" \ No newline at end of file diff --git a/tests/networks/pop.toml b/tests/networks/pop.toml index 9ad7c871d..643f1a0b5 100644 --- a/tests/networks/pop.toml +++ b/tests/networks/pop.toml @@ -24,4 +24,16 @@ default_command = "pop-node" [[parachains.collators]] name = "pop" -args = ["-lruntime::contracts=debug"] \ No newline at end of file +args = ["-lruntime::contracts=debug"] + +[[hrmp_channels]] +sender = 1000 +recipient = 4385 +max_capacity = 1000 +max_message_size = 5000 + +[[hrmp_channels]] +sender = 4385 +recipient = 1000 +max_capacity = 1000 +max_message_size = 8000 \ No newline at end of file diff --git a/tests/networks/westend+asset-hub.toml b/tests/networks/westend+asset-hub.toml new file mode 100644 index 000000000..3404173f8 --- /dev/null +++ b/tests/networks/westend+asset-hub.toml @@ -0,0 +1,19 @@ +# pop up parachain -f ./tests/networks/westend+asset-hub.toml + +[relaychain] +chain = "westend-local" + +[[relaychain.nodes]] +name = "alice" +validator = true + +[[relaychain.nodes]] +name = "bob" +validator = true + +[[parachains]] +id = 1000 +chain = "asset-hub-westend-local" + +[[parachains.collators]] +name = "asset-hub" \ No newline at end of file diff --git a/tests/networks/westend.toml b/tests/networks/westend.toml new file mode 100644 index 000000000..7bdb66bdd --- /dev/null +++ b/tests/networks/westend.toml @@ -0,0 +1,12 @@ +# pop up parachain -f ./tests/networks/westend.toml + +[relaychain] +chain = "westend-local" + +[[relaychain.nodes]] +name = "alice" +validator = true + +[[relaychain.nodes]] +name = "bob" +validator = true