From 66ff1c0029c7136bf5b104f7a31ddc8fc4b0e265 Mon Sep 17 00:00:00 2001 From: Karol Kokoszka Date: Thu, 7 May 2026 11:14:02 +0200 Subject: [PATCH 1/2] Adds support for Paseo Bulletin chain --- src/cli.rs | 13 ++++++--- src/config.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++---- src/overrides.rs | 66 ++++++++++++++++++++++++++++++++++++--------- src/sync.rs | 26 ++++++++++++------ 4 files changed, 145 insertions(+), 30 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index a73c873..58167ad 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -35,7 +35,7 @@ pub enum Commands { /// 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, - /// Parachains to include: asset-hub, coretime, people, bridge-hub, collectives (comma-separated) + /// Parachains to include: asset-hub, coretime, people, bridge-hub, collectives, bulletin (comma-separated) #[arg(long, short = 'p', value_delimiter = ',', verbatim_doc_comment)] parachains: Option>, /// Base path to use. if not provided we will check the env 'ZOMBIE_BITE_BASE_PATH' and if not present we will use `_timestamp` @@ -74,7 +74,7 @@ pub enum Commands { /// [Helper] Generate artifacts to be used by the next step (only 'spawn' and 'post' allowed) GenerateArtifacts { /// The network will be using for bite (will try the network + ah) - #[arg(short = 'r', long = "rc", value_parser = clap::builder::PossibleValuesParser::new(["polkadot", "kusame", "paseo"]), default_value="polkadot")] + #[arg(short = 'r', long = "rc", value_parser = clap::builder::PossibleValuesParser::new(["polkadot", "kusama", "paseo"]), default_value="polkadot")] relay: String, /// Base path to use. if not provided we will check the env 'ZOMBIE_BITE_BASE_PATH' and if not present we will use `_timestamp` #[arg(long, short = 'd', verbatim_doc_comment)] @@ -86,7 +86,7 @@ pub enum Commands { /// [Helper] Clean up directory to only include the needed artifacts CleanUpDir { /// The network will be using for bite (will try the network + ah) - #[arg(short = 'r', long = "rc", value_parser = clap::builder::PossibleValuesParser::new(["polkadot", "kusame", "paseo"]), default_value="polkadot")] + #[arg(short = 'r', long = "rc", value_parser = clap::builder::PossibleValuesParser::new(["polkadot", "kusama", "paseo"]), default_value="polkadot")] relay: String, /// Base path to use. if not provided we will check the env 'ZOMBIE_BITE_BASE_PATH' and if not present we will use `_timestamp` #[arg(long, short = 'd', verbatim_doc_comment)] @@ -215,10 +215,15 @@ pub fn resolve_bite_config( maybe_bite_at: None, maybe_rpc_endpoint: None, }), + "bulletin" => Some(Parachain::Bulletin { + maybe_override: None, + maybe_bite_at: None, + maybe_rpc_endpoint: None, + }), unknown => { warn!( "⚠️ Warning: Unknown parachain '{}' will be ignored. - Valid options are: asset-hub, coretime, people, bridge-hub, collectives", + Valid options are: asset-hub, coretime, people, bridge-hub, collectives, bulletin", unknown ); None diff --git a/src/config.rs b/src/config.rs index d43534f..c6ddb46 100644 --- a/src/config.rs +++ b/src/config.rs @@ -214,7 +214,7 @@ impl Relaychain { 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::Paseo { .. } => "wss://paseo-rpc.n.dwellir.com", }) } @@ -222,7 +222,7 @@ impl Relaychain { 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::Paseo { .. } => "wss://paseo-rpc.n.dwellir.com", }) } @@ -282,6 +282,11 @@ pub enum Parachain { maybe_bite_at: MaybeByteAt, maybe_rpc_endpoint: MaybeSyncUrl, }, + Bulletin { + maybe_override: MaybeWasmOverridePath, + maybe_bite_at: MaybeByteAt, + maybe_rpc_endpoint: MaybeSyncUrl, + }, } impl Parachain { @@ -302,6 +307,11 @@ impl Parachain { maybe_bite_at: None, maybe_rpc_endpoint: None, }, + "bulletin" => Parachain::Bulletin { + maybe_override: None, + maybe_bite_at: None, + maybe_rpc_endpoint: None, + }, _ => Parachain::AssetHub { maybe_override: None, maybe_bite_at: None, @@ -317,6 +327,7 @@ impl Parachain { Parachain::People { .. } => "people", Parachain::BridgeHub { .. } => "bridge-hub", Parachain::Collectives { .. } => "collectives", + Parachain::Bulletin { .. } => "bulletin", }; format!("{para_part}-{relay_part}-local") @@ -329,6 +340,7 @@ impl Parachain { Parachain::People { .. } => "people", Parachain::BridgeHub { .. } => "bridge-hub", Parachain::Collectives { .. } => "collectives", + Parachain::Bulletin { .. } => "bulletin", }; format!("{para_part}-{relay_part}") @@ -345,6 +357,7 @@ impl Parachain { Parachain::People { .. } => 1004, Parachain::BridgeHub { .. } => 1002, Parachain::Collectives { .. } => 1001, + Parachain::Bulletin { .. } => 5118, } } @@ -354,7 +367,8 @@ impl Parachain { | Parachain::Coretime { maybe_override, .. } | Parachain::People { maybe_override, .. } | Parachain::BridgeHub { maybe_override, .. } - | Parachain::Collectives { maybe_override, .. } => maybe_override.as_deref(), + | Parachain::Collectives { maybe_override, .. } + | Parachain::Bulletin { maybe_override, .. } => maybe_override.as_deref(), } } @@ -364,7 +378,8 @@ impl Parachain { | Parachain::Coretime { maybe_bite_at, .. } | Parachain::People { maybe_bite_at, .. } | Parachain::BridgeHub { maybe_bite_at, .. } - | Parachain::Collectives { maybe_bite_at, .. } => *maybe_bite_at, + | Parachain::Collectives { maybe_bite_at, .. } + | Parachain::Bulletin { maybe_bite_at, .. } => *maybe_bite_at, } } @@ -384,6 +399,9 @@ impl Parachain { } | Parachain::Collectives { maybe_rpc_endpoint, .. + } + | Parachain::Bulletin { + maybe_rpc_endpoint, .. } => maybe_rpc_endpoint.as_deref(), } } @@ -467,6 +485,7 @@ pub fn generate_network_config( Parachain::People { .. } => ("people", para.id()), Parachain::BridgeHub { .. } => ("bridge-hub", para.id()), Parachain::Collectives { .. } => ("collectives", para.id()), + Parachain::Bulletin { .. } => ("bulletin", para.id()), }; let chain = format!("{}-{}",chain_part, relay_chain); @@ -559,6 +578,11 @@ impl ParachainConfig { maybe_bite_at: self.bite_at, maybe_rpc_endpoint: self.rpc_endpoint.clone(), }), + "bulletin" => Some(Parachain::Bulletin { + maybe_override: self.runtime_override.clone(), + maybe_bite_at: self.bite_at, + maybe_rpc_endpoint: self.rpc_endpoint.clone(), + }), _ => None, } } else { @@ -710,7 +734,14 @@ mod test { #[test] fn all_parachain_types_supported() { - let types = vec!["asset-hub", "coretime", "people", "bridge-hub"]; + let types = vec![ + "asset-hub", + "coretime", + "people", + "bridge-hub", + "collectives", + "bulletin", + ]; for parachain_type in types { let config = ParachainConfig { @@ -767,6 +798,15 @@ mod test { .id(), 1002 ); + assert_eq!( + Parachain::Bulletin { + maybe_override: None, + maybe_bite_at: None, + maybe_rpc_endpoint: None + } + .id(), + 5118 + ); } #[test] @@ -809,6 +849,16 @@ mod test { .as_chain_string(relay), "bridge-hub-polkadot" ); + let paseo = "paseo"; + assert_eq!( + Parachain::Bulletin { + maybe_override: None, + maybe_bite_at: None, + maybe_rpc_endpoint: None + } + .as_chain_string(paseo), + "bulletin-paseo" + ); } #[test] @@ -851,6 +901,16 @@ mod test { .as_local_chain_string(relay), "bridge-hub-kusama-local" ); + let paseo = "paseo"; + assert_eq!( + Parachain::Bulletin { + maybe_override: None, + maybe_bite_at: None, + maybe_rpc_endpoint: None + } + .as_local_chain_string(paseo), + "bulletin-paseo-local" + ); } #[test] diff --git a/src/overrides.rs b/src/overrides.rs index 018244a..1da53e5 100644 --- a/src/overrides.rs +++ b/src/overrides.rs @@ -148,12 +148,7 @@ pub fn generate_rc_overrides( "", substorager::storage_value_key(&b"Hrmp"[..], b"HrmpIngressChannelsIndex"), ); - let core_descriptor_prefix = array_bytes::bytes2hex( - "", - substorager::storage_value_key(&b"CoretimeAssignmentProvider"[..], b"CoreDescriptors"), - ); - for (index, para) in paras.iter().enumerate() { - let index: u32 = index.try_into().expect("Index should be valid u32"); + for para in paras.iter() { let para_id = ParaId(para.id()); let para_twox64 = array_bytes::bytes2hex("", subhasher::twox64(para_id.encode())); @@ -168,15 +163,47 @@ pub fn generate_rc_overrides( // HRMP hrmpIngressChannelsIndex (empty for each para) let hrmp_channels_key = format!("{hrmp_hici_prefix}{para_key_part}"); overrides[hrmp_channels_key] = json!("00"); + } - // CoretimeAssignmentProvider CoreDescriptors - let core_descriptor_idx_key = format!( - "{core_descriptor_prefix}{}", - array_bytes::bytes2hex("", subhasher::twox256(index.to_le_bytes())) - ); - let core_descriptor = format!("00010402{}00e100e100010000e1", para_hex); - overrides[core_descriptor_idx_key] = json!(core_descriptor); + // ParaScheduler::CoreDescriptors is a StorageValue>. + // The bitten state has an entry for every para registered on the live relay (50+ on paseo), + // but `Configuration::activeConfig.num_cores` is overridden above to `paras.len()` cores. + // We rewrite the value to one (core_index, Task(para_id, ...)) pair per para in our list, + // so each kept para gets its own dedicated core. Without this, core 0 stays assigned to + // whichever live para sat in the first slot of the bitten state, and our paras never + // get backable-candidate requests. + // + // SCALE layout per (CoreIndex, CoreDescriptor) entry, matching live paseo state: + // [4 bytes core_index_u32_le][CoreDescriptor] + // CoreDescriptor for an active Task assignment to ParaId X: + // 00 queue: None + // 01 current_work: Some + // 04 assignments: Vec compact-len 1 + // 02 CoreAssignment::Task variant + // <4 bytes LE> ParaId + // 00e100e100010000e1 remaining WorkState bytes (mask + assignment state + pos/step) + let core_descriptors_key = array_bytes::bytes2hex( + "", + substorager::storage_value_key(&b"ParaScheduler"[..], b"CoreDescriptors"), + ); + let num_paras: u32 = paras + .len() + .try_into() + .expect("The number of paras needs to fit into a u32."); + // Compact-encode the vec length; we only support up to 63 paras here (single-byte form). + assert!( + num_paras < 64, + "more than 63 parachains is not supported by the CoreDescriptors override" + ); + let mut core_descriptors_value = format!("{:02x}", (num_paras as u8) << 2); + for (idx, para) in paras.iter().enumerate() { + let core_idx: u32 = idx.try_into().expect("Index should fit in u32"); + let para_hex = array_bytes::bytes2hex("", ParaId(para.id()).encode()); + let core_idx_hex = array_bytes::bytes2hex("", core_idx.to_le_bytes()); + core_descriptors_value + .push_str(&format!("{core_idx_hex}00010402{para_hex}00e100e100010000e1")); } + overrides[core_descriptors_key] = json!(core_descriptors_value); overrides } @@ -311,6 +338,19 @@ pub async fn generate_default_overrides_for_para( "15464cac3378d46f113cd5b7a4d71c84476f594316a7dfe49c1f352d95abdaf1": "01000000" }); + // Bulletin's pallet-bulletin-transaction-storage::on_finalize asserts that a per-block + // `apply_block_inherents` storage-proof inherent ran (via `ProofChecked`), or that the + // target block (n - RetentionPeriod) is zero / has no stored transactions. The generic + // doppelganger-parachain (omni-node) doesn't supply that inherent, so we sidestep the + // assert by overriding `RetentionPeriod` to u32::MAX — `n.saturating_sub(period)` then + // saturates to 0, the `is_zero()` branch short-circuits, and `on_finalize` passes. + // Storage key = twox128("TransactionStorage") + twox128("RetentionPeriod"); value is + // SCALE-encoded u32 LE (0xffffffff). + if let Parachain::Bulletin { .. } = para { + overrides["0e7b504e5df47062be129a8958a7a1278d69b77f53c8c31f3b84d472fdb7de2b"] = + json!("ffffffff"); + } + if let Some(override_wasm) = para.wasm_overrides() { let wasm_content = fs::read(override_wasm) .await diff --git a/src/sync.rs b/src/sync.rs index 8779839..61b71d0 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -18,6 +18,16 @@ use zombienet_support::net::wait_ws_ready; const PASEO_ASSET_HUB_SPEC_URL: &str = "https://paseo-r2.zondax.ch/chain-specs/paseo-asset-hub.json"; +const BULLETIN_PASEO_SPEC_URL: &str = + "https://raw.githubusercontent.com/paritytech/chainspecs/main/paseo/parachain/bulletin/raw.json"; + +fn maybe_hosted_spec_url(chain: &str) -> Option<&'static str> { + match chain { + "asset-hub-paseo" => Some(PASEO_ASSET_HUB_SPEC_URL), + "bulletin-paseo" => Some(BULLETIN_PASEO_SPEC_URL), + _ => None, + } +} #[allow(clippy::too_many_arguments)] pub async fn sync_relay_only( @@ -139,17 +149,17 @@ pub async fn sync_para( trace!("env: {env:?}"); - let dest_for_paseo = format!("{}/asset-hub-paseo.json", ns.base_dir().to_string_lossy(),); - let chain_arg = if chain == "asset-hub-paseo" { - // get chain spec from https://paseo-r2.zondax.ch/chain-specs/paseo-asset-hub.json - let response = reqwest::get(PASEO_ASSET_HUB_SPEC_URL) + let hosted_spec_dest = format!("{}/{}.json", ns.base_dir().to_string_lossy(), &chain); + let chain_arg = if let Some(url) = maybe_hosted_spec_url(chain.as_str()) { + info!("📥 Downloading chain spec for {chain} from {url}"); + let response = reqwest::get(url) .await - .unwrap_or_else(|_| panic!("Create file {dest_for_paseo} should work")); - let mut file = std::fs::File::create(&dest_for_paseo) - .unwrap_or_else(|_| panic!("Create file {dest_for_paseo} should work")); + .unwrap_or_else(|_| panic!("Fetch chain spec from {url} should work")); + let mut file = std::fs::File::create(&hosted_spec_dest) + .unwrap_or_else(|_| panic!("Create file {hosted_spec_dest} should work")); let mut content = Cursor::new(response.bytes().await.expect("Create cursor should works.")); std::io::copy(&mut content, &mut file).expect("Copy bytes should works."); - dest_for_paseo.as_str() + hosted_spec_dest.as_str() } else { chain.as_ref() }; From 20c19e5f76dbc5822cae97b689a05043486d3da7 Mon Sep 17 00:00:00 2001 From: Karol Kokoszka Date: Thu, 7 May 2026 11:30:01 +0200 Subject: [PATCH 2/2] Formatting --- src/overrides.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/overrides.rs b/src/overrides.rs index 1da53e5..ba0155c 100644 --- a/src/overrides.rs +++ b/src/overrides.rs @@ -200,8 +200,9 @@ pub fn generate_rc_overrides( let core_idx: u32 = idx.try_into().expect("Index should fit in u32"); let para_hex = array_bytes::bytes2hex("", ParaId(para.id()).encode()); let core_idx_hex = array_bytes::bytes2hex("", core_idx.to_le_bytes()); - core_descriptors_value - .push_str(&format!("{core_idx_hex}00010402{para_hex}00e100e100010000e1")); + core_descriptors_value.push_str(&format!( + "{core_idx_hex}00010402{para_hex}00e100e100010000e1" + )); } overrides[core_descriptors_key] = json!(core_descriptors_value);