Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <para_id>=<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:
Expand Down
87 changes: 72 additions & 15 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use anyhow::{anyhow, bail};
use clap::{Parser, Subcommand};
use std::{
env,
Expand All @@ -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)]
Expand Down Expand Up @@ -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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mordamax did you test this with polkadot/kusama?

/// Override the cores assigned to a parachain, format: <para_id>=<cores>
/// Can be set multiple times, once per para.
#[arg(long = "para-cores", verbatim_doc_comment)]
para_cores: Vec<String>,
/// 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<u32>,
Expand Down Expand Up @@ -154,8 +166,8 @@ pub struct ResolvedBiteConfig {
pub parachains: Vec<Parachain>,
pub base_path: PathBuf,
pub and_spawn: bool,
pub upgrades: Upgrades,
pub apply_upgrade: bool,
pub opts: BiteOptions,
}

#[derive(Debug)]
Expand All @@ -178,6 +190,8 @@ pub fn resolve_bite_config(
relay_upgrade: Option<String>,
para_upgrade: Vec<String>,
apply_upgrade: bool,
keep_messaging_state: bool,
para_cores: Vec<String>,
) -> Result<ResolvedBiteConfig, anyhow::Error> {
// Load config file if provided
let config_file = if let Some(path) = config_path {
Expand Down Expand Up @@ -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 &para_upgrade {
let (id, path) = entry.split_once('=').ok_or_else(|| {
anyhow!("--para-upgrade must be <para_id>=<wasm_path>, 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 <para_id>=<wasm_path>, 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() {
Expand All @@ -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 &para_cores {
let (id, c) = entry
.split_once('=')
.ok_or_else(|| anyhow!("--para-cores must be <para_id>=<cores>, 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,
},
})
}

Expand Down
46 changes: 45 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u32, u32>;

/// 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(&para.id()) {
return *cores;
}
match para {
Parachain::AssetHub { .. } => 3,
Parachain::People { .. } => match relay {
Expand Down Expand Up @@ -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<String> {
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()),
Expand Down Expand Up @@ -680,6 +717,11 @@ pub struct ZombieBiteConfig {
pub and_spawn: Option<bool>,
pub with_monitor: Option<bool>,
pub apply_upgrade: Option<bool>,
/// 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<bool>,
}

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1228,6 +1271,7 @@ mod test {
and_spawn: None,
with_monitor: None,
apply_upgrade: None,
keep_messaging_state: None,
};

let parachains = config.get_parachains();
Expand Down
48 changes: 38 additions & 10 deletions src/doppelganger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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};

Expand All @@ -64,7 +65,7 @@ pub async fn doppelganger_inner(
relay_chain: Relaychain,
paras_to: Vec<Parachain>,
database: &str,
upgrades: &Upgrades,
opts: &BiteOptions,
) -> Result<(), anyhow::Error> {
// Star the node and wait until finish (with temp dir managed by us)
info!(
Expand Down Expand Up @@ -93,13 +94,31 @@ pub async fn doppelganger_inner(
// Parachain sync
let mut syncs = vec![];
for para in &paras_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(&para.id()).map(String::as_str),
opts.upgrades.paras.get(&para.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() {
Expand Down Expand Up @@ -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,
&paras_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

Expand Down Expand Up @@ -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 &paras_to {
if let Some(upgrade_wasm) = upgrades.paras.get(&para.id()) {
if let Some(upgrade_wasm) = opts.upgrades.paras.get(&para.id()) {
let blob_name = format!(
"{}-upgrade.wasm",
para.as_chain_string(&relay_chain.as_chain_string())
Expand Down
9 changes: 7 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use zombienet_sdk::{LocalFileSystem, Network, NetworkNode};
mod cli;
mod config;
mod doppelganger;
mod metadata;
mod monit;
mod overrides;
mod sync;
Expand Down Expand Up @@ -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");
Expand All @@ -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)");
}

Expand All @@ -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");
Expand Down
Loading
Loading