diff --git a/examples/custom-parachain.toml b/examples/custom-parachain.toml new file mode 100644 index 0000000..83ec976 --- /dev/null +++ b/examples/custom-parachain.toml @@ -0,0 +1,10 @@ +[relaychain] +network = "kusama" + +[[parachains]] +type = "custom" +para_id = 3392 +name = "yap-3392" +rpc_endpoint = "wss://kusama-yap-3392.parity-chains.parity.io" +chain_spec = "/path/to/yap-3392-raw-chain-spec.json" +# runtime_override = "/path/to/new_runtime.wasm" diff --git a/src/cli.rs b/src/cli.rs index a0acef8..0f4421a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -5,7 +5,7 @@ use std::{ str::FromStr, time::{SystemTime, UNIX_EPOCH}, }; -use tracing::warn; +use tracing::{trace, warn}; use crate::config::{Parachain, Relaychain, ZombieBiteConfig}; @@ -36,6 +36,8 @@ pub enum Commands { #[arg(long = "rc-bite-at", verbatim_doc_comment)] relay_bite_at: Option, /// Parachains to include: asset-hub, coretime, people, bridge-hub, collectives (comma-separated) + /// For custom parachains use: custom%%%%[req_cores] + /// Example: custom:3392:wss://kusama-yap-3392.example.com:/path/to/chain-spec.json #[arg(long, short = 'p', value_delimiter = ',', verbatim_doc_comment)] parachains: Option>, /// Base path to use. if not provided we will check the env 'ZOMBIE_BITE_BASE_PATH' and if not present we will use `_timestamp` @@ -215,10 +217,13 @@ pub fn resolve_bite_config( maybe_bite_at: None, maybe_rpc_endpoint: None, }), + s if s.starts_with("custom%") => { + Some(resolve_custom_parachain(s)) + } unknown => { warn!( "⚠️ Warning: Unknown parachain '{}' will be ignored. - Valid options are: asset-hub, coretime, people, bridge-hub, collectives", + Valid options are: asset-hub, coretime, people, bridge-hub, collectives, custom%%%%[req_cores]", unknown ); None @@ -291,3 +296,97 @@ pub fn resolve_spawn_config( with_monitor: resolved_with_monitor, }) } + +fn resolve_custom_parachain(s: &str) -> Parachain { + let parts: Vec<&str> = s.splitn(5, '%').collect(); + trace!("custom parts: {parts:?}"); + if parts.len() < 4 || parts.len() > 5 { + panic!( + "Custom parachain format must be custom%%%%[req_cores], got: {}", + s + ); + } + let para_id: u32 = parts[1] + .parse() + .unwrap_or_else(|_| panic!("Invalid para_id '{}' in custom parachain", parts[1])); + let rpc_endpoint = parts[2].to_string(); + let chain_spec = parts[3].to_string(); + let name = format!("custom-{}", para_id); + let req_cores = if parts.len() == 5 { + parts[4] + .parse() + .unwrap_or_else(|_| panic!("Invalid req_cores '{}' in custom parachain", parts[4])) + } else { + // default to 1 core + 1 + }; + Parachain::Custom { + id: para_id, + name, + chain_spec, + maybe_override: None, + maybe_bite_at: None, + maybe_rpc_endpoint: Some(rpc_endpoint), + cores: req_cores, + } +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn custom_para_works() { + let s = "custom%3392%wss://kusama-yap-3392.example.com%/path/to/chain-spec.json"; + let para = resolve_custom_parachain(s); + assert_eq!(para.id(), 3392, "para id should be valid"); + assert_eq!( + para.rpc_endpoint(), + Some("wss://kusama-yap-3392.example.com"), + "rpc should match" + ); + assert_eq!( + para.chain_spec(), + Some("/path/to/chain-spec.json"), + "chain-spec should match" + ); + } + + #[test] + fn custom_para_works_with_port_number() { + let s = "custom%3392%wss://kusama-yap-3392.example.com:1234%/path/to/chain-spec.json"; + let para = resolve_custom_parachain(s); + assert_eq!( + para.rpc_endpoint(), + Some("wss://kusama-yap-3392.example.com:1234"), + "rpc should match" + ); + } + + #[test] + fn custom_para_works_with_cores() { + let s = "custom%3392%wss://kusama-yap-3392.example.com:1234%/path/to/chain-spec.json%3"; + let para = resolve_custom_parachain(s); + assert_eq!(para.req_cores(), Some(3), "cores should match"); + } + + #[test] + fn custom_para_works_with_default_cores() { + let s = "custom%3392%wss://kusama-yap-3392.example.com:1234%/path/to/chain-spec.json"; + let para = resolve_custom_parachain(s); + assert_eq!(para.req_cores(), Some(1), "cores should match"); + } + + #[test] + #[should_panic(expected = "Invalid para_id 'abc3392' in custom parachain")] + fn custom_para_id_parse_err() { + let s = "custom%abc3392%wss://kusama-yap-3392.example.com:1234%/path/to/chain-spec.json%3"; + let _para = resolve_custom_parachain(s); + } + + #[test] + #[should_panic(expected = "Invalid req_cores 'abc' in custom parachain")] + fn custom_para_cores_parse_err() { + let s = "custom%3392%wss://kusama-yap-3392.example.com:1234%/path/to/chain-spec.json%abc"; + let _para = resolve_custom_parachain(s); + } +} diff --git a/src/config.rs b/src/config.rs index b90dc34..6a728cf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -354,6 +354,7 @@ pub enum Parachain { chain_spec: String, id: u32, cores: u32, + name: String, }, } @@ -385,6 +386,7 @@ impl Parachain { maybe_override: None, maybe_bite_at: None, maybe_rpc_endpoint: None, + name: "custom".to_string(), // placeholder chain_spec: chain.to_string(), id: 2000, // placeholder cores: 1, // placeholder @@ -462,6 +464,13 @@ impl Parachain { } } + pub fn req_cores(&self) -> Option { + match self { + Parachain::Custom { cores, .. } => Some(*cores), + _ => None, + } + } + pub fn rpc_endpoint(&self) -> Option<&str> { match self { Parachain::AssetHub { @@ -484,6 +493,17 @@ impl Parachain { } => maybe_rpc_endpoint.as_deref(), } } + + pub fn chain_spec_path(&self) -> Option<&str> { + match self { + Parachain::Custom { chain_spec, .. } => Some(chain_spec.as_str()), + _ => None, + } + } + + pub fn is_custom(&self) -> bool { + matches!(self, Parachain::Custom { .. }) + } } // Chain generator command template @@ -560,40 +580,58 @@ pub fn generate_network_config( let network_builder = paras.iter().fold(network_builder, |builder, para| { println!("para: {:?}", para); - let (chain_part, id) = match para { - Parachain::AssetHub { .. } => ("asset-hub", para.id()), - Parachain::Coretime{ .. } => ("coretime", para.id()), - Parachain::People { .. } => ("people", para.id()), - Parachain::BridgeHub { .. } => ("bridge-hub", para.id()), - Parachain::Collectives { .. } => ("collectives", para.id()), - Parachain::Custom { chain_spec,.. } => (chain_spec.as_str(), para.id()), - }; + // let (chain_part, id) = match para { + // Parachain::AssetHub { .. } => ("asset-hub", para.id()), + // Parachain::Coretime{ .. } => ("coretime", para.id()), + // Parachain::People { .. } => ("people", para.id()), + // Parachain::BridgeHub { .. } => ("bridge-hub", para.id()), + // Parachain::Collectives { .. } => ("collectives", para.id()), + // Parachain::Custom { chain_spec,.. } => (chain_spec.as_str(), para.id()), + // }; + + // let chain = if let Parachain::Custom { .. } = para { + // chain_part.to_string() + // } else { + // format!("{}-{}",chain_part, relay_chain) + // }; + + let collator_name = format!("Collator-{}", para.id()); - let chain = if let Parachain::Custom { .. } = para { - chain_part.to_string() - } else { - format!("{}-{}",chain_part, relay_chain) - }; - - let collator_name = format!("Collator-{}", id); builder.with_parachain(|p| { - p.with_id(id) - .with_default_command(para_context.cmd().as_str()) - .with_chain(chain.as_str()) - .with_chain_spec_command(chain_spec_cmd) - .with_collator(|c| { - let col_builder = c.with_name(&collator_name) - .with_args(vec![ - ("-l", "aura=debug,runtime=trace,cumulus-consensus=trace,consensus::common=trace,parachain::collation-generation=trace,parachain::collator-protocol=trace,parachain=debug,basic-authorship=trace").into(), - "--force-authoring".into() - ]); - if let Ok(port) = env::var("ZOMBIE_BITE_AH_PORT") { - let rpc_port = port.parse().expect("env var ZOMBIE_BITE_AH_PORT must be a valid u16"); - col_builder.with_rpc_port(rpc_port) - } else { - col_builder - } - }) + let p = p + .with_id(para.id()) + .with_default_command(para_context.cmd().as_str()); + + // Custom paras use chain_spec_path directly; system paras use chain name + spec command + let p = if let Some(spec_path) = para.chain_spec_path() { + p.with_chain_spec_path(spec_path) + } else { + let chain_part = match para { + Parachain::AssetHub { .. } => "asset-hub", + Parachain::Coretime { .. } => "coretime", + Parachain::People { .. } => "people", + Parachain::BridgeHub { .. } => "bridge-hub", + Parachain::Collectives { .. } => "collectives", + Parachain::Custom { .. } => unreachable!(), + }; + let chain = format!("{}-{}", chain_part, relay_chain); + p.with_chain(chain.as_str()) + .with_chain_spec_command(chain_spec_cmd) + }; + + p.with_collator(|c| { + let col_builder = c.with_name(&collator_name) + .with_args(vec![ + ("-l", "aura=debug,runtime=trace,cumulus-consensus=trace,consensus::common=trace,parachain::collation-generation=trace,parachain::collator-protocol=trace,parachain=debug,basic-authorship=trace").into(), + "--force-authoring".into() + ]); + if let Ok(port) = env::var("ZOMBIE_BITE_AH_PORT") { + let rpc_port = port.parse().expect("env var ZOMBIE_BITE_AH_PORT must be a valid u16"); + col_builder.with_rpc_port(rpc_port) + } else { + col_builder + } + }) }) }); @@ -628,13 +666,13 @@ pub struct RelaychainConfig { #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct ParachainConfig { #[serde(rename = "type")] - pub parachain_type: String, // asset-hub, coretime, people, bridge-hub + pub parachain_type: String, // asset-hub, coretime, people, bridge-hub, collectives, custom pub runtime_override: Option, pub enabled: Option, // default true pub bite_at: Option, pub rpc_endpoint: Option, // Parachain id. Used for custom - pub id: Option, + pub id: Option, // Parachain chain-spec (or full chain name). Used for custom. pub chain_spec: Option, /// Number of cores to assign, NOTE: only used in `custom` paras @@ -672,19 +710,17 @@ impl ParachainConfig { }), "custom" => { // validate chain / id - let (Some(id), Some(chain_spec)) = (self.id.clone(), self.chain_spec.clone()) + let (Some(id), Some(chain_spec), Some(rpc_endpoint)) = + (self.id, self.chain_spec.clone(), self.rpc_endpoint.clone()) else { - panic!("Invalid custom parachain config, id and chain_spec are required"); + panic!("Invalid custom parachain config, 'id', 'chain_spec' and 'rpc_endpoint' are required"); }; - let id: u32 = id - .parse() - .unwrap_or_else(|_| panic!("Invalid custom para id: {}", id)); - Some(Parachain::Custom { maybe_override: self.runtime_override.clone(), maybe_bite_at: self.bite_at, - maybe_rpc_endpoint: self.rpc_endpoint.clone(), + maybe_rpc_endpoint: Some(rpc_endpoint), + name: format!("custom-{}", id), chain_spec, id, cores: self.cores.unwrap_or(1), @@ -1252,4 +1288,180 @@ network = "polkadot" let parachains = config.get_parachains(); assert_eq!(parachains.len(), 0); // No parachains specified } + + #[test] + fn custom_parachain_id() { + let para = Parachain::Custom { + id: 3392, + name: "yap-3392".to_string(), + chain_spec: "/path/to/spec.json".to_string(), + maybe_override: None, + maybe_bite_at: None, + maybe_rpc_endpoint: Some("wss://example.com".to_string()), + cores: 0, + }; + assert_eq!(para.id(), 3392); + } + + #[test] + fn custom_parachain_chain_strings() { + let para = Parachain::Custom { + id: 3392, + name: "yap-3392".to_string(), + chain_spec: "/path/to/spec.json".to_string(), + maybe_override: None, + maybe_bite_at: None, + maybe_rpc_endpoint: Some("wss://example.com".to_string()), + cores: 1, + }; + assert_eq!(para.as_chain_string("kusama"), "3392-kusama"); + assert_eq!(para.as_local_chain_string("kusama"), "3392-kusama-local"); + } + + #[test] + fn custom_parachain_chain_spec_path() { + let para = Parachain::Custom { + id: 3392, + name: "yap-3392".to_string(), + chain_spec: "/path/to/spec.json".to_string(), + maybe_override: None, + maybe_bite_at: None, + maybe_rpc_endpoint: Some("wss://example.com".to_string()), + cores: 1, + }; + assert_eq!(para.chain_spec_path(), Some("/path/to/spec.json")); + assert!(para.is_custom()); + + // Non-custom paras return None for chain_spec_path + let ah = Parachain::new("asset-hub"); + assert_eq!(ah.chain_spec_path(), None); + assert!(!ah.is_custom()); + } + + #[test] + fn custom_parachain_config_from_toml() { + let toml_content = r#" +[relaychain] +network = "kusama" + +[[parachains]] +type = "custom" +id = 3392 +name = "yap-3392" +rpc_endpoint = "wss://kusama-yap-3392.parity-chains.parity.io" +chain_spec = "/path/to/yap-3392-raw-chain-spec.json" + "#; + + let config: ZombieBiteConfig = toml::from_str(toml_content).unwrap(); + let parachains = config.get_parachains(); + assert_eq!(parachains.len(), 1); + + let para = ¶chains[0]; + assert_eq!(para.id(), 3392); + assert!(para.is_custom()); + assert_eq!( + para.chain_spec_path(), + Some("/path/to/yap-3392-raw-chain-spec.json") + ); + assert_eq!( + para.rpc_endpoint(), + Some("wss://kusama-yap-3392.parity-chains.parity.io") + ); + assert_eq!(para.as_chain_string("kusama"), "3392-kusama"); + } + + #[test] + fn custom_parachain_config_default_name() { + let config = ParachainConfig { + parachain_type: "custom".to_string(), + runtime_override: None, + enabled: Some(true), + bite_at: None, + rpc_endpoint: Some("wss://example.com".to_string()), + id: Some(3392), + chain_spec: Some("/path/to/spec.json".to_string()), + cores: None, + }; + + let para = config.to_parachain().unwrap(); + assert_eq!(para.as_chain_string("kusama"), "3392-kusama"); + } + + #[test] + #[should_panic( + expected = "Invalid custom parachain config, 'id', 'chain_spec' and 'rpc_endpoint' are required" + )] + fn custom_parachain_config_missing_para_id() { + let config = ParachainConfig { + parachain_type: "custom".to_string(), + runtime_override: None, + enabled: Some(true), + bite_at: None, + rpc_endpoint: Some("wss://example.com".to_string()), + id: None, + chain_spec: Some("/path/to/spec.json".to_string()), + cores: None, + }; + config.to_parachain(); + } + + #[test] + #[should_panic( + expected = "Invalid custom parachain config, 'id', 'chain_spec' and 'rpc_endpoint' are required" + )] + fn custom_parachain_config_missing_chain_spec() { + let config = ParachainConfig { + parachain_type: "custom".to_string(), + runtime_override: None, + enabled: Some(true), + bite_at: None, + rpc_endpoint: Some("wss://example.com".to_string()), + id: Some(3392), + chain_spec: None, + cores: None, + }; + config.to_parachain(); + } + + #[test] + #[should_panic( + expected = "Invalid custom parachain config, 'id', 'chain_spec' and 'rpc_endpoint' are required" + )] + fn custom_parachain_config_missing_rpc_endpoint() { + let config = ParachainConfig { + parachain_type: "custom".to_string(), + runtime_override: None, + enabled: Some(true), + bite_at: None, + rpc_endpoint: None, + id: Some(3392), + chain_spec: Some("/path/to/spec.json".to_string()), + cores: None, + }; + config.to_parachain(); + } + + #[test] + fn generate_config_with_custom_parachain() { + let relaychain = Relaychain::new("kusama"); + // Create a temp chain spec file for the test + let spec_path = "/tmp/test-custom-para-spec.json"; + std::fs::write(spec_path, r#"{"bootNodes": []}"#).unwrap(); + + let parachains = vec![Parachain::Custom { + id: 3392, + name: "yap-3392".to_string(), + chain_spec: spec_path.to_string(), + maybe_override: None, + maybe_bite_at: None, + maybe_rpc_endpoint: Some("wss://example.com".to_string()), + cores: 1, + }]; + + let config = generate_network_config(&relaychain, parachains).unwrap(); + let parachains = config.parachains(); + assert_eq!(parachains.len(), 1); + let para_config = parachains.first().unwrap(); + assert_eq!(para_config.id(), 3392); + } } diff --git a/src/doppelganger.rs b/src/doppelganger.rs index 4f1021a..77a3130 100644 --- a/src/doppelganger.rs +++ b/src/doppelganger.rs @@ -149,16 +149,32 @@ pub async fn doppelganger_inner( // }; let sync_chain_name = para.as_chain_string(&relay_chain.as_chain_string()); - let chain_spec_path = format!("{}/{}-spec.json", base_dir_str, sync_chain_name); - generate_chain_spec( - ns.clone(), - &chain_spec_path, - &context_para.doppelganger_cmd(), - &sync_chain, - ) - .await - .unwrap(); + + if para.is_custom() { + // For custom paras, copy the user's chain spec and clear bootNodes + // instead of running `doppelganger-parachain build-spec` which may not + // understand arbitrary runtimes. + let spec_content = tokio::fs::read_to_string(&sync_chain) + .await + .unwrap_or_else(|_| panic!("Failed to read custom chain spec: {}", sync_chain)); + let mut spec_json: serde_json::Value = serde_json::from_str(&spec_content) + .unwrap_or_else(|_| { + panic!("Failed to parse custom chain spec JSON: {}", sync_chain) + }); + spec_json["bootNodes"] = serde_json::Value::Array(vec![]); + let contents = serde_json::to_string_pretty(&spec_json).unwrap(); + tokio::fs::write(&chain_spec_path, contents).await.unwrap(); + } else { + generate_chain_spec( + ns.clone(), + &chain_spec_path, + &context_para.doppelganger_cmd(), + &sync_chain, + ) + .await + .unwrap(); + } // generate the data.tgz to use as snapshot let snap_path = format!("{}/{}-snap.tgz", base_dir_str, sync_chain_name); diff --git a/src/sync.rs b/src/sync.rs index 52bbf43..efc3d91 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -150,8 +150,13 @@ pub async fn sync_para( trace!("env: {env:?}"); + // Custom parachains: use the provided chain spec file path directly + let chain_arg_for_custom; let dest_for_paseo = format!("{}/asset-hub-paseo.json", ns.base_dir().to_string_lossy(),); - let chain_arg = if chain == "asset-hub-paseo" { + let chain_arg = if let Some(spec_path) = parachain.chain_spec_path() { + chain_arg_for_custom = spec_path.to_string(); + chain_arg_for_custom.as_str() + } else if chain == "asset-hub-paseo" { // get chain spec from https://paseo-r2.zondax.ch/chain-specs/paseo-asset-hub.json let response = reqwest::get(PASEO_ASSET_HUB_SPEC_URL) .await