diff --git a/README.md b/README.md index 23617e5..c7cd541 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,17 @@ zombie-bite bite -r kusama --rc-upgrade ./kusama_runtime.wasm --and-spawn --appl zombie-bite spawn -d /tmp/base_path --apply-upgrade ``` +#### 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. +- `--keep-messaging-state` keeps the inherited HRMP/DMP state instead of clearing it. Only correct when the relay's parachains are exactly the ones being bitten, so both snapshots agree on channel heads; on a shared relay the mismatch makes cumulus panic with `HRMP head mismatch`. + +#### Overrides are checked against the runtime + +Storage keys are derived from pallet and item names, and every value is decoded against its real on-chain type and required to re-encode byte-identically, so a renamed item or changed type fails the bite instead of silently landing as something else. Items the runtime does not have are skipped — except ones you asked for explicitly (a carried upgrade, a wasm override, `ZOMBIE_SUDO`), which are errors. `HostConfiguration` is patched from the live value (only `num_cores` changes) rather than replaced, so executor params, async backing and `max_pov_size` of the bitten chain are preserved. + +Metadata and the live values are read at the block being bitten (`--rc-bite-at` / a para's `bite_at`), so they match the state being imported. Parachains use a default public endpoint when no `rpc_endpoint` is configured; if it can't be reached, the bite still runs with a warning and those overrides go unverified. Custom parachains are only verified when their config supplies an `rpc_endpoint`. + #### Spawn Spawn a new instance of the _bited_ network with the following cmd: diff --git a/src/cli.rs b/src/cli.rs index 24816a5..4920822 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,3 +1,4 @@ +use anyhow::{anyhow, bail}; use clap::{Parser, Subcommand}; use std::{ env, @@ -7,7 +8,9 @@ use std::{ }; use tracing::{trace, warn}; -use crate::config::{Parachain, Relaychain, Upgrades, ZombieBiteConfig}; +use crate::config::{ + BiteOptions, CoresOverride, Parachain, Relaychain, Upgrades, ZombieBiteConfig, +}; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] @@ -47,6 +50,15 @@ pub enum Commands { /// for every carried upgrade and wait until it enacts. #[arg(long, default_value_t = false, verbatim_doc_comment)] apply_upgrade: bool, + /// Keep the inherited HRMP/DMP state instead of clearing it. Only correct + /// when the relay's parachains are exactly the ones being bitten, so the + /// two snapshots agree on channel heads. + #[arg(long, default_value_t = false, verbatim_doc_comment)] + keep_messaging_state: bool, + /// Override the cores assigned to a parachain, format: = + /// Can be set multiple times, once per para. + #[arg(long = "para-cores", verbatim_doc_comment)] + para_cores: Vec, /// If provided we will _bite_ the live network at the supplied block hieght #[arg(long = "rc-bite-at", verbatim_doc_comment)] relay_bite_at: Option, @@ -154,8 +166,8 @@ pub struct ResolvedBiteConfig { pub parachains: Vec, pub base_path: PathBuf, pub and_spawn: bool, - pub upgrades: Upgrades, pub apply_upgrade: bool, + pub opts: BiteOptions, } #[derive(Debug)] @@ -178,6 +190,8 @@ pub fn resolve_bite_config( relay_upgrade: Option, para_upgrade: Vec, apply_upgrade: bool, + keep_messaging_state: bool, + para_cores: Vec, ) -> Result { // Load config file if provided let config_file = if let Some(path) = config_path { @@ -281,20 +295,19 @@ pub fn resolve_bite_config( }; // Resolve upgrades (CLI overrides config file) + let mut para_upgrades = std::collections::HashMap::new(); + for entry in ¶_upgrade { + let (id, path) = entry.split_once('=').ok_or_else(|| { + anyhow!("--para-upgrade must be =, got '{entry}'") + })?; + let id: u32 = id + .parse() + .map_err(|_| anyhow!("invalid para_id '{id}' in --para-upgrade"))?; + para_upgrades.insert(id, path.to_string()); + } let mut upgrades = Upgrades { relay: relay_upgrade, - paras: para_upgrade - .iter() - .map(|entry| { - let (id, path) = entry.split_once('=').unwrap_or_else(|| { - panic!("--para-upgrade format must be =, got: {entry}") - }); - let id: u32 = id - .parse() - .unwrap_or_else(|_| panic!("Invalid para_id '{id}' in --para-upgrade")); - (id, path.to_string()) - }) - .collect(), + paras: para_upgrades, }; if let Some(ref config) = config_file { if upgrades.relay.is_none() { @@ -315,13 +328,57 @@ pub fn resolve_bite_config( false }; + // Per-para cores: CLI entries win over the config file's `cores`. + let mut cores: CoresOverride = CoresOverride::new(); + if let Some(ref config) = config_file { + for para_cfg in config.parachains.as_deref().unwrap_or_default() { + if let (Some(c), Some(para)) = (para_cfg.cores, para_cfg.to_parachain()) { + cores.insert(para.id(), c); + } + } + } + for entry in ¶_cores { + let (id, c) = entry + .split_once('=') + .ok_or_else(|| anyhow!("--para-cores must be =, got '{entry}'"))?; + let id: u32 = id + .parse() + .map_err(|_| anyhow!("invalid para_id '{id}' in --para-cores"))?; + let c: u32 = c + .parse() + .map_err(|_| anyhow!("invalid cores '{c}' in --para-cores"))?; + if c == 0 { + bail!("--para-cores {id}=0: a parachain with no cores can't have blocks backed"); + } + cores.insert(id, c); + } + // A core count for a para that is not part of the bite is a typo, not a + // silently ignorable no-op. + for id in cores.keys() { + if !resolved_parachains.iter().any(|para| para.id() == *id) { + bail!("--para-cores/config sets cores for para {id}, which is not part of this bite"); + } + } + + let resolved_keep_messaging = if keep_messaging_state { + true + } else if let Some(ref config) = config_file { + config.keep_messaging_state.unwrap_or(false) + } else { + false + }; + Ok(ResolvedBiteConfig { relaychain, parachains: resolved_parachains, base_path: resolved_base_path, and_spawn: resolved_and_spawn, - upgrades, apply_upgrade: resolved_apply_upgrade, + opts: BiteOptions { + upgrades, + cores, + keep_messaging_state: resolved_keep_messaging, + }, }) } diff --git a/src/config.rs b/src/config.rs index f9f84e3..fdcae28 100644 --- a/src/config.rs +++ b/src/config.rs @@ -184,7 +184,22 @@ impl Upgrades { } } -pub fn get_assigned_cores(relay: &Relaychain, para: &Parachain) -> u32 { +/// Per-parachain core counts from configuration, keyed by para id. Overrides +/// the built-in defaults below, which mirror the live networks. +pub type CoresOverride = std::collections::HashMap; + +/// Everything a bite needs beyond the chains themselves. +#[derive(Debug, Default, Clone)] +pub struct BiteOptions { + pub upgrades: Upgrades, + pub cores: CoresOverride, + pub keep_messaging_state: bool, +} + +pub fn get_assigned_cores(relay: &Relaychain, para: &Parachain, override_: &CoresOverride) -> u32 { + if let Some(cores) = override_.get(¶.id()) { + return *cores; + } match para { Parachain::AssetHub { .. } => 3, Parachain::People { .. } => match relay { @@ -520,6 +535,28 @@ impl Parachain { } } + /// Endpoint used to read the parachain's metadata when none is configured, + /// so overrides are checked against the runtime by default. A wrong or + /// unreachable guess only costs the verification (with a warning), never the + /// bite itself. + // TODO: same as the relay endpoints, these should be configurable. + pub fn default_rpc_endpoint(&self, relay: &Relaychain) -> Option { + let prefix = match self { + Parachain::AssetHub { .. } => "asset-hub", + Parachain::Coretime { .. } => "coretime", + Parachain::People { .. } => "people", + Parachain::BridgeHub { .. } => "bridge-hub", + Parachain::Collectives { .. } => "collectives", + // A custom para is only reachable through the endpoint its config + // supplies. + Parachain::Custom { .. } => return None, + }; + Some(format!( + "wss://{prefix}-{}-rpc.n.dwellir.com", + relay.as_chain_string() + )) + } + pub fn chain_spec_path(&self) -> Option<&str> { match self { Parachain::Custom { chain_spec, .. } => Some(chain_spec.as_str()), @@ -680,6 +717,11 @@ pub struct ZombieBiteConfig { pub and_spawn: Option, pub with_monitor: Option, pub apply_upgrade: Option, + /// Keep inherited HRMP/DMP state instead of clearing it. Correct when the + /// relay and parachain snapshots agree on channel heads (a relay whose only + /// parachains are the ones being bitten); wrong for a shared relay, where + /// the mismatch makes cumulus panic with `HRMP head mismatch`. + pub keep_messaging_state: Option, } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] @@ -1174,6 +1216,7 @@ mod test { and_spawn: None, with_monitor: None, apply_upgrade: None, + keep_messaging_state: None, }; assert_eq!(config.get_parachains().len(), 0); @@ -1228,6 +1271,7 @@ mod test { and_spawn: None, with_monitor: None, apply_upgrade: None, + keep_messaging_state: None, }; let parachains = config.get_parachains(); diff --git a/src/doppelganger.rs b/src/doppelganger.rs index 281c92b..e6c6386 100644 --- a/src/doppelganger.rs +++ b/src/doppelganger.rs @@ -21,7 +21,7 @@ use flate2::Compression; use tar::Builder; use tracing::debug; -use tracing::{info, trace}; +use tracing::{info, trace, warn}; use zombienet_configuration::shared::types::AssetLocation; use zombienet_configuration::NetworkConfigBuilder; use zombienet_orchestrator::network::Network; @@ -38,8 +38,9 @@ use crate::utils::{ }; use crate::config::{ - get_assigned_cores, get_state_pruning_config, Context, Parachain, Relaychain, Step, Upgrades, + get_assigned_cores, get_state_pruning_config, BiteOptions, Context, Parachain, Relaychain, Step, }; +use crate::metadata::ChainMetadata; use crate::overrides::{generate_default_overrides_for_para, generate_default_overrides_for_rc}; use crate::sync::{sync_para, sync_relay_only}; @@ -64,7 +65,7 @@ pub async fn doppelganger_inner( relay_chain: Relaychain, paras_to: Vec, database: &str, - upgrades: &Upgrades, + opts: &BiteOptions, ) -> Result<(), anyhow::Error> { // Star the node and wait until finish (with temp dir managed by us) info!( @@ -93,13 +94,31 @@ pub async fn doppelganger_inner( // Parachain sync let mut syncs = vec![]; for para in ¶s_to { + let para_meta = match para + .rpc_endpoint() + .map(str::to_string) + .or_else(|| para.default_rpc_endpoint(&relay_chain)) + { + Some(url) => { + ChainMetadata::fetch(&format!("para {}", para.id()), &url, para.at_block()).await + } + None => { + warn!( + "para {}: no 'rpc_endpoint' configured, overrides will not be verified against the runtime", + para.id() + ); + None + } + }; let para_default_overrides_path = generate_default_overrides_for_para( &base_dir_str, para, &relay_chain, - upgrades.paras.get(¶.id()).map(String::as_str), + opts.upgrades.paras.get(¶.id()).map(String::as_str), + para_meta.as_ref(), + opts.keep_messaging_state, ) - .await; + .await?; let info_path = format!("{base_dir_str}/para-{}.txt", para.id()); let maybe_target_header_path = if let Some(at_block) = para.at_block() { @@ -222,16 +241,25 @@ pub async fn doppelganger_inner( } let req_cores: u32 = paras_to.iter().fold(0u32, |acc, para| { - acc + get_assigned_cores(&relay_chain, para) + acc + get_assigned_cores(&relay_chain, para, &opts.cores) }); + let rc_meta = ChainMetadata::fetch( + &relay_chain.as_chain_string(), + &relay_chain.rpc_endpoint(), + relay_chain.at_block(), + ) + .await; let rc_default_overrides_path = generate_default_overrides_for_rc( &base_dir_str, &relay_chain, ¶s_to, req_cores, - upgrades.relay.as_deref(), + opts.upgrades.relay.as_deref(), + rc_meta.as_ref(), + &opts.cores, + opts.keep_messaging_state, ) - .await; + .await?; let rc_info_path = format!("{base_dir_str}/rc_info.txt"); // RELAYCHAIN sync @@ -368,14 +396,14 @@ pub async fn doppelganger_inner( // Carried upgrade blobs live next to ready.json (outside the step dirs, so // they survive clean-up) and must match the seeded System::AuthorizedUpgrade. let global_base_dir_str = global_base_dir.to_string_lossy(); - if let Some(upgrade_wasm) = &upgrades.relay { + if let Some(upgrade_wasm) = &opts.upgrades.relay { let blob_name = format!("{}-upgrade.wasm", relay_chain.as_chain_string()); let (blob, hash) = copy_upgrade_blob(upgrade_wasm, &global_base_dir_str, &blob_name).await; ready_content["rc_upgrade_wasm"] = json!(blob); ready_content["rc_upgrade_hash"] = json!(hash); } for para in ¶s_to { - if let Some(upgrade_wasm) = upgrades.paras.get(¶.id()) { + if let Some(upgrade_wasm) = opts.upgrades.paras.get(¶.id()) { let blob_name = format!( "{}-upgrade.wasm", para.as_chain_string(&relay_chain.as_chain_string()) diff --git a/src/main.rs b/src/main.rs index c93cef1..caf26a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ use zombienet_sdk::{LocalFileSystem, Network, NetworkNode}; mod cli; mod config; mod doppelganger; +mod metadata; mod monit; mod overrides; mod sync; @@ -165,6 +166,8 @@ async fn main() -> Result<(), anyhow::Error> { relay_upgrade, para_upgrade, apply_upgrade, + keep_messaging_state, + para_cores, } => { if with_monitor && !and_spawn { bail!("--with-monitor can only be used with --and-spawn"); @@ -182,12 +185,14 @@ async fn main() -> Result<(), anyhow::Error> { relay_upgrade, para_upgrade, apply_upgrade, + keep_messaging_state, + para_cores, )?; if resolved_config.apply_upgrade && !resolved_config.and_spawn { bail!("--apply-upgrade can only be used with --and-spawn"); } - if resolved_config.apply_upgrade && resolved_config.upgrades.is_empty() { + if resolved_config.apply_upgrade && resolved_config.opts.upgrades.is_empty() { bail!("--apply-upgrade needs an upgrade to carry (--rc-upgrade / --para-upgrade)"); } @@ -197,7 +202,7 @@ async fn main() -> Result<(), anyhow::Error> { resolved_config.relaychain, resolved_config.parachains, &database, - &resolved_config.upgrades, + &resolved_config.opts, ) .await .expect("bite should work"); diff --git a/src/metadata.rs b/src/metadata.rs new file mode 100644 index 0000000..c6a789b --- /dev/null +++ b/src/metadata.rs @@ -0,0 +1,286 @@ +//! Metadata-checked storage overrides. +//! +//! Storage keys are derived from pallet/item names and every candidate value is +//! decoded against the item's real on-chain type, so a runtime that renames an +//! item, changes a type or reorders a struct fails the bite loudly instead of +//! producing a network that silently never builds blocks. + +use anyhow::{anyhow, bail}; +use tracing::{debug, warn}; +use zombienet_sdk::subxt::{ + ext::{ + scale_value, + subxt_rpcs::{client::RpcParams, RpcClient}, + }, + Metadata, +}; + +pub fn storage_key(pallet: &str, item: &str) -> String { + array_bytes::bytes2hex( + "", + substorager::storage_value_key(pallet.as_bytes(), item.as_bytes()), + ) +} + +/// What an override is checked against. `ChainMetadata` is the real +/// implementation; tests use a double. +pub trait RuntimeCheck { + fn has_item(&self, pallet: &str, item: &str) -> bool; + fn verify_value(&self, pallet: &str, item: &str, value_hex: &str) -> Result<(), anyhow::Error>; +} + +pub struct ChainMetadata { + rpc: RpcClient, + metadata: Metadata, + /// Block the state is read at, so metadata and storage come from the same + /// runtime as the state being imported. + at: Option, +} + +impl ChainMetadata { + /// Fetch metadata from the chain being bitten, at `at_block` when the bite + /// is pinned to a block (otherwise at head). Returns `None` (with a + /// warning) when the endpoint can't be reached, so a bite without network + /// access to the source falls back to unverified overrides rather than + /// failing outright. + pub async fn fetch(chain: &str, url: &str, at_block: Option) -> Option { + let rpc = match RpcClient::from_url(url).await { + Ok(rpc) => rpc, + Err(e) => { + warn!("{chain}: can't reach {url}, overrides will not be verified against the runtime: {e}"); + return None; + } + }; + + let at = match at_block { + Some(block) => match block_hash(&rpc, block).await { + Ok(Some(hash)) => Some(hash), + _ => { + warn!("{chain}: can't resolve the hash of block {block}, overrides will not be verified against the runtime"); + return None; + } + }, + None => None, + }; + + match fetch_metadata(&rpc, at.as_deref()).await { + Ok(metadata) => { + debug!( + "{chain}: metadata fetched from {url} at {}", + at.as_deref().unwrap_or("head") + ); + Some(Self { rpc, metadata, at }) + } + Err(e) => { + warn!("{chain}: can't fetch metadata from {url}, overrides will not be verified against the runtime: {e}"); + None + } + } + } + + /// Type id of the item's value, or `None` when the runtime has no such + /// pallet or item. + fn value_ty(&self, pallet: &str, item: &str) -> Option { + Some( + self.metadata + .pallet_by_name(pallet)? + .storage()? + .entry_by_name(item)? + .entry_type() + .value_ty(), + ) + } + + fn verify(&self, pallet: &str, item: &str, value_hex: &str) -> Result<(), anyhow::Error> { + let ty = self + .value_ty(pallet, item) + .ok_or_else(|| anyhow!("{pallet}::{item} not in metadata"))?; + let bytes = hex::decode(value_hex) + .map_err(|e| anyhow!("{pallet}::{item}: value is not valid hex: {e}"))?; + + let mut cursor = &bytes[..]; + let value = scale_value::scale::decode_as_type(&mut cursor, ty, self.metadata.types()) + .map_err(|e| { + anyhow!("{pallet}::{item}: value does not decode as its on-chain type: {e}") + })?; + if !cursor.is_empty() { + bail!( + "{pallet}::{item}: {} trailing byte(s) after decoding, the value is malformed (wrong length prefix?)", + cursor.len() + ); + } + + let mut re_encoded = vec![]; + scale_value::scale::encode_as_type(&value, ty, self.metadata.types(), &mut re_encoded) + .map_err(|e| anyhow!("{pallet}::{item}: value does not re-encode: {e}"))?; + if re_encoded != bytes { + bail!( + "{pallet}::{item}: value is not byte-identical after a decode/encode round-trip (got 0x{}, expected 0x{value_hex})", + hex::encode(&re_encoded) + ); + } + Ok(()) + } + + /// Read a live storage value as hex (no `0x` prefix), at the same block the + /// metadata came from. + pub async fn storage_value(&self, key: &str) -> Result, anyhow::Error> { + let mut params = RpcParams::new(); + params.push(format!("0x{key}"))?; + if let Some(at) = &self.at { + params.push(at)?; + } + let raw: Option = self.rpc.request("state_getStorage", params).await?; + Ok(raw.map(|v| v.trim_start_matches("0x").to_string())) + } + + /// 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. + pub fn patch_value( + &self, + pallet: &str, + item: &str, + value_hex: &str, + patch: impl FnOnce(&mut scale_value::Value) -> Result<(), anyhow::Error>, + ) -> Result { + let ty = self + .value_ty(pallet, item) + .ok_or_else(|| anyhow!("{pallet}::{item} not in metadata"))?; + let bytes = hex::decode(value_hex)?; + let mut cursor = &bytes[..]; + let mut value = scale_value::scale::decode_as_type(&mut cursor, ty, self.metadata.types()) + .map_err(|e| anyhow!("{pallet}::{item}: live value does not decode: {e}"))?; + // Trailing bytes mean the metadata and the value disagree (e.g. the + // runtime upgraded between the two reads); patching would silently + // truncate the tail and still round-trip cleanly. + if !cursor.is_empty() { + bail!( + "{pallet}::{item}: live value has {} trailing byte(s) against this runtime's type, refusing to patch it", + cursor.len() + ); + } + + patch(&mut value)?; + + let mut out = vec![]; + scale_value::scale::encode_as_type(&value, ty, self.metadata.types(), &mut out) + .map_err(|e| anyhow!("{pallet}::{item}: patched value does not re-encode: {e}"))?; + Ok(hex::encode(out)) + } +} + +impl RuntimeCheck for ChainMetadata { + fn has_item(&self, pallet: &str, item: &str) -> bool { + self.value_ty(pallet, item).is_some() + } + + /// Require `value_hex` to decode against the item's on-chain type and + /// re-encode to the same bytes. Trailing bytes are an error too - that is + /// what a wrong length prefix looks like. + fn verify_value(&self, pallet: &str, item: &str, value_hex: &str) -> Result<(), anyhow::Error> { + self.verify(pallet, item, value_hex) + } +} + +async fn block_hash(rpc: &RpcClient, block: u32) -> Result, anyhow::Error> { + let mut params = RpcParams::new(); + params.push(block)?; + Ok(rpc.request("chain_getBlockHash", params).await?) +} + +async fn fetch_metadata(rpc: &RpcClient, at: Option<&str>) -> Result { + use zombienet_sdk::subxt::ext::{ + codec::Decode, + frame_metadata::{RuntimeMetadata, RuntimeMetadataPrefixed}, + }; + + let mut params = RpcParams::new(); + if let Some(at) = at { + params.push(at)?; + } + let raw: String = rpc.request("state_getMetadata", params).await?; + let bytes = hex::decode(raw.trim_start_matches("0x"))?; + let prefixed = RuntimeMetadataPrefixed::decode(&mut &bytes[..])?; + if !matches!( + prefixed.1, + RuntimeMetadata::V14(_) | RuntimeMetadata::V15(_) + ) { + bail!("unsupported metadata version, expected v14 or v15"); + } + Metadata::try_from(prefixed).map_err(|e| anyhow!("can't read metadata: {e}")) +} + +/// Get a mutable handle on a named inner struct. +pub fn nested_mut<'v>( + value: &'v mut scale_value::Value, + field: &str, +) -> Option<&'v mut scale_value::Value> { + let scale_value::ValueDef::Composite(scale_value::Composite::Named(fields)) = &mut value.value + else { + return None; + }; + fields + .iter_mut() + .find(|(name, _)| name == field) + .map(|(_, v)| v) +} + +/// Set a named field on a composite `Value`, keeping every other field as the +/// live runtime had it. +pub fn set_field( + value: &mut scale_value::Value, + field: &str, + to: scale_value::Value, +) -> Result<(), anyhow::Error> { + let scale_value::ValueDef::Composite(scale_value::Composite::Named(fields)) = &mut value.value + else { + bail!("expected a struct with named fields to set '{field}' on"); + }; + let entry = fields + .iter_mut() + .find(|(name, _)| name == field) + .ok_or_else(|| anyhow!("no field '{field}' in value"))?; + entry.1 = to; + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + use zombienet_sdk::subxt::ext::scale_value::{value, Value as ScaleValue}; + + fn cores(n: u128) -> ScaleValue { + ScaleValue::u128(n).map_context(|_| 0_u32) + } + + #[test] + fn set_field_replaces_only_that_field() { + let mut v = value!({ num_cores: 18u32, max_pov_size: 5u32 }).map_context(|_| 0_u32); + set_field(&mut v, "num_cores", cores(5)).unwrap(); + + let expected = value!({ num_cores: 5u32, max_pov_size: 5u32 }).map_context(|_| 0_u32); + assert_eq!(v, expected); + } + + #[test] + fn set_field_errors_on_unknown_field_and_wrong_shape() { + let mut named = value!({ num_cores: 1u32 }).map_context(|_| 0_u32); + assert!(set_field(&mut named, "nope", cores(5)).is_err()); + + let mut unnamed = ScaleValue::u128(1).map_context(|_| 0_u32); + assert!(set_field(&mut unnamed, "num_cores", cores(5)).is_err()); + } + + #[test] + fn nested_mut_finds_the_inner_struct() { + let mut v = value!({ scheduler_params: { num_cores: 18u32 } }).map_context(|_| 0_u32); + + let params = nested_mut(&mut v, "scheduler_params").expect("nested struct"); + set_field(params, "num_cores", cores(5)).unwrap(); + + let expected = value!({ scheduler_params: { num_cores: 5u32 } }).map_context(|_| 0_u32); + assert_eq!(v, expected); + assert!(nested_mut(&mut v, "missing").is_none()); + } +} diff --git a/src/overrides.rs b/src/overrides.rs index 39ed095..ab81eb0 100644 --- a/src/overrides.rs +++ b/src/overrides.rs @@ -2,9 +2,11 @@ use codec::Encode; use serde_json::{json, Value}; use std::{env, path::PathBuf}; use tokio::fs; +use tracing::{info, warn}; use crate::{ - config::{get_assigned_cores, Parachain, Relaychain}, + config::{get_assigned_cores, CoresOverride, Parachain, Relaychain}, + metadata::{nested_mut, set_field, storage_key, ChainMetadata, RuntimeCheck}, utils::{ generate_collator_key_from_seed, generate_collator_next_keys_injects, get_validator_keys, ParaId, ValidationCode, @@ -12,48 +14,228 @@ use crate::{ }; use zombienet_sdk::generators::core_assignment; +use zombienet_sdk::subxt::ext::scale_value::Value as ScaleValue; + +/// Storage overrides and injects for one chain, keyed by pallet and item name. +/// +/// Every entry is checked against the chain's own metadata when it is +/// available: an item the runtime does not have is skipped, and a value that +/// does not survive a decode/encode round-trip against its real on-chain type +/// fails the bite. +struct OverrideSet<'a> { + meta: Option<&'a dyn RuntimeCheck>, + overrides: Value, + injects: Value, + skipped: Vec, + errors: Vec, +} + +impl<'a> OverrideSet<'a> { + fn new(meta: Option<&'a dyn RuntimeCheck>) -> Self { + Self { + meta, + overrides: json!({}), + injects: json!({}), + skipped: vec![], + errors: vec![], + } + } + + /// `None` when the entry should be dropped (item absent, or value invalid). + /// + /// `required` entries are ones the user explicitly asked for (a carried + /// upgrade, a wasm override, a sudo key): an item the runtime does not have + /// is an error there, not something to quietly drop. + fn checked_key( + &mut self, + pallet: &str, + item: &str, + value: &str, + required: bool, + ) -> Option { + if let Some(meta) = self.meta { + if !meta.has_item(pallet, item) { + if required { + self.errors.push(format!( + "{pallet}::{item} is not in the runtime, so it can't be set" + )); + } else { + self.skipped.push(format!("{pallet}::{item}")); + } + return None; + } + if let Err(e) = meta.verify_value(pallet, item, value) { + self.errors.push(e.to_string()); + return None; + } + } + Some(storage_key(pallet, item)) + } + + fn set(&mut self, pallet: &str, item: &str, value: impl AsRef) { + let value = value.as_ref(); + if let Some(key) = self.checked_key(pallet, item, value, false) { + self.overrides[key] = json!(value); + } + } + + /// Like `set`, for an entry the user asked for explicitly. + fn set_required(&mut self, pallet: &str, item: &str, value: impl AsRef) { + let value = value.as_ref(); + if let Some(key) = self.checked_key(pallet, item, value, true) { + self.overrides[key] = json!(value); + } + } + + /// Map entry; `key_suffix` is the already-hashed map key. + fn set_map(&mut self, pallet: &str, item: &str, key_suffix: &str, value: impl AsRef) { + let value = value.as_ref(); + if let Some(key) = self.checked_key(pallet, item, value, false) { + self.overrides[format!("{key}{key_suffix}")] = json!(value); + } + } + + fn set_map_required( + &mut self, + pallet: &str, + item: &str, + key_suffix: &str, + value: impl AsRef, + ) { + let value = value.as_ref(); + if let Some(key) = self.checked_key(pallet, item, value, true) { + self.overrides[format!("{key}{key_suffix}")] = json!(value); + } + } + + fn inject(&mut self, pallet: &str, item: &str, value: impl AsRef) { + let value = value.as_ref(); + if let Some(key) = self.checked_key(pallet, item, value, false) { + self.injects[key] = json!(value); + } + } + + fn inject_required(&mut self, pallet: &str, item: &str, value: impl AsRef) { + let value = value.as_ref(); + if let Some(key) = self.checked_key(pallet, item, value, true) { + self.injects[key] = json!(value); + } + } + + fn inject_map(&mut self, pallet: &str, item: &str, key_suffix: &str, value: impl AsRef) { + let value = value.as_ref(); + if let Some(key) = self.checked_key(pallet, item, value, false) { + self.injects[format!("{key}{key_suffix}")] = json!(value); + } + } + + fn inject_map_required( + &mut self, + pallet: &str, + item: &str, + key_suffix: &str, + value: impl AsRef, + ) { + let value = value.as_ref(); + if let Some(key) = self.checked_key(pallet, item, value, true) { + self.injects[format!("{key}{key_suffix}")] = json!(value); + } + } + + /// Item the runtime must have, whose value is too large to be worth + /// verifying (a multi-MB runtime blob decodes into millions of `Value` + /// nodes and the value is built by us from `Encode` anyway). + fn inject_map_unverified( + &mut self, + pallet: &str, + item: &str, + key_suffix: &str, + value: impl AsRef, + ) { + if let Some(meta) = self.meta { + if !meta.has_item(pallet, item) { + self.errors.push(format!( + "{pallet}::{item} is not in the runtime, so it can't be set" + )); + return; + } + } + let key = storage_key(pallet, item); + self.injects[format!("{key}{key_suffix}")] = json!(value.as_ref()); + } + + /// Well-known keys that are not pallet storage items (`:code`, + /// `:UsePreviousValidators:`), so there is no type to check them against. + fn set_raw(&mut self, key: &str, value: impl AsRef) { + self.overrides[key] = json!(value.as_ref()); + } + + fn inject_raw(&mut self, key: &str, value: impl AsRef) { + self.injects[key] = json!(value.as_ref()); + } + + fn finish(self, chain: &str) -> Result<(Value, Value), anyhow::Error> { + if !self.errors.is_empty() { + anyhow::bail!( + "{chain}: {} override(s) do not match the runtime:\n - {}", + self.errors.len(), + self.errors.join("\n - ") + ); + } + if !self.skipped.is_empty() { + info!( + "{chain}: skipped {} override(s) the runtime does not have: {}", + self.skipped.len(), + self.skipped.join(", ") + ); + } + Ok((self.overrides, self.injects)) + } +} /// Seed `System::AuthorizedUpgrade` with the blob's hash, the state a passed /// `authorize_upgrade(hash)` referendum leaves behind. The permissionless /// `apply_authorized_upgrade(blob)` can then enact the upgrade through the /// production path, which needs no sudo (usable on Kusama/Polkadot forks). -async fn inject_authorized_upgrade(injects: &mut Value, upgrade_wasm: &str) { +async fn inject_authorized_upgrade(set: &mut OverrideSet<'_>, upgrade_wasm: &str) { let wasm_content = fs::read(upgrade_wasm) .await .unwrap_or_else(|_| panic!("Error reading upgrade wasm from path {}", upgrade_wasm)); - let auth_key = array_bytes::bytes2hex( - "", - substorager::storage_value_key(&b"System"[..], b"AuthorizedUpgrade"), - ); // CodeUpgradeAuthorization { code_hash, check_version: true } let value = format!( "{}01", hex::encode(subhasher::blake2_256(&wasm_content[..])) ); - injects[auth_key] = Value::String(value); + set.inject_required("System", "AuthorizedUpgrade", value); } -/// Generate the injects for Session.NextKeys storage overrides for validators -fn generate_next_keys_injects( - validator_keys: &[&crate::utils::ValidatorKeys], -) -> serde_json::Value { - let mut next_keys_injects = serde_json::json!({}); - for keys in validator_keys { - let stash_bytes = hex::decode(keys.stash).expect("stash should be valid hex"); - let stash_hash = array_bytes::bytes2hex("", &subhasher::twox64_concat(&stash_bytes)[..8]); - let inject_key = format!( - "cec5070d609dd3497f72bde07fc96ba04c014e6bf8b8c2c011e7290b85696bb3{}{}", - stash_hash, keys.stash - ); - next_keys_injects[inject_key] = serde_json::json!(keys.session_keys_encoded()); +/// Patch `num_cores` into the live `HostConfiguration`, leaving every other +/// field the production chain configured (executor params, async backing, +/// max_pov_size) untouched. +/// +/// The built-in per-relay blob is only used when the source can't be reached at +/// all: it is a snapshot of a past runtime, so it drops whatever the live chain +/// has configured since. +async fn host_config( + relay: &Relaychain, + num_cores: u32, + meta: Option<&ChainMetadata>, +) -> Result { + if let Some(meta) = meta { + let key = storage_key("Configuration", "ActiveConfig"); + let live = meta + .storage_value(&key) + .await? + .ok_or_else(|| anyhow::anyhow!("Configuration::ActiveConfig is empty on the source chain, refusing to replace it with a built-in blob"))?; + let patched = patch_num_cores(meta, &live, num_cores)?; + info!("Configuration::ActiveConfig patched from the live value (num_cores -> {num_cores})"); + return Ok(patched); } - next_keys_injects -} -// Build HostConfig per Relay -fn host_config(relay: &Relaychain, num_cores: u32) -> 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()); - match relay { + Ok(match relay { Relaychain::Westend { .. } => { format!("00003000005000005555150000008000fbff0100000200000a000000c80000006400000006000000020000000000a00000c800000a00000000c0220fca950300000000000000000000c0220fca9503000000000000000000e8030000009001000a000000009001000c01002000000600c4090000000000000601983a0000000000008070000001c800000006000000580200000200000028000000000000000200000001000000020000000f00000002000000100a010000000a00000005000000010500000005000000{}1027000080b2e60e80c3c9018096980000000000000000000000000000000000", cores) } @@ -66,10 +248,49 @@ fn host_config(relay: &Relaychain, num_cores: u32) -> String { Relaychain::Paseo { .. } => { format!("e067350000800000aaaa020000001000fbff0000100000000a0000003c0000003c00000003000000020000000000a00000c800001e0000000000000000000000000000000000000000000000000000000000000000000000e8030000009001001e000000009001000c01002000000600c4090000000000000601983a000000000000b00400000006000000640000000200000019000000000000000200000002000000020000000500000001000000100b010000000a00000004000000010300000005000000{}6400000080b2e60e80c3c9018096980000000000000000000000000000000000", cores) } + }) +} + +/// `num_cores` lives in `scheduler_params` on current runtimes and at the top +/// level on older ones. +fn patch_num_cores( + meta: &ChainMetadata, + live: &str, + num_cores: u32, +) -> Result { + meta.patch_value("Configuration", "ActiveConfig", live, |value| { + // context is only used for decode diagnostics, encoding ignores it + let cores = ScaleValue::u128(num_cores as u128).map_context(|_| 0_u32); + if let Some(params) = nested_mut(value, "scheduler_params") { + set_field(params, "num_cores", cores) + } else { + set_field(value, "num_cores", cores) + } + }) +} + +/// Generate the injects for Session.NextKeys storage overrides for validators +fn generate_next_keys_injects( + set: &mut OverrideSet<'_>, + validator_keys: &[&crate::utils::ValidatorKeys], +) { + for keys in validator_keys { + let stash_bytes = hex::decode(keys.stash).expect("stash should be valid hex"); + let stash_hash = array_bytes::bytes2hex("", &subhasher::twox64_concat(&stash_bytes)[..8]); + set.inject_map( + "Session", + "NextKeys", + &format!("{stash_hash}{}", keys.stash), + keys.session_keys_encoded(), + ); } } + /// Generate the storage overrides for relay chain validators -pub fn generate_rc_overrides(validator_keys: &[&crate::utils::ValidatorKeys]) -> serde_json::Value { +fn generate_rc_overrides( + set: &mut OverrideSet<'_>, + validator_keys: &[&crate::utils::ValidatorKeys], +) { let num_validators = validator_keys.len(); // Build stash list for validators (concatenated hex) @@ -128,56 +349,72 @@ pub fn generate_rc_overrides(validator_keys: &[&crate::utils::ValidatorKeys]) -> // Format validator count as compact encoded let validator_count_hex = format!("{:02x}", num_validators * 4); // *4 because we encode each as 4 bytes - // Build base overrides object - let overrides = json!({ - // Validator Validators (dynamic list) - "7d9fe37370ac390779f35763d98106e888dcde934c658227ee1dfafcd6e16903": format!("{}{}", validator_count_hex, stash_list), - // Session Validators (dynamic list) - "cec5070d609dd3497f72bde07fc96ba088dcde934c658227ee1dfafcd6e16903": format!("{}{}", validator_count_hex, stash_list), - // Session QueuedKeys (dynamic list) - "cec5070d609dd3497f72bde07fc96ba0e0cdd062e6eaf24295ad4ccfc41d4609": format!("{}{}", validator_count_hex, queued_keys), - // Babe Authorities (dynamic list) - "1cb6f36e027abb2091cfb5110ab5087f5e0621c4869aa60c02be9adcc98a0d1d": format!("{}{}", validator_count_hex, babe_authorities), - // Babe NextAuthorities (dynamic list) - "1cb6f36e027abb2091cfb5110ab5087faacf00b9b41fda7a9268821c2a2b3e4c": format!("{}{}", validator_count_hex, babe_authorities), - // Grandpa Authorities (dynamic list) - "5f9cc45b7a00c5899361e1c6099678dc5e0621c4869aa60c02be9adcc98a0d1d": format!("{}{}", validator_count_hex, grandpa_authorities), - // Staking Invulnerables (dynamic list) - "5f3e4907f716ac89b6347d15ececedca5579297f4dfb9609e7e4c2ebab9ce40a": format!("{}{}", validator_count_hex, stash_list), - // paraScheduler validatorGroup (dynamic groups based on validator count) - "94eadf0156a8ad5156507773d0471e4a16973e1142f5bd30d9464076794007db": validator_groups, - // paraShared activeValidatorIndices (dynamic) - "b341e3a63e58a188839b242d17f8c9f82586833f834350b4d435d5fd269ecc8b": format!("{}{}", validator_count_hex, validator_indices), - // paraShared activeValidatorKeys (dynamic) - "b341e3a63e58a188839b242d17f8c9f87a50c904b368210021127f9238883a6e": format!("{}{}", validator_count_hex, para_validator_keys), - // authorityDiscovery keys (dynamic) - "2099d7f109d6e535fb000bba623fd4409f99a2ce711f3a31b2fc05604c93f179": format!("{}{}", validator_count_hex, authority_discovery_keys), - // authorityDiscovery nextKeys (dynamic) - "2099d7f109d6e535fb000bba623fd4404c014e6bf8b8c2c011e7290b85696bb3": format!("{}{}", validator_count_hex, authority_discovery_keys), - // Sudo Key (Alice) - "5c0d1176a568c1f92944340dbfed9e9c530ebca703c85910e7164cb7d1c9e47b": "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", - }); - - overrides + let stashes = format!("{validator_count_hex}{stash_list}"); + // Only present on chains using the validator-set pallet. + set.set("ValidatorSet", "Validators", &stashes); + set.set("Session", "Validators", &stashes); + set.set("Staking", "Invulnerables", &stashes); + set.set( + "Session", + "QueuedKeys", + format!("{validator_count_hex}{queued_keys}"), + ); + set.set( + "Babe", + "Authorities", + format!("{validator_count_hex}{babe_authorities}"), + ); + set.set( + "Babe", + "NextAuthorities", + format!("{validator_count_hex}{babe_authorities}"), + ); + set.set( + "Grandpa", + "Authorities", + format!("{validator_count_hex}{grandpa_authorities}"), + ); + set.set("ParaScheduler", "ValidatorGroups", &validator_groups); + set.set( + "ParasShared", + "ActiveValidatorIndices", + format!("{validator_count_hex}{validator_indices}"), + ); + set.set( + "ParasShared", + "ActiveValidatorKeys", + format!("{validator_count_hex}{para_validator_keys}"), + ); + set.set( + "AuthorityDiscovery", + "Keys", + format!("{validator_count_hex}{authority_discovery_keys}"), + ); + set.set( + "AuthorityDiscovery", + "NextKeys", + format!("{validator_count_hex}{authority_discovery_keys}"), + ); + set.set( + "Sudo", + "Key", + "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + ); } -fn augment_overrides_for_paras(relay: &Relaychain, paras: &[&Parachain], overrides: &mut Value) { +fn augment_overrides_for_paras( + set: &mut OverrideSet<'_>, + relay: &Relaychain, + paras: &[&Parachain], + cores_override: &CoresOverride, + keep_messaging_state: bool, +) { // Generate paras_parachains let para_ids: Vec = paras.iter().map(|para| para.id()).collect(); - let paras_parachains = generate_paras_parachains_value(para_ids); - - // paras parachains (dynamic based on first parachain) - overrides["cd710b30bd2eab0352ddcc26417aa1940b76934f4cc08dee01012d059e1b83ee"] = - json!(paras_parachains); - - // Add DMP and HRMP storage keys for each parachain - let dmp_dmqh_prefix = array_bytes::bytes2hex( - "", - substorager::storage_value_key(&b"Dmp"[..], b"DownwardMessageQueueHeads"), - ); - let hrmp_hici_prefix = array_bytes::bytes2hex( - "", - substorager::storage_value_key(&b"Hrmp"[..], b"HrmpIngressChannelsIndex"), + set.set( + "Paras", + "Parachains", + generate_paras_parachains_value(para_ids), ); // used to assign cores @@ -191,80 +428,96 @@ fn augment_overrides_for_paras(relay: &Relaychain, paras: &[&Parachain], overrid let para_hex = array_bytes::bytes2hex("", para_id.encode()); let para_key_part = format!("{para_twox64}{para_hex}"); - // DMP downwardMessageQueueHeads (empty for each para) - let dmp_queue_key = format!("{dmp_dmqh_prefix}{para_key_part}"); - overrides[dmp_queue_key] = - json!("0000000000000000000000000000000000000000000000000000000000000000"); - - // HRMP hrmpIngressChannelsIndex (empty for each para) - let hrmp_channels_key = format!("{hrmp_hici_prefix}{para_key_part}"); - overrides[hrmp_channels_key] = json!("00"); + if keep_messaging_state { + // The relay and parachain snapshots agree on channel heads (a + // self-owned relay), so inherited HRMP/DMP channels stay usable. + } else { + set.set_map( + "Dmp", + "DownwardMessageQueueHeads", + ¶_key_part, + "0000000000000000000000000000000000000000000000000000000000000000", + ); + set.set_map("Hrmp", "HrmpIngressChannelsIndex", ¶_key_part, "00"); + } // ParaScheduler - let para_cores = get_assigned_cores(relay, para); + 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_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`) - let scheduler_key = core_assignment::get_parascheduler_storage_key(); - overrides[&scheduler_key[2..]] = json!(core_assign_value); } + + 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); } +#[allow(clippy::too_many_arguments)] pub async fn generate_default_overrides_for_rc( base_dir: &str, relay: &Relaychain, paras: &Vec, req_cores: u32, maybe_upgrade: Option<&str>, -) -> PathBuf { + meta: Option<&ChainMetadata>, + cores_override: &CoresOverride, + keep_messaging_state: bool, +) -> Result { let num_validators = crate::config::num_validators_for_cores(req_cores); let validator_keys = get_validator_keys(num_validators as usize); - let next_keys_injects = generate_next_keys_injects(&validator_keys); + let mut set = OverrideSet::new(meta.map(|m| m as &dyn RuntimeCheck)); - // Generate the rc overrides with parachains - let paras_refs: Vec<&Parachain> = paras.iter().collect(); - let mut overrides = generate_rc_overrides(&validator_keys); + generate_rc_overrides(&mut set, &validator_keys); - // add the paras related keys to override to _overrides_ json - augment_overrides_for_paras(relay, ¶s_refs, &mut overrides); + // add the paras related keys to override + let paras_refs: Vec<&Parachain> = paras.iter().collect(); + augment_overrides_for_paras( + &mut set, + relay, + ¶s_refs, + cores_override, + keep_messaging_state, + ); - // Override Configuration activeConfig - overrides["06de3d8a54d27e44a9d5ce189618f22db4b49d95320d9021994c850f25b8e385"] = - json!(host_config(relay, req_cores)); + set.set( + "Configuration", + "ActiveConfig", + host_config(relay, req_cores, meta).await?, + ); - // Keys to inject (mostly storage maps that are not present in the current state) - // < Item> - let mut injects = next_keys_injects; + generate_next_keys_injects(&mut set, &validator_keys); // set `UsePreviousValidators` to true to keep using the same validator set. - injects["c57d82d01f0fc18afc048ca20ac460dd"] = json!("01"); + set.inject_raw("c57d82d01f0fc18afc048ca20ac460dd", "01"); // RcMigrator Manager (set //Alice by default) - injects["2185d18cb42ae97242af0e70e6ad689012fcd13ee43ae32cc87f798eb5ed3295"] = - json!("d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"); + set.inject( + "RcMigrator", + "Manager", + "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + ); // update the overrides / injects map to use IFF the key is provided if let Ok(sudo_key) = env::var("ZOMBIE_SUDO") { - // Sudo Key - overrides["5c0d1176a568c1f92944340dbfed9e9c530ebca703c85910e7164cb7d1c9e47b"] = - Value::String(sudo_key.clone()); - - // RcMigrator Manager - injects["2185d18cb42ae97242af0e70e6ad689012fcd13ee43ae32cc87f798eb5ed3295"] = - Value::String(sudo_key); + set.set_required("Sudo", "Key", &sudo_key); + set.inject("RcMigrator", "Manager", &sudo_key); } if let Some(override_wasm) = relay.wasm_overrides() { let wasm_content = fs::read(override_wasm) .await .unwrap_or_else(|_| panic!("Error reading override_wasm from path {}", override_wasm)); - overrides["3a636f6465"] = Value::String(hex::encode(wasm_content)); + set.set_raw("3a636f6465", hex::encode(wasm_content)); + } + + if let Some(upgrade_wasm) = maybe_upgrade { + inject_authorized_upgrade(&mut set, upgrade_wasm).await; } // also check if any parachain includes a wasm override but we can't doit in the @@ -275,42 +528,24 @@ pub async fn generate_default_overrides_for_rc( panic!("Error reading override_wasm from path {}", override_wasm) }); let code_hash = hex::encode(subhasher::blake2_256(&wasm_content[..])); - - // we should now override let para_id_map_key = crate::utils::para_id_for_map_hash(para.id()); - // Paras.CurrentCodeHash(paraId) - let current_code_hash_prefix = array_bytes::bytes2hex( - "", - substorager::storage_value_key(&b"Paras"[..], b"CurrentCodeHash"), - ); - overrides[&format!("{current_code_hash_prefix}{para_id_map_key}")] = - Value::String(code_hash.clone()); - // Paras.CodeByHash (should be injected since is have a reference to hash of the code itself) - let code_by_hash_prefix = array_bytes::bytes2hex( - "", - substorager::storage_value_key(&b"Paras"[..], b"CodeByHash"), - ); + set.set_map_required("Paras", "CurrentCodeHash", ¶_id_map_key, &code_hash); + + // CodeByHash / CodeByHashRefs are injected since the map key is the + // hash of the code itself, so they are never in the imported state. let validation_code: ValidationCode = ValidationCode(wasm_content); - let validation_code_encoded = validation_code.encode(); - injects[&format!("{code_by_hash_prefix}{code_hash}")] = - Value::String(hex::encode(validation_code_encoded)); - - // Paras.CodeByHashRefs (should be injected since is have a reference to hash of the code itself) - let code_by_hash_prefix = array_bytes::bytes2hex( - "", - substorager::storage_value_key(&b"Paras"[..], b"CodeByHashRefs"), + set.inject_map_unverified( + "Paras", + "CodeByHash", + &code_hash, + hex::encode(validation_code.encode()), ); - // hardcoded to 1 encoded - injects[&format!("{code_by_hash_prefix}{code_hash}")] = - Value::String("01000000".into()); + set.inject_map_required("Paras", "CodeByHashRefs", &code_hash, "01000000"); } } - if let Some(upgrade_wasm) = maybe_upgrade { - inject_authorized_upgrade(&mut injects, upgrade_wasm).await; - } - + let (overrides, injects) = set.finish(&relay.as_chain_string())?; let full_content = json!({ "overrides": overrides, "injects": injects @@ -321,7 +556,7 @@ pub async fn generate_default_overrides_for_rc( fs::write(&file_path, contents) .await .expect("write file should works."); - file_path + Ok(file_path) } pub async fn generate_default_overrides_for_para( @@ -329,7 +564,9 @@ pub async fn generate_default_overrides_for_para( para: &Parachain, relay: &Relaychain, maybe_upgrade: Option<&str>, -) -> PathBuf { + meta: Option<&ChainMetadata>, + keep_messaging_state: bool, +) -> Result { // For AH determine key type based on relay chain: ed25519 for Polkadot, sr25519 for others let key_type = match (relay, para) { (Relaychain::Polkadot { .. }, Parachain::AssetHub { .. }) => "ed", @@ -340,41 +577,59 @@ pub async fn generate_default_overrides_for_para( let seed = format!("Collator-{}", para.id()); let key_to_use = generate_collator_key_from_seed(&seed, key_type); - // Generate the injects using the helper function - let mut injects = generate_collator_next_keys_injects(&key_to_use); + let mut set = OverrideSet::new(meta.map(|m| m as &dyn RuntimeCheck)); - if let Some(upgrade_wasm) = maybe_upgrade { - inject_authorized_upgrade(&mut injects, upgrade_wasm).await; + set.set("Session", "Validators", format!("04{key_to_use}")); + set.set( + "Session", + "QueuedKeys", + format!("04{key_to_use}{key_to_use}"), + ); + set.set( + "CollatorSelection", + "Invulnerables", + format!("04{key_to_use}"), + ); + set.set("Aura", "Authorities", format!("04{key_to_use}")); + set.set("AuraExt", "Authorities", format!("04{key_to_use}")); + set.set("CollatorSelection", "DesiredCandidates", "01000000"); + set.set( + "Sudo", + "Key", + "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + ); + + if !keep_messaging_state { + set.set( + "ParachainSystem", + "LastDmqMqcHead", + "0000000000000000000000000000000000000000000000000000000000000000", + ); } - // - // e.g Validator Validators - let mut overrides = json!({ - // Session Validators - "cec5070d609dd3497f72bde07fc96ba088dcde934c658227ee1dfafcd6e16903": &format!("04{key_to_use}"), - // Session QueuedKeys - "cec5070d609dd3497f72bde07fc96ba0e0cdd062e6eaf24295ad4ccfc41d4609": &format!("04{key_to_use}{key_to_use}"), - // CollatorSelection Invulnerables (collator) - "15464cac3378d46f113cd5b7a4d71c845579297f4dfb9609e7e4c2ebab9ce40a": &format!("04{key_to_use}"), - // Aura authorities - "57f8dc2f5ab09467896f47300f0424385e0621c4869aa60c02be9adcc98a0d1d": &format!("04{key_to_use}"), - // AuraExt authorities - "3c311d57d4daf52904616cf69648081e5e0621c4869aa60c02be9adcc98a0d1d": &format!("04{key_to_use}"), - // parachainSystem lastDmqMqcHead (emtpy) - "45323df7cc47150b3930e2666b0aa313911a5dd3f1155f5b7d0c5aa102a757f9": "0000000000000000000000000000000000000000000000000000000000000000", - // CollatorSelection DesiredCandidates (set to 1) - "15464cac3378d46f113cd5b7a4d71c84476f594316a7dfe49c1f352d95abdaf1": "01000000", - // Sudo Key (Alice) - "5c0d1176a568c1f92944340dbfed9e9c530ebca703c85910e7164cb7d1c9e47b": "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", - }); + // Session.NextKeys for the collator + for (key, value) in generate_collator_next_keys_injects(&key_to_use) + .as_object() + .expect("collator injects should be a map") + { + set.inject_raw( + key, + value.as_str().expect("collator inject should be a string"), + ); + } if let Some(override_wasm) = para.wasm_overrides() { let wasm_content = fs::read(override_wasm) .await .unwrap_or_else(|_| panic!("Error reading override_wasm from path {}", override_wasm)); - overrides["3a636f6465"] = Value::String(hex::encode(wasm_content)); + set.set_raw("3a636f6465", hex::encode(wasm_content)); + } + + if let Some(upgrade_wasm) = maybe_upgrade { + inject_authorized_upgrade(&mut set, upgrade_wasm).await; } + let (overrides, injects) = set.finish(&format!("para {}", para.id()))?; let full_content = json!({ "overrides": overrides, "injects": injects @@ -385,7 +640,7 @@ pub async fn generate_default_overrides_for_para( fs::write(&file_path, contents) .await .expect("write file should works."); - file_path + Ok(file_path) } fn generate_paras_parachains_value(ids: impl Into>) -> String { @@ -404,30 +659,6 @@ mod test { use super::*; - #[test] - fn genesis_slot_encode_u64() { - let prefix = array_bytes::bytes2hex( - "", - substorager::storage_value_key(&b"Babe"[..], b"GenesisSlot"), - ); - let a = 295769115_u64; - let a_encoded = a.encode(); - println!("{}: {}", prefix, array_bytes::bytes2hex("", a_encoded)); - } - - #[test] - fn encode_u32() { - let a = 100_u32; - let a_encoded = a.encode(); - println!("encoded: {}", array_bytes::bytes2hex("", a_encoded)); - } - - #[test] - fn validator_groups_count_hex() { - let validator_groups_count_hex = format!("{:02x}", 3 * 4); - println!("c: {validator_groups_count_hex}"); - } - #[test] fn generate_paras_parachains_value_works() { let value = generate_paras_parachains_value([1000_u32]); @@ -444,45 +675,28 @@ mod test { ¶s, 2, None, + None, + &CoresOverride::new(), + false, ) - .await; + .await + .unwrap(); } #[test] fn test_generate_next_keys_injects() { let validator_keys = get_validator_keys(2); - let next_keys_injects = generate_next_keys_injects(&validator_keys); - - let expected = json!({ - // Session NextKeys (alice) - "cec5070d609dd3497f72bde07fc96ba04c014e6bf8b8c2c011e7290b85696bb3e535263148daaf49be5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f": "88dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0eed43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27dd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27dd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27dd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1", - // Session NextKeys (bob) - "cec5070d609dd3497f72bde07fc96ba04c014e6bf8b8c2c011e7290b85696bb30e5be00fbc2e15b5fe65717dad0447d715f660a0a58411de509b42e6efb8375f562f58a554d5860e": "d17c2d7823ebf260fd138f2d7e27d114c0145d968b5ff5006125f2414fadae698eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a488eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a488eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a488eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a480390084fdbf27d2b79d26a4f13f0ccd982cb755a661969143c37cbc49ef5b91f27", - }); - - assert_eq!(next_keys_injects, expected); - } - - #[tokio::test] - async fn alive_and_bob_inject_keys() { - // Just 2 is alice and bob - let validator_keys = get_validator_keys(2); - let mut next_keys_injects = generate_next_keys_injects(&validator_keys); - - // RcMigrator Manager (set //Alice by default) - next_keys_injects["2185d18cb42ae97242af0e70e6ad689012fcd13ee43ae32cc87f798eb5ed3295"] = - json!("d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"); + let mut set = OverrideSet::new(None); + generate_next_keys_injects(&mut set, &validator_keys); let expected = json!({ // Session NextKeys (alice) "cec5070d609dd3497f72bde07fc96ba04c014e6bf8b8c2c011e7290b85696bb3e535263148daaf49be5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f": "88dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0eed43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27dd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27dd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27dd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1", // Session NextKeys (bob) "cec5070d609dd3497f72bde07fc96ba04c014e6bf8b8c2c011e7290b85696bb30e5be00fbc2e15b5fe65717dad0447d715f660a0a58411de509b42e6efb8375f562f58a554d5860e": "d17c2d7823ebf260fd138f2d7e27d114c0145d968b5ff5006125f2414fadae698eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a488eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a488eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a488eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a480390084fdbf27d2b79d26a4f13f0ccd982cb755a661969143c37cbc49ef5b91f27", - // RcMigrator Manager (set //Alice by default) see: https://github.com/polkadot-fellows/runtimes/blob/22116f7d02c220db4f7187c6967dbd6bf89274cf/pallets/rc-migrator/src/lib.rs#L702-L707 - "2185d18cb42ae97242af0e70e6ad689012fcd13ee43ae32cc87f798eb5ed3295": "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" }); - assert_eq!(next_keys_injects, expected); + assert_eq!(set.injects, expected); } #[test] @@ -491,11 +705,14 @@ mod test { let validator_keys = get_validator_keys(2); let para = crate::config::Parachain::new("asset-hub"); let paras = vec![¶]; - let mut overrides = generate_rc_overrides(&validator_keys); let rc = Relaychain::new("polkadot"); - augment_overrides_for_paras(&rc, ¶s, &mut overrides); - // Validator Validators + let mut set = OverrideSet::new(None); + generate_rc_overrides(&mut set, &validator_keys); + augment_overrides_for_paras(&mut set, &rc, ¶s, &CoresOverride::new(), false); + let overrides = set.overrides; + + // ValidatorSet Validators assert_eq!( overrides["7d9fe37370ac390779f35763d98106e888dcde934c658227ee1dfafcd6e16903"], "08be5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25ffe65717dad0447d715f660a0a58411de509b42e6efb8375f562f58a554d5860e" @@ -537,6 +754,7 @@ mod test { overrides["94eadf0156a8ad5156507773d0471e4a16973e1142f5bd30d9464076794007db"], array_bytes::bytes2hex("", expected_groups.encode()) ); + // pin the wire bytes too: compact(2) then each group with its own compact len assert_eq!( overrides["94eadf0156a8ad5156507773d0471e4a16973e1142f5bd30d9464076794007db"], "0804000000000401000000" @@ -559,94 +777,165 @@ mod test { overrides["5c0d1176a568c1f92944340dbfed9e9c530ebca703c85910e7164cb7d1c9e47b"], "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" ); - } - #[tokio::test] - async fn inject_authorized_upgrade_seeds_hash_and_check_version() { - let wasm_path = "/tmp/zombie-bite-test-upgrade.wasm"; - let wasm = b"not-a-real-runtime"; - tokio::fs::write(wasm_path, wasm).await.unwrap(); + // Dmp / Hrmp cleared for the para + let para_id = ParaId(1000); + let para_key = format!( + "{}{}", + array_bytes::bytes2hex("", subhasher::twox64(para_id.encode())), + array_bytes::bytes2hex("", para_id.encode()) + ); + assert_eq!( + overrides[format!( + "{}{para_key}", + storage_key("Hrmp", "HrmpIngressChannelsIndex") + )], + "00" + ); + } - let mut injects = json!({}); - inject_authorized_upgrade(&mut injects, wasm_path).await; + #[test] + fn keep_messaging_state_leaves_channels_alone() { + let validator_keys = get_validator_keys(2); + let para = crate::config::Parachain::new("asset-hub"); + let paras = vec![¶]; + let rc = Relaychain::new("polkadot"); - let key = array_bytes::bytes2hex( - "", - substorager::storage_value_key(&b"System"[..], b"AuthorizedUpgrade"), + let mut set = OverrideSet::new(None); + generate_rc_overrides(&mut set, &validator_keys); + augment_overrides_for_paras(&mut set, &rc, ¶s, &CoresOverride::new(), true); + + let keys: Vec<&String> = set + .overrides + .as_object() + .unwrap() + .keys() + .filter(|k| { + k.starts_with(&storage_key("Hrmp", "HrmpIngressChannelsIndex")) + || k.starts_with(&storage_key("Dmp", "DownwardMessageQueueHeads")) + }) + .collect(); + assert!( + keys.is_empty(), + "should not touch messaging state: {keys:?}" ); - let expected = format!("{}01", hex::encode(subhasher::blake2_256(&wasm[..]))); - assert_eq!(injects[key], json!(expected)); } #[test] - fn encode_dmq() { - let dmp_dmqh_prefix = array_bytes::bytes2hex( - "", - substorager::storage_value_key(&b"Dmp"[..], b"DownwardMessageQueueHeads"), - ); + fn cores_override_changes_assignment() { + let para = crate::config::Parachain::new("asset-hub"); + let rc = Relaychain::new("polkadot"); + let mut cores = CoresOverride::new(); + cores.insert(1000, 1); - println!("Dmp_DownwardMessageQueueHeads {dmp_dmqh_prefix}"); + 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); + set.overrides[&scheduler_key[2..]] + .as_str() + .unwrap() + .to_string() + }; + + // asset-hub defaults to three cores, the override brings it down to one + let default = assignment(&CoresOverride::new()); + let overridden = assignment(&cores); + assert!(default.starts_with("0c"), "expected 3 cores, got {default}"); + assert!( + overridden.starts_with("04"), + "expected 1 core, got {overridden}" + ); + } - let para_id = ParaId(1005); + #[tokio::test] + async fn inject_authorized_upgrade_seeds_hash_and_check_version() { + let wasm_path = "/tmp/zombie-bite-test-upgrade.wasm"; + let wasm = b"not-a-real-runtime"; + tokio::fs::write(wasm_path, wasm).await.unwrap(); - let para_twox64 = array_bytes::bytes2hex("", subhasher::twox64(para_id.encode())); - let para_hex = array_bytes::bytes2hex("", para_id.encode()); + let mut set = OverrideSet::new(None); + inject_authorized_upgrade(&mut set, wasm_path).await; - println!("Dmp_DownwardMessageQueueHeads_1005 {dmp_dmqh_prefix}{para_twox64}{para_hex}"); + let expected = format!("{}01", hex::encode(subhasher::blake2_256(&wasm[..]))); + assert_eq!( + set.injects[storage_key("System", "AuthorizedUpgrade")], + json!(expected) + ); } - #[test] - fn encode_two_128() { - let name = array_bytes::bytes2hex("", subhasher::twox128(b":UsePreviousValidators:")); - println!(":UsePreviousValidators: : {name}"); + /// Runtime check stub: `present` lists the items the runtime has, and any + /// value equal to `bad_value` fails verification. + struct FakeRuntime { + present: Vec<(&'static str, &'static str)>, + bad_value: &'static str, } - #[test] - fn booleans() { - let t = true.encode(); - let f = false.encode(); - println!("true: {}", array_bytes::bytes2hex("", t)); - println!("false: {}", array_bytes::bytes2hex("", f)); + impl RuntimeCheck for FakeRuntime { + fn has_item(&self, pallet: &str, item: &str) -> bool { + self.present.iter().any(|(p, i)| *p == pallet && *i == item) + } + + fn verify_value( + &self, + pallet: &str, + item: &str, + value_hex: &str, + ) -> Result<(), anyhow::Error> { + if value_hex == self.bad_value { + anyhow::bail!("{pallet}::{item}: bad value"); + } + Ok(()) + } } + #[test] - fn encode_hrmp() { - let hrmp_prefix = array_bytes::bytes2hex( - "", - substorager::storage_value_key(&b"Hrmp"[..], b"HrmpIngressChannelsIndex"), - ); - println!("Hrmp_HrmpIngressChannelsIndex {hrmp_prefix}"); + fn missing_item_is_skipped_but_required_one_is_an_error() { + let runtime = FakeRuntime { + present: vec![("Session", "Validators")], + bad_value: "", + }; + + let mut set = OverrideSet::new(Some(&runtime)); + set.set("Session", "Validators", "04ff"); + // absent from the runtime: dropped quietly + set.set("ValidatorSet", "Validators", "04ff"); + // absent but explicitly requested: must fail the bite + set.inject_required("System", "AuthorizedUpgrade", "04ff"); + + assert_eq!(set.overrides[storage_key("Session", "Validators")], "04ff"); + assert_eq!(set.skipped, vec!["ValidatorSet::Validators"]); + let err = set.finish("test").unwrap_err().to_string(); + assert!(err.contains("System::AuthorizedUpgrade"), "got: {err}"); } #[test] - fn core_descriptor() { - let prefix = array_bytes::bytes2hex( - "", - substorager::storage_value_key(&b"CoretimeAssignmentProvider"[..], b"CoreDescriptors"), - ); - println!("p {prefix}"); - let core_0_descriptor_idx_key = format!( - "{prefix}{}", - array_bytes::bytes2hex("", subhasher::twox256(0_u32.to_le_bytes())) - ); - println!("core: {core_0_descriptor_idx_key}"); + fn value_that_fails_verification_fails_the_bite() { + let runtime = FakeRuntime { + present: vec![("Session", "Validators")], + bad_value: "deadbeef", + }; + + let mut set = OverrideSet::new(Some(&runtime)); + set.set("Session", "Validators", "deadbeef"); + + assert!(set.overrides.as_object().unwrap().is_empty()); + let err = set.finish("test").unwrap_err().to_string(); + assert!(err.contains("bad value"), "got: {err}"); } #[test] - fn create_cores_desc() { - let para_id = ParaId(1000); - let para_hex = array_bytes::bytes2hex("", para_id.encode()); - let prefix = array_bytes::bytes2hex( - "", - substorager::storage_value_key(&b"CoretimeAssignmentProvider"[..], b"CoreDescriptors"), - ); - for core in 0..3 { - let core_descriptor_idx_key = format!( - "{prefix}{}", - array_bytes::bytes2hex("", subhasher::twox256((core as u32).to_le_bytes())) - ); - let core_descriptor_value = format!("00010402{}00e100e100010000e1", para_hex); - - println!("\"0x{core_descriptor_idx_key}: 0x{core_descriptor_value}\""); - } + fn unverified_map_inject_still_requires_the_item() { + let runtime = FakeRuntime { + present: vec![], + bad_value: "", + }; + + let mut set = OverrideSet::new(Some(&runtime)); + // a huge wasm blob is not verified, but the item must exist + set.inject_map_unverified("Paras", "CodeByHash", "aa", "00ff"); + + let err = set.finish("test").unwrap_err().to_string(); + assert!(err.contains("Paras::CodeByHash"), "got: {err}"); } }