diff --git a/README.md b/README.md index c7cd541..ec1d8f0 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,20 @@ zombie-bite bite -r kusama --rc-upgrade ./kusama_runtime.wasm --and-spawn --appl zombie-bite spawn -d /tmp/base_path --apply-upgrade ``` +#### Forking a relay that is not a public network + +`-r` also takes `custom%%%`, for a relay zombie-bite has no built-in knowledge of: + +```sh +zombie-bite bite -d /tmp/base_path \ + -r custom%previewnet%wss://previewnet.example.com%/path/to/previewnet.json \ + -p custom%2000%wss://para.example.com%/path/to/para.json +``` + +The name is what the artifacts are named after, the endpoint is what the bite reads state and metadata from, and the chain-spec is what the node is started with. There is no built-in host config for such a relay, so the endpoint has to be reachable — `Configuration::ActiveConfig` is read from it. + +A relay name that is not one of `polkadot`, `kusama`, `paseo` or `westend` is treated as a custom relay rather than silently falling back to polkadot. + #### Cores and messaging state - `--para-cores =` overrides how many cores a parachain gets (defaults mirror the live networks, e.g. asset-hub takes 3 for elastic scaling). The relay's validator count follows the total. diff --git a/src/cli.rs b/src/cli.rs index 4920822..9334c1b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -12,6 +12,8 @@ use crate::config::{ BiteOptions, CoresOverride, Parachain, Relaychain, Upgrades, ZombieBiteConfig, }; +const KNOWN_RELAYS: [&str; 4] = ["polkadot", "kusama", "paseo", "westend"]; + #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] pub struct Args { @@ -29,7 +31,10 @@ pub enum Commands { /// The network will be using for bite /// If not specified, will use the value from config. /// If not in config, defaults to polkadot. - #[arg(short = 'r', long = "rc", value_parser = clap::builder::PossibleValuesParser::new(["polkadot", "kusama", "paseo", "westend"]))] + /// The network to bite: polkadot, kusama, paseo or westend. + /// For a relay that is not a public network use: + /// custom%%% + #[arg(short = 'r', long = "rc", verbatim_doc_comment)] relay: Option, /// If provided we will override the runtime as part of the process of 'bite' /// The resulting network will be running with this runtime. @@ -210,8 +215,16 @@ pub fn resolve_bite_config( "polkadot".to_string() }; - let relaychain = if relay_runtime.is_some() || rc_sync_url.is_some() || relay_bite_at.is_some() - { + let relaychain = if relay_network.starts_with("custom%") { + resolve_custom_relaychain(&relay_network, relay_runtime.clone(), relay_bite_at)? + } else if !KNOWN_RELAYS.contains(&relay_network.as_str()) { + // Anything else is a typo, not a chain to bite: a custom relay has to + // come with its endpoint and chain-spec. + bail!( + "unknown relay '{relay_network}'; use one of {} or custom%%%", + KNOWN_RELAYS.join(", ") + ); + } else if relay_runtime.is_some() || rc_sync_url.is_some() || relay_bite_at.is_some() { // CLI args provided, use them Relaychain::new_with_values(&relay_network, relay_runtime, rc_sync_url, relay_bite_at) } else if let Some(ref config) = config_file { @@ -426,6 +439,29 @@ pub fn resolve_spawn_config( }) } +/// custom%%% +fn resolve_custom_relaychain( + s: &str, + maybe_override: Option, + maybe_bite_at: Option, +) -> Result { + let parts: Vec<&str> = s.splitn(4, '%').collect(); + if parts.len() != 4 { + bail!("custom relay must be custom%%%, got '{s}'"); + } + let (name, rpc, chain_spec) = (parts[1], parts[2], parts[3]); + if name.is_empty() || rpc.is_empty() || chain_spec.is_empty() { + bail!("custom relay needs a name, an rpc endpoint and a chain-spec path, got '{s}'"); + } + Ok(Relaychain::new_custom( + name, + chain_spec, + rpc, + maybe_override, + maybe_bite_at, + )) +} + fn resolve_custom_parachain(s: &str) -> Parachain { let parts: Vec<&str> = s.splitn(5, '%').collect(); trace!("custom parts: {parts:?}"); @@ -518,4 +554,45 @@ mod test { let s = "custom%3392%wss://kusama-yap-3392.example.com:1234%/path/to/chain-spec.json%abc"; let _para = resolve_custom_parachain(s); } + #[test] + fn custom_relay_works() { + let rc = resolve_custom_relaychain( + "custom%previewnet%wss://previewnet.example.com%/path/to/previewnet.json", + None, + Some(42), + ) + .unwrap(); + + assert_eq!(rc.as_chain_string(), "previewnet"); + assert_eq!(rc.chain_spec(), Some("/path/to/previewnet.json")); + // a custom relay is passed to the node as a spec path, not a name + assert_eq!(rc.chain_arg(), "/path/to/previewnet.json"); + assert_eq!(rc.rpc_endpoint(), "wss://previewnet.example.com"); + assert_eq!(rc.sync_endpoint(), "wss://previewnet.example.com"); + assert_eq!(rc.at_block(), Some(42)); + assert!(rc.is_custom()); + } + + #[test] + fn custom_relay_needs_every_part() { + for bad in [ + "custom%previewnet%wss://previewnet.example.com", + "custom%previewnet%%/path/to/spec.json", + "custom%%wss://x%/path/to/spec.json", + ] { + assert!( + resolve_custom_relaychain(bad, None, None).is_err(), + "should reject '{bad}'" + ); + } + } + + #[test] + fn unknown_relay_name_keeps_its_name() { + // helper subcommands only get the name back as a string, and the + // artifacts are named after it + let rc = Relaychain::new("previewnet"); + assert_eq!(rc.as_chain_string(), "previewnet"); + assert!(rc.is_custom()); + } } diff --git a/src/config.rs b/src/config.rs index fdcae28..ad9532b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -234,6 +234,16 @@ pub enum Relaychain { maybe_sync_url: MaybeSyncUrl, maybe_bite_at: MaybeByteAt, }, + /// A relay chain that is not one of the public networks: its name is used + /// for the artifact file names, and the chain-spec and endpoint have to be + /// supplied because there is nothing to look them up from. + Custom { + name: String, + chain_spec: MaybeChainSpec, + maybe_override: MaybeWasmOverridePath, + maybe_sync_url: MaybeSyncUrl, + maybe_bite_at: MaybeByteAt, + }, } impl Relaychain { @@ -254,11 +264,36 @@ impl Relaychain { maybe_sync_url: None, maybe_bite_at: None, }, - _ => Self::Polkadot { + "polkadot" => Self::Polkadot { maybe_override: None, maybe_sync_url: None, maybe_bite_at: None, }, + // Keeps a custom relay's artifact names working in the helper + // subcommands, which only get the name back as a string. + other => Self::Custom { + name: other.to_string(), + chain_spec: None, + maybe_override: None, + maybe_sync_url: None, + maybe_bite_at: None, + }, + } + } + + pub fn new_custom( + name: impl Into, + chain_spec: impl Into, + rpc: impl Into, + maybe_override: MaybeWasmOverridePath, + maybe_bite_at: MaybeByteAt, + ) -> Self { + Self::Custom { + name: name.into(), + chain_spec: Some(chain_spec.into()), + maybe_override, + maybe_sync_url: Some(rpc.into()), + maybe_bite_at, } } @@ -284,7 +319,14 @@ impl Relaychain { maybe_sync_url, maybe_bite_at, }, - _ => Self::Polkadot { + "polkadot" => Self::Polkadot { + maybe_override, + maybe_sync_url, + maybe_bite_at, + }, + other => Self::Custom { + name: other.to_string(), + chain_spec: None, maybe_override, maybe_sync_url, maybe_bite_at, @@ -293,12 +335,7 @@ impl Relaychain { } pub fn as_local_chain_string(&self) -> String { - String::from(match self { - Relaychain::Polkadot { .. } => "polkadot-local", - Relaychain::Kusama { .. } => "kusama-local", - Relaychain::Paseo { .. } => "paseo-local", - Relaychain::Westend { .. } => "westend-local", - }) + format!("{}-local", self.as_chain_string()) } pub fn as_chain_string(&self) -> String { @@ -307,26 +344,67 @@ impl Relaychain { Relaychain::Kusama { .. } => "kusama", Relaychain::Paseo { .. } => "paseo", Relaychain::Westend { .. } => "westend", + Relaychain::Custom { name, .. } => name, }) } - // TODO: make this endpoints configurables - pub fn sync_endpoint(&self) -> String { - String::from(match self { + /// Chain-spec of a custom relay; the public networks are known to the node + /// by name. + pub fn chain_spec(&self) -> Option<&str> { + match self { + Relaychain::Custom { chain_spec, .. } => chain_spec.as_deref(), + _ => None, + } + } + + /// Value for the node's `--chain`: a spec path for a custom relay, the + /// network name otherwise. + pub fn chain_arg(&self) -> String { + self.chain_spec() + .map(str::to_string) + .unwrap_or_else(|| self.as_chain_string()) + } + + pub fn is_custom(&self) -> bool { + matches!(self, Relaychain::Custom { .. }) + } + + /// Endpoint supplied with `--rc-sync-url` / the config's `sync_url`, used + /// instead of the public default. Both the parachain sync and the reads the + /// bite does against the source go through it: the reason to pass it is that + /// the public endpoint is unusable (rate limited, down, or not reachable + /// from where the bite runs). + pub fn sync_url(&self) -> Option<&str> { + match self { + Relaychain::Polkadot { maybe_sync_url, .. } + | Relaychain::Kusama { maybe_sync_url, .. } + | Relaychain::Paseo { maybe_sync_url, .. } + | Relaychain::Westend { maybe_sync_url, .. } + | Relaychain::Custom { maybe_sync_url, .. } => maybe_sync_url.as_deref(), + } + } + + fn default_endpoint(&self) -> &'static str { + match self { Relaychain::Polkadot { .. } => "wss://rpc.polkadot.io", Relaychain::Kusama { .. } => "wss://kusama-rpc.polkadot.io", Relaychain::Paseo { .. } => "wss://paseo-rpc.dwellir.com", Relaychain::Westend { .. } => "wss://westend-rpc.n.dwellir.com", - }) + // A custom relay has no public endpoint to fall back to. + Relaychain::Custom { .. } => "", + } + } + + pub fn sync_endpoint(&self) -> String { + self.sync_url() + .unwrap_or_else(|| self.default_endpoint()) + .to_string() } pub fn rpc_endpoint(&self) -> String { - String::from(match self { - Relaychain::Polkadot { .. } => "wss://rpc.polkadot.io", - Relaychain::Kusama { .. } => "wss://kusama-rpc.polkadot.io", - Relaychain::Paseo { .. } => "wss://paseo-rpc.dwellir.com", - Relaychain::Westend { .. } => "wss://westend-rpc.n.dwellir.com", - }) + self.sync_url() + .unwrap_or_else(|| self.default_endpoint()) + .to_string() } pub fn context(&self) -> Context { @@ -338,7 +416,8 @@ impl Relaychain { Relaychain::Kusama { maybe_override, .. } | Relaychain::Polkadot { maybe_override, .. } | Relaychain::Westend { maybe_override, .. } - | Relaychain::Paseo { maybe_override, .. } => maybe_override.as_deref(), + | Relaychain::Paseo { maybe_override, .. } + | Relaychain::Custom { maybe_override, .. } => maybe_override.as_deref(), } } @@ -347,6 +426,9 @@ impl Relaychain { Relaychain::Paseo { .. } => 600, Relaychain::Kusama { .. } => 600, Relaychain::Westend { .. } => 600, + // TODO: read it from the chain instead of assuming a testnet-sized + // epoch for a custom relay. + Relaychain::Custom { .. } => 600, _ => 2400, } } @@ -356,7 +438,8 @@ impl Relaychain { Relaychain::Kusama { maybe_bite_at, .. } | Relaychain::Polkadot { maybe_bite_at, .. } | Relaychain::Westend { maybe_bite_at, .. } - | Relaychain::Paseo { maybe_bite_at, .. } => *maybe_bite_at, + | Relaychain::Paseo { maybe_bite_at, .. } + | Relaychain::Custom { maybe_bite_at, .. } => *maybe_bite_at, } } } @@ -599,7 +682,7 @@ pub fn generate_network_config( Relaychain::Polkadot { .. } | Relaychain::Kusama { .. } | Relaychain::Westend { .. } => { CMD_TPL } - Relaychain::Paseo { .. } => DEFAULT_CHAIN_SPEC_TPL_COMMAND, + Relaychain::Paseo { .. } | Relaychain::Custom { .. } => DEFAULT_CHAIN_SPEC_TPL_COMMAND, }; // Calculate required validators based on parachain count @@ -1126,9 +1209,12 @@ mod test { let paseo = Relaychain::new("paseo"); assert_eq!(paseo.as_chain_string(), "paseo"); - // Unknown defaults to polkadot + // An unknown name is a custom relay keeping its name, not a silent + // fallback to polkadot: the helper subcommands name artifacts after it, + // and a typo now fails instead of biting the wrong chain. let unknown = Relaychain::new("unknown"); - assert_eq!(unknown.as_chain_string(), "polkadot"); + assert_eq!(unknown.as_chain_string(), "unknown"); + assert!(unknown.is_custom()); } #[test] @@ -1554,4 +1640,19 @@ chain_spec = "/path/to/yap-3392-raw-chain-spec.json" let para_config = parachains.first().unwrap(); assert_eq!(para_config.id(), 3392); } + #[test] + fn sync_url_overrides_the_public_endpoint() { + let default = Relaychain::new("kusama"); + assert_eq!(default.sync_endpoint(), "wss://kusama-rpc.polkadot.io"); + assert_eq!(default.rpc_endpoint(), "wss://kusama-rpc.polkadot.io"); + + let custom = Relaychain::new_with_values( + "kusama", + None, + Some("wss://my-own-kusama.example.com".to_string()), + None, + ); + assert_eq!(custom.sync_endpoint(), "wss://my-own-kusama.example.com"); + assert_eq!(custom.rpc_endpoint(), "wss://my-own-kusama.example.com"); + } } diff --git a/src/doppelganger.rs b/src/doppelganger.rs index e6c6386..00e61ac 100644 --- a/src/doppelganger.rs +++ b/src/doppelganger.rs @@ -278,7 +278,7 @@ pub async fn doppelganger_inner( let (sync_node, sync_db_path, sync_chain) = sync_relay_only( ns.clone(), "doppelganger", - relay_chain.as_chain_string(), + &relay_chain, para_heads_env, rc_default_overrides_path, &rc_info_path, @@ -299,18 +299,20 @@ pub async fn doppelganger_inner( ns.clone(), &r_chain_spec_path, &context_relay.doppelganger_cmd(), - &sync_chain, + &relay_chain.chain_arg(), ) .await .unwrap(); // remove `parachains` db + // The node keeps its db under the chain-spec's own id, which is not always + // the name we use for the artifacts. let sync_chain_in_path = if sync_chain == "kusama" { - "ksmcc3" + "ksmcc3".to_string() } else if sync_chain == "westend" { - "westend2" + "westend2".to_string() } else { - sync_chain.as_str() + spec_chain_id(&r_chain_spec_path).await? }; let parachains_path = if database == "rocksdb" { @@ -329,8 +331,12 @@ pub async fn doppelganger_inner( generate_snap(&sync_db_path, &r_snap_path).await.unwrap(); let relay_artifacts = ChainArtifact { - // cmd: context_relay.doppelganger_cmd(), - cmd: context_relay.cmd(), + // The relay validators must run the doppelganger binary: it honours + // ZOMBIE_DISPUTE_CANDIDATE_LIFETIME_AFTER_FINALIZATION, without which + // the stock dispute coordinator scans ancestor headers a warp-synced + // bite does not have, never initializes, and caps finality at the bite + // block forever while blocks keep being produced. + cmd: context_relay.doppelganger_cmd(), chain: sync_chain, spec_path: r_chain_spec_path, snap_path: r_snap_path, @@ -456,6 +462,19 @@ pub async fn doppelganger_inner( Ok(()) } +/// `id` of a chain-spec, which is the directory the node stores its db under. +async fn spec_chain_id(spec_path: &str) -> Result { + let content = fs::read_to_string(spec_path) + .await + .map_err(|e| anyhow!("can't read chain-spec {spec_path}: {e}"))?; + let spec: serde_json::Value = serde_json::from_str(&content) + .map_err(|e| anyhow!("chain-spec {spec_path} is not valid json: {e}"))?; + spec["id"] + .as_str() + .map(str::to_string) + .ok_or_else(|| anyhow!("chain-spec {spec_path} has no 'id'")) +} + async fn copy_upgrade_blob(from: &str, base_dir: &str, blob_name: &str) -> (String, String) { let wasm = fs::read(from) .await diff --git a/src/main.rs b/src/main.rs index caf26a6..dbce4d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -196,6 +196,15 @@ async fn main() -> Result<(), anyhow::Error> { bail!("--apply-upgrade needs an upgrade to carry (--rc-upgrade / --para-upgrade)"); } + if resolved_config.relaychain.is_custom() { + if resolved_config.relaychain.chain_spec().is_none() { + bail!("a custom relay needs a chain-spec: use -r custom%%%"); + } + if resolved_config.relaychain.sync_url().is_none() { + bail!("a custom relay needs an rpc endpoint: use -r custom%%%"); + } + } + debug!("{:?}", resolved_config.relaychain); doppelganger_inner( resolved_config.base_path.clone(), diff --git a/src/metadata.rs b/src/metadata.rs index c6a789b..c055e50 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -134,6 +134,24 @@ impl ChainMetadata { Ok(raw.map(|v| v.trim_start_matches("0x").to_string())) } + /// Encode a value against the item's on-chain type, so its shape is the + /// runtime's rather than a hand-rolled guess. + pub fn encode_value( + &self, + pallet: &str, + item: &str, + value: &scale_value::Value, + ) -> Result { + let ty = self + .value_ty(pallet, item) + .ok_or_else(|| anyhow!("{pallet}::{item} not in metadata"))?; + let mut out = vec![]; + scale_value::scale::encode_as_type(value, ty, self.metadata.types(), &mut out).map_err( + |e| anyhow!("{pallet}::{item}: value does not encode against the runtime's type: {e}"), + )?; + Ok(hex::encode(out)) + } + /// Decode a live value, hand it to `patch`, and re-encode it. Only the /// fields `patch` touches change - everything else the live runtime /// configured is preserved byte for byte. diff --git a/src/overrides.rs b/src/overrides.rs index ab81eb0..3111988 100644 --- a/src/overrides.rs +++ b/src/overrides.rs @@ -232,6 +232,13 @@ async fn host_config( return Ok(patched); } + if relay.is_custom() { + anyhow::bail!( + "{}: there is no built-in host config for a custom relay, so the bite needs to read Configuration::ActiveConfig from it - check the endpoint", + relay.as_chain_string() + ); + } + warn!("using the built-in host config: it is a snapshot of a past runtime, so executor params, async backing settings and max_pov_size of the live chain are lost"); let cores = array_bytes::bytes2hex("", num_cores.encode()); @@ -245,6 +252,7 @@ async fn host_config( Relaychain::Kusama { .. } => { format!("0000300000500000aaaa0a0000004000fbff0000800000000a000000100e00005802000006000000020000000000a00000c800001e000000005039278c0400000000000000000000005039278c040000000000000000000019000000009001001e000000009001000c01002000000600c4090000000000000601983a0000000000008070000001bc0200000600000058020000030000002b010000000000001e00000006000000020000001400000002000000100b060000000a0000000a000000010500000005000000{}f401000080b2e60e80c3c90100f2052a01000000000000000000000000000000", cores) } + Relaychain::Custom { .. } => unreachable!("custom relays bail above"), Relaychain::Paseo { .. } => { format!("e067350000800000aaaa020000001000fbff0000100000000a0000003c0000003c00000003000000020000000000a00000c800001e0000000000000000000000000000000000000000000000000000000000000000000000e8030000009001001e000000009001000c01002000000600c4090000000000000601983a000000000000b00400000006000000640000000200000019000000000000000200000002000000020000000500000001000000100b010000000a00000004000000010300000005000000{}6400000080b2e60e80c3c9018096980000000000000000000000000000000000", cores) } @@ -290,6 +298,7 @@ fn generate_next_keys_injects( fn generate_rc_overrides( set: &mut OverrideSet<'_>, validator_keys: &[&crate::utils::ValidatorKeys], + req_cores: u32, ) { let num_validators = validator_keys.len(); @@ -334,9 +343,14 @@ fn generate_rc_overrides( .collect::>() .join(""); - // ValidatorGroups is Vec>, so each single-validator group - // carries its own compact length prefix. - let validator_groups: Vec> = (0..num_validators as u32).map(|i| vec![i]).collect(); + // ValidatorGroups is Vec>, one group per *core* (the + // scheduler and approval subsystems index groups by core), validators + // spread round-robin so no group is empty. + let num_groups = req_cores.max(1) as usize; + let mut validator_groups: Vec> = vec![vec![]; num_groups]; + for v in 0..num_validators as u32 { + validator_groups[v as usize % num_groups].push(v); + } let validator_groups = array_bytes::bytes2hex("", validator_groups.encode()); // Build para validator keys (same as authority discovery for our purposes) @@ -375,6 +389,11 @@ fn generate_rc_overrides( format!("{validator_count_hex}{grandpa_authorities}"), ); set.set("ParaScheduler", "ValidatorGroups", &validator_groups); + // Stock runtimes have no `:UsePreviousValidators:` hook, so without this the + // first session rotation re-elects the production validators - whose + // Session::NextKeys the bite has just replaced - and authoring halts an + // epoch in. Forcing::ForceNone keeps the dev set elected. + set.inject("Staking", "ForceEra", "02"); set.set( "ParasShared", "ActiveValidatorIndices", @@ -408,6 +427,7 @@ fn augment_overrides_for_paras( paras: &[&Parachain], cores_override: &CoresOverride, keep_messaging_state: bool, + meta: Option<&ChainMetadata>, ) { // Generate paras_parachains let para_ids: Vec = paras.iter().map(|para| para.id()).collect(); @@ -420,6 +440,7 @@ fn augment_overrides_for_paras( // used to assign cores let mut core_index = 0_u32; let mut para_scheduler_value_parts: Vec = vec![]; + let mut core_plan: Vec<(u32, u32)> = vec![]; for para in paras.iter() { let para_id = ParaId(para.id()); @@ -445,16 +466,92 @@ fn augment_overrides_for_paras( let para_cores = get_assigned_cores(relay, para, cores_override); for _ in 0..para_cores { para_scheduler_value_parts.push(core_assignment::generate(core_index, para.id())); + core_plan.push((core_index, para.id())); core_index += 1; } } - let count_prefix = format!("{:02x}", para_scheduler_value_parts.len() * 4); - let core_assign_value = format!("{count_prefix}{}", para_scheduler_value_parts.join("")); - // key is generated with prefix (`0x`), and the item is not in metadata on - // every runtime, so it goes in raw. - let scheduler_key = core_assignment::get_parascheduler_storage_key(); - set.set_raw(&scheduler_key[2..], core_assign_value); + // `CoreDescriptors` is the value whose hand-rolled encoding has already + // produced silently-corrupt forks, so with metadata it is built against the + // runtime's own type. The raw fallback is only for a bite with no source + // access. + match core_descriptors_value(meta, &core_plan) { + Some(Ok(value)) => { + set.set_required("ParaScheduler", "CoreDescriptors", value); + } + Some(Err(e)) => set.errors.push(e.to_string()), + None => { + let count_prefix = format!("{:02x}", para_scheduler_value_parts.len() * 4); + let core_assign_value = + format!("{count_prefix}{}", para_scheduler_value_parts.join("")); + let scheduler_key = core_assignment::get_parascheduler_storage_key(); + set.set_raw(&scheduler_key[2..], core_assign_value); + } + } +} + +/// The scheduler's `BTreeMap`: every planned core +/// runs its parachain full time. `None` without metadata. +fn core_descriptors_value( + meta: Option<&ChainMetadata>, + core_plan: &[(u32, u32)], +) -> Option> { + use zombienet_sdk::subxt::ext::scale_value::{Composite, Value as V, ValueDef}; + + let meta = meta?; + // A whole core, in the parts-per-57600 unit the scheduler uses. + const FULL_CORE: u128 = 57600; + + fn none() -> V<()> { + V::variant("None", Composite::Unnamed(vec![])) + } + fn tuple(items: Vec>) -> V<()> { + V { + value: ValueDef::Composite(Composite::Unnamed(items)), + context: (), + } + } + fn record(fields: Vec<(&str, V<()>)>) -> V<()> { + V { + value: ValueDef::Composite(Composite::Named( + fields + .into_iter() + .map(|(n, v)| (n.to_string(), v)) + .collect(), + )), + context: (), + } + } + + let entries: Vec> = core_plan + .iter() + .map(|(core, para)| { + let assignment = tuple(vec![ + V::variant("Task", Composite::Unnamed(vec![V::u128(*para as u128)])), + record(vec![ + ("ratio", V::u128(FULL_CORE)), + ("remaining", V::u128(FULL_CORE)), + ]), + ]); + let work_state = record(vec![ + ("assignments", tuple(vec![assignment])), + ("end_hint", none()), + ("pos", V::u128(0)), + ("step", V::u128(FULL_CORE)), + ]); + let descriptor = record(vec![ + ("queue", none()), + ( + "current_work", + V::variant("Some", Composite::Unnamed(vec![work_state])), + ), + ]); + tuple(vec![V::u128(*core as u128), descriptor]) + }) + .collect(); + let map = tuple(entries).map_context(|_| 0_u32); + + Some(meta.encode_value("ParaScheduler", "CoreDescriptors", &map)) } #[allow(clippy::too_many_arguments)] @@ -469,11 +566,16 @@ pub async fn generate_default_overrides_for_rc( keep_messaging_state: bool, ) -> Result { let num_validators = crate::config::num_validators_for_cores(req_cores); + if req_cores > num_validators { + anyhow::bail!( + "{req_cores} cores requested but only {num_validators} dev validators exist; every core needs a validator group, so reduce per-para cores" + ); + } let validator_keys = get_validator_keys(num_validators as usize); let mut set = OverrideSet::new(meta.map(|m| m as &dyn RuntimeCheck)); - generate_rc_overrides(&mut set, &validator_keys); + generate_rc_overrides(&mut set, &validator_keys, req_cores); // add the paras related keys to override let paras_refs: Vec<&Parachain> = paras.iter().collect(); @@ -483,6 +585,7 @@ pub async fn generate_default_overrides_for_rc( ¶s_refs, cores_override, keep_messaging_state, + meta, ); set.set( @@ -671,7 +774,7 @@ mod test { let paras = vec![]; let _path = generate_default_overrides_for_rc( "/tmp", - &crate::config::Relaychain::new("polakdot"), + &crate::config::Relaychain::new("polkadot"), ¶s, 2, None, @@ -708,8 +811,8 @@ mod test { let rc = Relaychain::new("polkadot"); let mut set = OverrideSet::new(None); - generate_rc_overrides(&mut set, &validator_keys); - augment_overrides_for_paras(&mut set, &rc, ¶s, &CoresOverride::new(), false); + generate_rc_overrides(&mut set, &validator_keys, 2); + augment_overrides_for_paras(&mut set, &rc, ¶s, &CoresOverride::new(), false, None); let overrides = set.overrides; // ValidatorSet Validators @@ -748,7 +851,7 @@ mod test { "08be5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25ffe65717dad0447d715f660a0a58411de509b42e6efb8375f562f58a554d5860e" ); - // ParaScheduler ValidatorGroups: Vec>, two groups of one + // ParaScheduler ValidatorGroups: one group per core (2), round-robin let expected_groups: Vec> = vec![vec![0], vec![1]]; assert_eq!( overrides["94eadf0156a8ad5156507773d0471e4a16973e1142f5bd30d9464076794007db"], @@ -802,8 +905,8 @@ mod test { let rc = Relaychain::new("polkadot"); let mut set = OverrideSet::new(None); - generate_rc_overrides(&mut set, &validator_keys); - augment_overrides_for_paras(&mut set, &rc, ¶s, &CoresOverride::new(), true); + generate_rc_overrides(&mut set, &validator_keys, 2); + augment_overrides_for_paras(&mut set, &rc, ¶s, &CoresOverride::new(), true, None); let keys: Vec<&String> = set .overrides @@ -831,7 +934,7 @@ mod test { let scheduler_key = core_assignment::get_parascheduler_storage_key(); let assignment = |cores: &CoresOverride| { let mut set = OverrideSet::new(None); - augment_overrides_for_paras(&mut set, &rc, &[¶], cores, false); + augment_overrides_for_paras(&mut set, &rc, &[¶], cores, false, None); set.overrides[&scheduler_key[2..]] .as_str() .unwrap() diff --git a/src/sync.rs b/src/sync.rs index efc3d91..feaeba6 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -23,7 +23,7 @@ const PASEO_ASSET_HUB_SPEC_URL: &str = pub async fn sync_relay_only( ns: DynNamespace, cmd: impl AsRef, - chain: impl AsRef, + relaychain: &Relaychain, para_heads_env: Vec<(String, String)>, overrides_path: PathBuf, info_path: impl AsRef, @@ -51,18 +51,20 @@ pub async fn sync_relay_only( env.push(("ZOMBIE_RC_OVERRIDES_PATH".to_string(), rc_overrides_path)); env.push(("RUST_LOG".into(), "doppelganger=debug".into())); env.push(("ZOMBIE_INFO_PATH".into(), info_path.as_ref().into())); - env.push(("ZOMBIE_CHAIN".into(), chain.as_ref().into())); + // A custom relay is passed as a chain-spec path; the public networks are + // known to the node by name. + let chain = relaychain.chain_arg(); + let chain = chain.as_str(); + env.push(("ZOMBIE_CHAIN".into(), chain.into())); - // get the epoch duration from the chain config - let rc = Relaychain::new(chain.as_ref()); env.push(( "ZOMBIE_RC_EPOCH_DURATION".into(), - rc.epoch_duration().to_string(), + relaychain.epoch_duration().to_string(), )); // if we are sync westend, let doppelganger know to bypass // justification checks - if chain.as_ref() == "westend" { + if chain == "westend" { env.push(("ZOMBIE_IS_WESTEND".into(), "1".into())); } @@ -72,7 +74,7 @@ pub async fn sync_relay_only( let opts = SpawnNodeOptions::new("sync-node", cmd.as_ref()) .args(vec![ "--chain", - chain.as_ref(), + chain, "--sync", "warp", "-d", @@ -98,9 +100,9 @@ pub async fn sync_relay_only( wait_ws_ready(&metrics_url).await.unwrap(); let url = reqwest::Url::try_from(metrics_url.as_str()).unwrap(); wait_sync(url).await.unwrap(); - info!("✅ Synced (chain: {})", chain.as_ref()); + info!("✅ Synced (chain: {})", chain); // we should just paused - Ok((sync_node, sync_db_path, chain.as_ref().to_string())) + Ok((sync_node, sync_db_path, relaychain.as_chain_string())) } #[allow(clippy::too_many_arguments)]