feat: Vendor pezkuwi-subxt and pezkuwi-zombienet-sdk into monorepo

- Add pezkuwi-subxt crates to vendor/pezkuwi-subxt
- Add pezkuwi-zombienet-sdk crates to vendor/pezkuwi-zombienet-sdk
- Convert git dependencies to path dependencies
- Add vendor crates to workspace members
- Remove test/example crates from vendor (not needed for SDK)
- Fix feature propagation issues detected by zepter
- Fix workspace inheritance for internal dependencies
- All 606 crates now in workspace
- All 6919 internal dependency links verified correct
- No git dependencies remaining
This commit is contained in:
2025-12-22 23:31:24 +03:00
parent 4c8f281051
commit 70ddb6516f
386 changed files with 76759 additions and 36 deletions
@@ -0,0 +1,31 @@
//! Zombienet Orchestrator error definitions.
use provider::ProviderError;
use support::fs::FileSystemError;
use crate::generators;
#[derive(Debug, thiserror::Error)]
pub enum OrchestratorError {
// TODO: improve invalid config reporting
#[error("Invalid network configuration: {0}")]
InvalidConfig(String),
#[error("Invalid network config to use provider {0}: {1}")]
InvalidConfigForProvider(String, String),
#[error("Invalid configuration for node: {0}, field: {1}")]
InvalidNodeConfig(String, String),
#[error("Invariant not fulfilled {0}")]
InvariantError(&'static str),
#[error("Global network spawn timeout: {0} secs")]
GlobalTimeOut(u32),
#[error("Generator error: {0}")]
GeneratorError(#[from] generators::errors::GeneratorError),
#[error("Provider error")]
ProviderError(#[from] ProviderError),
#[error("FileSystem error")]
FileSystemError(#[from] FileSystemError),
#[error("Serialization error")]
SerializationError(#[from] serde_json::Error),
#[error(transparent)]
SpawnerError(#[from] anyhow::Error),
}
@@ -0,0 +1,22 @@
pub mod chain_spec;
pub mod errors;
pub mod key;
pub mod para_artifact;
mod arg_filter;
mod bootnode_addr;
mod command;
mod identity;
mod keystore;
mod keystore_key_types;
mod port;
pub use bootnode_addr::generate as generate_node_bootnode_addr;
pub use command::{
generate_for_cumulus_node as generate_node_command_cumulus,
generate_for_node as generate_node_command, GenCmdOptions,
};
pub use identity::generate as generate_node_identity;
pub use key::generate as generate_node_keys;
pub use keystore::generate as generate_node_keystore;
pub use port::generate as generate_node_port;
@@ -0,0 +1,138 @@
use configuration::types::Arg;
/// Parse args to extract those marked for removal (with `-:` prefix).
/// Returns a set of arg names/flags that should be removed from the final command.
///
/// # Examples
/// - `-:--insecure-validator-i-know-what-i-do` -> removes `--insecure-validator-i-know-what-i-do`
/// - `-:insecure-validator` -> removes `--insecure-validator` (normalized)
/// - `-:--prometheus-port` -> removes `--prometheus-port`
pub fn parse_removal_args(args: &[Arg]) -> Vec<String> {
args.iter()
.filter_map(|arg| match arg {
Arg::Flag(flag) if flag.starts_with("-:") => {
let mut flag_to_exclude = flag[2..].to_string();
// Normalize flag format - ensure it starts with --
if !flag_to_exclude.starts_with("--") {
flag_to_exclude = format!("--{flag_to_exclude}");
}
Some(flag_to_exclude)
},
_ => None,
})
.collect()
}
/// Apply arg removals to a vector of string arguments.
/// This filters out any args that match the removal list.
///
/// # Arguments
/// * `args` - The command arguments to filter
/// * `removals` - List of arg names/flags to remove
///
/// # Returns
/// Filtered vector with specified args removed
pub fn apply_arg_removals(args: Vec<String>, removals: &[String]) -> Vec<String> {
if removals.is_empty() {
return args;
}
let mut res = Vec::new();
let mut skip_next = false;
for (i, arg) in args.iter().enumerate() {
if skip_next {
skip_next = false;
continue;
}
let should_remove = removals
.iter()
.any(|removal| arg == removal || arg.starts_with(&format!("{removal}=")));
if should_remove {
// Only skip next if this looks like an option (starts with --) and next arg doesn't start with --
if !arg.contains("=") && i + 1 < args.len() {
let next_arg = &args[i + 1];
if !next_arg.starts_with("-") {
skip_next = true;
}
}
continue;
}
res.push(arg.clone());
}
res
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_removal_args() {
let args = vec![
Arg::Flag("-:--insecure-validator-i-know-what-i-do".to_string()),
Arg::Flag("--validator".to_string()),
Arg::Flag("-:--no-telemetry".to_string()),
];
let removals = parse_removal_args(&args);
assert_eq!(removals.len(), 2);
assert!(removals.contains(&"--insecure-validator-i-know-what-i-do".to_string()));
assert!(removals.contains(&"--no-telemetry".to_string()));
}
#[test]
fn test_apply_arg_removals_flag() {
let args = vec![
"--validator".to_string(),
"--insecure-validator-i-know-what-i-do".to_string(),
"--no-telemetry".to_string(),
];
let removals = vec!["--insecure-validator-i-know-what-i-do".to_string()];
let res = apply_arg_removals(args, &removals);
assert_eq!(res.len(), 2);
assert!(res.contains(&"--validator".to_string()));
assert!(res.contains(&"--no-telemetry".to_string()));
assert!(!res.contains(&"--insecure-validator-i-know-what-i-do".to_string()));
}
#[test]
fn test_apply_arg_removals_option_with_equals() {
let args = vec!["--name=alice".to_string(), "--port=30333".to_string()];
let removals = vec!["--port".to_string()];
let res = apply_arg_removals(args, &removals);
assert_eq!(res.len(), 1);
assert_eq!(res[0], "--name=alice");
}
#[test]
fn test_apply_arg_removals_option_with_space() {
let args = vec![
"--name".to_string(),
"alice".to_string(),
"--port".to_string(),
"30333".to_string(),
];
let removals = vec!["--port".to_string()];
let res = apply_arg_removals(args, &removals);
assert_eq!(res.len(), 2);
assert_eq!(res[0], "--name");
assert_eq!(res[1], "alice");
}
#[test]
fn test_apply_arg_removals_empty() {
let args = vec!["--validator".to_string()];
let removals = vec![];
let res = apply_arg_removals(args, &removals);
assert_eq!(res, vec!["--validator".to_string()]);
}
}
@@ -0,0 +1,111 @@
use std::{fmt::Display, net::IpAddr};
use super::errors::GeneratorError;
pub fn generate<T: AsRef<str> + Display>(
peer_id: &str,
ip: &IpAddr,
port: u16,
args: &[T],
p2p_cert: &Option<String>,
) -> Result<String, GeneratorError> {
let addr = if let Some(index) = args.iter().position(|arg| arg.as_ref().eq("--listen-addr")) {
let listen_value = args
.as_ref()
.get(index + 1)
.ok_or(GeneratorError::BootnodeAddrGeneration(
"can not generate bootnode address from args".into(),
))?
.to_string();
let ip_str = ip.to_string();
let port_str = port.to_string();
let mut parts = listen_value.split('/').collect::<Vec<&str>>();
parts[2] = &ip_str;
parts[4] = port_str.as_str();
parts.join("/")
} else {
format!("/ip4/{ip}/tcp/{port}/ws")
};
let mut addr_with_peer = format!("{addr}/p2p/{peer_id}");
if let Some(p2p_cert) = p2p_cert {
addr_with_peer.push_str("/certhash/");
addr_with_peer.push_str(p2p_cert)
}
Ok(addr_with_peer)
}
#[cfg(test)]
mod tests {
use provider::constants::LOCALHOST;
use super::*;
#[test]
fn generate_for_alice_without_args() {
let peer_id = "12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm"; // from alice as seed
let args: Vec<&str> = vec![];
let bootnode_addr = generate(peer_id, &LOCALHOST, 5678, &args, &None).unwrap();
assert_eq!(
&bootnode_addr,
"/ip4/127.0.0.1/tcp/5678/ws/p2p/12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm"
);
}
#[test]
fn generate_for_alice_with_listen_addr() {
// Should override the ip/port
let peer_id = "12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm"; // from alice as seed
let args: Vec<String> = [
"--some",
"other",
"--listen-addr",
"/ip4/192.168.100.1/tcp/30333/ws",
]
.iter()
.map(|x| x.to_string())
.collect();
let bootnode_addr =
generate(peer_id, &LOCALHOST, 5678, args.iter().as_ref(), &None).unwrap();
assert_eq!(
&bootnode_addr,
"/ip4/127.0.0.1/tcp/5678/ws/p2p/12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm"
);
}
#[test]
fn generate_for_alice_with_listen_addr_without_value_must_fail() {
// Should override the ip/port
let peer_id = "12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm"; // from alice as seed
let args: Vec<String> = ["--some", "other", "--listen-addr"]
.iter()
.map(|x| x.to_string())
.collect();
let bootnode_addr = generate(peer_id, &LOCALHOST, 5678, args.iter().as_ref(), &None);
assert!(bootnode_addr.is_err());
assert!(matches!(
bootnode_addr,
Err(GeneratorError::BootnodeAddrGeneration(_))
));
}
#[test]
fn generate_for_alice_withcert() {
let peer_id = "12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm"; // from alice as seed
let args: Vec<&str> = vec![];
let bootnode_addr = generate(
peer_id,
&LOCALHOST,
5678,
&args,
&Some(String::from("data")),
)
.unwrap();
assert_eq!(
&bootnode_addr,
"/ip4/127.0.0.1/tcp/5678/ws/p2p/12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm/certhash/data"
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,634 @@
use configuration::types::Arg;
use support::constants::THIS_IS_A_BUG;
use super::arg_filter::{apply_arg_removals, parse_removal_args};
use crate::{network_spec::node::NodeSpec, shared::constants::*};
pub struct GenCmdOptions<'a> {
pub relay_chain_name: &'a str,
pub cfg_path: &'a str,
pub data_path: &'a str,
pub relay_data_path: &'a str,
pub use_wrapper: bool,
pub bootnode_addr: Vec<String>,
pub use_default_ports_in_cmd: bool,
pub is_native: bool,
}
impl Default for GenCmdOptions<'_> {
fn default() -> Self {
Self {
relay_chain_name: "rococo-local",
cfg_path: "/cfg",
data_path: "/data",
relay_data_path: "/relay-data",
use_wrapper: true,
bootnode_addr: vec![],
use_default_ports_in_cmd: false,
is_native: true,
}
}
}
const FLAGS_ADDED_BY_US: [&str; 3] = ["--no-telemetry", "--collator", "--"];
const OPS_ADDED_BY_US: [&str; 6] = [
"--chain",
"--name",
"--rpc-cors",
"--rpc-methods",
"--parachain-id",
"--node-key",
];
// TODO: can we abstract this and use only one fn (or at least split and reuse in small fns)
pub fn generate_for_cumulus_node(
node: &NodeSpec,
options: GenCmdOptions,
para_id: u32,
) -> (String, Vec<String>) {
let NodeSpec {
key,
args,
is_validator,
bootnodes_addresses,
..
} = node;
let mut tmp_args: Vec<String> = vec!["--node-key".into(), key.clone()];
if !args.contains(&Arg::Flag("--prometheus-external".into())) {
tmp_args.push("--prometheus-external".into())
}
if *is_validator && !args.contains(&Arg::Flag("--validator".into())) {
tmp_args.push("--collator".into())
}
if !bootnodes_addresses.is_empty() {
tmp_args.push("--bootnodes".into());
let bootnodes = bootnodes_addresses
.iter()
.map(|m| m.to_string())
.collect::<Vec<String>>()
.join(" ");
tmp_args.push(bootnodes)
}
// ports
let (prometheus_port, rpc_port, p2p_port) =
resolve_ports(node, options.use_default_ports_in_cmd);
tmp_args.push("--prometheus-port".into());
tmp_args.push(prometheus_port.to_string());
tmp_args.push("--rpc-port".into());
tmp_args.push(rpc_port.to_string());
tmp_args.push("--listen-addr".into());
tmp_args.push(format!("/ip4/0.0.0.0/tcp/{p2p_port}/ws"));
let mut collator_args: &[Arg] = &[];
let mut full_node_args: &[Arg] = &[];
if !args.is_empty() {
if let Some(index) = args.iter().position(|arg| match arg {
Arg::Flag(flag) => flag.eq("--"),
Arg::Option(..) => false,
Arg::Array(..) => false,
}) {
(collator_args, full_node_args) = args.split_at(index);
} else {
// Assume args are those specified for collator only
collator_args = args;
}
}
// set our base path
tmp_args.push("--base-path".into());
tmp_args.push(options.data_path.into());
let node_specific_bootnodes: Vec<String> = node
.bootnodes_addresses
.iter()
.map(|b| b.to_string())
.collect();
let full_bootnodes = [node_specific_bootnodes, options.bootnode_addr].concat();
if !full_bootnodes.is_empty() {
tmp_args.push("--bootnodes".into());
tmp_args.push(full_bootnodes.join(" "));
}
let mut full_node_p2p_needs_to_be_injected = true;
let mut full_node_prometheus_needs_to_be_injected = true;
let mut full_node_args_filtered = full_node_args
.iter()
.filter_map(|arg| match arg {
Arg::Flag(flag) => {
if flag.starts_with("-:") || FLAGS_ADDED_BY_US.contains(&flag.as_str()) {
None
} else {
Some(vec![flag.to_owned()])
}
},
Arg::Option(k, v) => {
if OPS_ADDED_BY_US.contains(&k.as_str()) {
None
} else if k.eq(&"port") {
if v.eq(&"30333") {
full_node_p2p_needs_to_be_injected = true;
None
} else {
// non default
full_node_p2p_needs_to_be_injected = false;
Some(vec![k.to_owned(), v.to_owned()])
}
} else if k.eq(&"--prometheus-port") {
if v.eq(&"9616") {
full_node_prometheus_needs_to_be_injected = true;
None
} else {
// non default
full_node_prometheus_needs_to_be_injected = false;
Some(vec![k.to_owned(), v.to_owned()])
}
} else {
Some(vec![k.to_owned(), v.to_owned()])
}
},
Arg::Array(k, v) => {
let mut args = vec![k.to_owned()];
args.extend(v.to_owned());
Some(args)
},
})
.flatten()
.collect::<Vec<String>>();
let full_p2p_port = node
.full_node_p2p_port
.as_ref()
.expect(&format!(
"full node p2p_port should be specifed: {THIS_IS_A_BUG}"
))
.0;
let full_prometheus_port = node
.full_node_prometheus_port
.as_ref()
.expect(&format!(
"full node prometheus_port should be specifed: {THIS_IS_A_BUG}"
))
.0;
// full_node: change p2p port if is the default
if full_node_p2p_needs_to_be_injected {
full_node_args_filtered.push("--port".into());
full_node_args_filtered.push(full_p2p_port.to_string());
}
// full_node: change prometheus port if is the default
if full_node_prometheus_needs_to_be_injected {
full_node_args_filtered.push("--prometheus-port".into());
full_node_args_filtered.push(full_prometheus_port.to_string());
}
let mut args_filtered = collator_args
.iter()
.filter_map(|arg| match arg {
Arg::Flag(flag) => {
if flag.starts_with("-:") || FLAGS_ADDED_BY_US.contains(&flag.as_str()) {
None
} else {
Some(vec![flag.to_owned()])
}
},
Arg::Option(k, v) => {
if OPS_ADDED_BY_US.contains(&k.as_str()) {
None
} else {
Some(vec![k.to_owned(), v.to_owned()])
}
},
Arg::Array(k, v) => {
let mut args = vec![k.to_owned()];
args.extend(v.to_owned());
Some(args)
},
})
.flatten()
.collect::<Vec<String>>();
tmp_args.append(&mut args_filtered);
let parachain_spec_path = format!("{}/{}.json", options.cfg_path, para_id);
let mut final_args = vec![
node.command.as_str().to_string(),
"--chain".into(),
parachain_spec_path,
"--name".into(),
node.name.clone(),
"--rpc-cors".into(),
"all".into(),
"--rpc-methods".into(),
"unsafe".into(),
];
// The `--unsafe-rpc-external` option spawns an additional RPC server on a random port,
// which can conflict with reserved ports, causing an "Address already in use" error
// when using the `native` provider. Since this option isn't needed for `native`,
// it should be omitted in that case.
if !options.is_native {
final_args.push("--unsafe-rpc-external".into());
}
final_args.append(&mut tmp_args);
let relaychain_spec_path = format!("{}/{}.json", options.cfg_path, options.relay_chain_name);
let mut full_node_injected: Vec<String> = vec![
"--".into(),
"--base-path".into(),
options.relay_data_path.into(),
"--chain".into(),
relaychain_spec_path,
"--execution".into(),
"wasm".into(),
];
final_args.append(&mut full_node_injected);
final_args.append(&mut full_node_args_filtered);
let removals = parse_removal_args(args);
final_args = apply_arg_removals(final_args, &removals);
if options.use_wrapper {
("/cfg/zombie-wrapper.sh".to_string(), final_args)
} else {
(final_args.remove(0), final_args)
}
}
pub fn generate_for_node(
node: &NodeSpec,
options: GenCmdOptions,
para_id: Option<u32>,
) -> (String, Vec<String>) {
let NodeSpec {
key,
args,
is_validator,
bootnodes_addresses,
..
} = node;
let mut tmp_args: Vec<String> = vec![
"--node-key".into(),
key.clone(),
// TODO:(team) we should allow to set the telemetry url from config
"--no-telemetry".into(),
];
if !args.contains(&Arg::Flag("--prometheus-external".into())) {
tmp_args.push("--prometheus-external".into())
}
if let Some(para_id) = para_id {
tmp_args.push("--parachain-id".into());
tmp_args.push(para_id.to_string());
}
if *is_validator && !args.contains(&Arg::Flag("--validator".into())) {
tmp_args.push("--validator".into());
if node.supports_arg("--insecure-validator-i-know-what-i-do") {
tmp_args.push("--insecure-validator-i-know-what-i-do".into());
}
}
if !bootnodes_addresses.is_empty() {
tmp_args.push("--bootnodes".into());
let bootnodes = bootnodes_addresses
.iter()
.map(|m| m.to_string())
.collect::<Vec<String>>()
.join(" ");
tmp_args.push(bootnodes)
}
// ports
let (prometheus_port, rpc_port, p2p_port) =
resolve_ports(node, options.use_default_ports_in_cmd);
// Prometheus
tmp_args.push("--prometheus-port".into());
tmp_args.push(prometheus_port.to_string());
// RPC
// TODO (team): do we want to support old --ws-port?
tmp_args.push("--rpc-port".into());
tmp_args.push(rpc_port.to_string());
let listen_value = if let Some(listen_val) = args.iter().find_map(|arg| match arg {
Arg::Flag(_) => None,
Arg::Option(k, v) => {
if k.eq("--listen-addr") {
Some(v)
} else {
None
}
},
Arg::Array(..) => None,
}) {
let mut parts = listen_val.split('/').collect::<Vec<&str>>();
// TODO: move this to error
let port_part = parts
.get_mut(4)
.expect(&format!("should have at least 5 parts {THIS_IS_A_BUG}"));
let port_to_use = p2p_port.to_string();
*port_part = port_to_use.as_str();
parts.join("/")
} else {
format!("/ip4/0.0.0.0/tcp/{p2p_port}/ws")
};
tmp_args.push("--listen-addr".into());
tmp_args.push(listen_value);
// set our base path
tmp_args.push("--base-path".into());
tmp_args.push(options.data_path.into());
let node_specific_bootnodes: Vec<String> = node
.bootnodes_addresses
.iter()
.map(|b| b.to_string())
.collect();
let full_bootnodes = [node_specific_bootnodes, options.bootnode_addr].concat();
if !full_bootnodes.is_empty() {
tmp_args.push("--bootnodes".into());
tmp_args.push(full_bootnodes.join(" "));
}
// add the rest of the args
let mut args_filtered = args
.iter()
.filter_map(|arg| match arg {
Arg::Flag(flag) => {
if flag.starts_with("-:") || FLAGS_ADDED_BY_US.contains(&flag.as_str()) {
None
} else {
Some(vec![flag.to_owned()])
}
},
Arg::Option(k, v) => {
if OPS_ADDED_BY_US.contains(&k.as_str()) {
None
} else {
Some(vec![k.to_owned(), v.to_owned()])
}
},
Arg::Array(k, v) => {
let mut args = vec![k.to_owned()];
args.extend(v.to_owned());
Some(args)
},
})
.flatten()
.collect::<Vec<String>>();
tmp_args.append(&mut args_filtered);
let chain_spec_path = format!("{}/{}.json", options.cfg_path, options.relay_chain_name);
let mut final_args = vec![
node.command.as_str().to_string(),
"--chain".into(),
chain_spec_path,
"--name".into(),
node.name.clone(),
"--rpc-cors".into(),
"all".into(),
"--rpc-methods".into(),
"unsafe".into(),
];
// The `--unsafe-rpc-external` option spawns an additional RPC server on a random port,
// which can conflict with reserved ports, causing an "Address already in use" error
// when using the `native` provider. Since this option isn't needed for `native`,
// it should be omitted in that case.
if !options.is_native {
final_args.push("--unsafe-rpc-external".into());
}
final_args.append(&mut tmp_args);
if let Some(ref subcommand) = node.subcommand {
final_args.insert(1, subcommand.as_str().to_string());
}
let removals = parse_removal_args(args);
final_args = apply_arg_removals(final_args, &removals);
if options.use_wrapper {
("/cfg/zombie-wrapper.sh".to_string(), final_args)
} else {
(final_args.remove(0), final_args)
}
}
/// Returns (prometheus, rpc, p2p) ports to use in the command
fn resolve_ports(node: &NodeSpec, use_default_ports_in_cmd: bool) -> (u16, u16, u16) {
if use_default_ports_in_cmd {
(PROMETHEUS_PORT, RPC_PORT, P2P_PORT)
} else {
(node.prometheus_port.0, node.rpc_port.0, node.p2p_port.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{generators, shared::types::NodeAccounts};
fn get_node_spec(full_node_present: bool) -> NodeSpec {
let mut name = String::from("luca");
let initial_balance = 1_000_000_000_000_u128;
let seed = format!("//{}{name}", name.remove(0).to_uppercase());
let accounts = NodeAccounts {
accounts: generators::generate_node_keys(&seed).unwrap(),
seed,
};
let (full_node_p2p_port, full_node_prometheus_port) = if full_node_present {
(
Some(generators::generate_node_port(None).unwrap()),
Some(generators::generate_node_port(None).unwrap()),
)
} else {
(None, None)
};
NodeSpec {
name,
accounts,
initial_balance,
full_node_p2p_port,
full_node_prometheus_port,
..Default::default()
}
}
#[test]
fn generate_for_native_cumulus_node_works() {
let node = get_node_spec(true);
let opts = GenCmdOptions {
use_wrapper: false,
is_native: true,
..GenCmdOptions::default()
};
let (program, args) = generate_for_cumulus_node(&node, opts, 1000);
assert_eq!(program.as_str(), "polkadot");
let divider_flag = args.iter().position(|x| x == "--").unwrap();
// ensure full node ports
let i = args[divider_flag..]
.iter()
.position(|x| {
x == node
.full_node_p2p_port
.as_ref()
.unwrap()
.0
.to_string()
.as_str()
})
.unwrap();
assert_eq!(&args[divider_flag + i - 1], "--port");
let i = args[divider_flag..]
.iter()
.position(|x| {
x == node
.full_node_prometheus_port
.as_ref()
.unwrap()
.0
.to_string()
.as_str()
})
.unwrap();
assert_eq!(&args[divider_flag + i - 1], "--prometheus-port");
assert!(!args.iter().any(|arg| arg == "--unsafe-rpc-external"));
}
#[test]
fn generate_for_native_cumulus_node_rpc_external_is_not_removed_if_is_set_by_user() {
let mut node = get_node_spec(true);
node.args.push("--unsafe-rpc-external".into());
let opts = GenCmdOptions {
use_wrapper: false,
is_native: true,
..GenCmdOptions::default()
};
let (_, args) = generate_for_cumulus_node(&node, opts, 1000);
assert!(args.iter().any(|arg| arg == "--unsafe-rpc-external"));
}
#[test]
fn generate_for_non_native_cumulus_node_works() {
let node = get_node_spec(true);
let opts = GenCmdOptions {
use_wrapper: false,
is_native: false,
..GenCmdOptions::default()
};
let (program, args) = generate_for_cumulus_node(&node, opts, 1000);
assert_eq!(program.as_str(), "polkadot");
let divider_flag = args.iter().position(|x| x == "--").unwrap();
// ensure full node ports
let i = args[divider_flag..]
.iter()
.position(|x| {
x == node
.full_node_p2p_port
.as_ref()
.unwrap()
.0
.to_string()
.as_str()
})
.unwrap();
assert_eq!(&args[divider_flag + i - 1], "--port");
let i = args[divider_flag..]
.iter()
.position(|x| {
x == node
.full_node_prometheus_port
.as_ref()
.unwrap()
.0
.to_string()
.as_str()
})
.unwrap();
assert_eq!(&args[divider_flag + i - 1], "--prometheus-port");
// we expect to find this arg in collator node part
assert!(&args[0..divider_flag]
.iter()
.any(|arg| arg == "--unsafe-rpc-external"));
}
#[test]
fn generate_for_native_node_rpc_external_works() {
let node = get_node_spec(false);
let opts = GenCmdOptions {
use_wrapper: false,
is_native: true,
..GenCmdOptions::default()
};
let (program, args) = generate_for_node(&node, opts, Some(1000));
assert_eq!(program.as_str(), "polkadot");
assert!(!args.iter().any(|arg| arg == "--unsafe-rpc-external"));
}
#[test]
fn generate_for_non_native_node_rpc_external_works() {
let node = get_node_spec(false);
let opts = GenCmdOptions {
use_wrapper: false,
is_native: false,
..GenCmdOptions::default()
};
let (program, args) = generate_for_node(&node, opts, Some(1000));
assert_eq!(program.as_str(), "polkadot");
assert!(args.iter().any(|arg| arg == "--unsafe-rpc-external"));
}
#[test]
fn test_arg_removal_removes_insecure_validator_flag() {
let mut node = get_node_spec(false);
node.args
.push(Arg::Flag("-:--insecure-validator-i-know-what-i-do".into()));
node.is_validator = true;
node.available_args_output = Some("--insecure-validator-i-know-what-i-do".to_string());
let opts = GenCmdOptions {
use_wrapper: false,
is_native: true,
..GenCmdOptions::default()
};
let (program, args) = generate_for_node(&node, opts, Some(1000));
assert_eq!(program.as_str(), "polkadot");
assert!(args.iter().any(|arg| arg == "--validator"));
assert!(!args
.iter()
.any(|arg| arg == "--insecure-validator-i-know-what-i-do"));
}
}
@@ -0,0 +1,24 @@
use provider::ProviderError;
use support::fs::FileSystemError;
#[derive(Debug, thiserror::Error)]
pub enum GeneratorError {
#[error("Generating key {0} with input {1}")]
KeyGeneration(String, String),
#[error("Generating port {0}, err {1}")]
PortGeneration(u16, String),
#[error("Chain-spec build error: {0}")]
ChainSpecGeneration(String),
#[error("Provider error: {0}")]
ProviderError(#[from] ProviderError),
#[error("FileSystem error")]
FileSystemError(#[from] FileSystemError),
#[error("Generating identity, err {0}")]
IdentityGeneration(String),
#[error("Generating bootnode address, err {0}")]
BootnodeAddrGeneration(String),
#[error("Error overriding wasm on raw chain-spec, err {0}")]
OverridingWasm(String),
#[error("Error overriding raw chain-spec, err {0}")]
OverridingRawSpec(String),
}
@@ -0,0 +1,41 @@
use hex::FromHex;
use libp2p::identity::{ed25519, Keypair};
use sha2::digest::Digest;
use super::errors::GeneratorError;
// Generate p2p identity for node
// return `node-key` and `peerId`
pub fn generate(node_name: &str) -> Result<(String, String), GeneratorError> {
let key = hex::encode(sha2::Sha256::digest(node_name));
let bytes = <[u8; 32]>::from_hex(key.clone()).map_err(|_| {
GeneratorError::IdentityGeneration("can not transform hex to [u8;32]".into())
})?;
let sk = ed25519::SecretKey::try_from_bytes(bytes)
.map_err(|_| GeneratorError::IdentityGeneration("can not create sk from bytes".into()))?;
let local_identity: Keypair = ed25519::Keypair::from(sk).into();
let local_public = local_identity.public();
let local_peer_id = local_public.to_peer_id();
Ok((key, local_peer_id.to_base58()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_for_alice() {
let s = "alice";
let (key, peer_id) = generate(s).unwrap();
assert_eq!(
&key,
"2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90"
);
assert_eq!(
&peer_id,
"12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm"
);
}
}
@@ -0,0 +1,151 @@
use sp_core::{crypto::SecretStringError, ecdsa, ed25519, keccak_256, sr25519, Pair, H160, H256};
use super::errors::GeneratorError;
use crate::shared::types::{Accounts, NodeAccount};
const KEYS: [&str; 5] = ["sr", "sr_stash", "ed", "ec", "eth"];
pub fn generate_pair<T: Pair>(seed: &str) -> Result<T::Pair, SecretStringError> {
let pair = T::Pair::from_string(seed, None)?;
Ok(pair)
}
pub fn generate(seed: &str) -> Result<Accounts, GeneratorError> {
let mut accounts: Accounts = Default::default();
for k in KEYS {
let (address, public_key) = match k {
"sr" => {
let pair = generate_pair::<sr25519::Pair>(seed)
.map_err(|_| GeneratorError::KeyGeneration(k.into(), seed.into()))?;
(pair.public().to_string(), hex::encode(pair.public()))
},
"sr_stash" => {
let pair = generate_pair::<sr25519::Pair>(&format!("{seed}//stash"))
.map_err(|_| GeneratorError::KeyGeneration(k.into(), seed.into()))?;
(pair.public().to_string(), hex::encode(pair.public()))
},
"ed" => {
let pair = generate_pair::<ed25519::Pair>(seed)
.map_err(|_| GeneratorError::KeyGeneration(k.into(), seed.into()))?;
(pair.public().to_string(), hex::encode(pair.public()))
},
"ec" => {
let pair = generate_pair::<ecdsa::Pair>(seed)
.map_err(|_| GeneratorError::KeyGeneration(k.into(), seed.into()))?;
(pair.public().to_string(), hex::encode(pair.public()))
},
"eth" => {
let pair = generate_pair::<ecdsa::Pair>(seed)
.map_err(|_| GeneratorError::KeyGeneration(k.into(), seed.into()))?;
let decompressed = libsecp256k1::PublicKey::parse_compressed(&pair.public().0)
.map_err(|_| GeneratorError::KeyGeneration(k.into(), seed.into()))?
.serialize();
let mut m = [0u8; 64];
m.copy_from_slice(&decompressed[1..65]);
let account = H160::from(H256::from(keccak_256(&m)));
(hex::encode(account), hex::encode(account))
},
_ => unreachable!(),
};
accounts.insert(k.into(), NodeAccount::new(address, public_key));
}
Ok(accounts)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_for_alice() {
use sp_core::crypto::Ss58Codec;
let s = "Alice";
let seed = format!("//{s}");
let pair = generate_pair::<sr25519::Pair>(&seed).unwrap();
assert_eq!(
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
pair.public().to_ss58check()
);
let pair = generate_pair::<ecdsa::Pair>(&seed).unwrap();
assert_eq!(
"0x020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1",
format!("0x{}", hex::encode(pair.public()))
);
let pair = generate_pair::<ed25519::Pair>(&seed).unwrap();
assert_eq!(
"5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu",
pair.public().to_ss58check()
);
}
#[test]
fn generate_for_zombie() {
use sp_core::crypto::Ss58Codec;
let s = "Zombie";
let seed = format!("//{s}");
let pair = generate_pair::<sr25519::Pair>(&seed).unwrap();
assert_eq!(
"5FTcLfwFc7ctvqp3RhbEig6UuHLHcHVRujuUm8r21wy4dAR8",
pair.public().to_ss58check()
);
}
#[test]
fn generate_pair_invalid_should_fail() {
let s = "Alice";
let seed = s.to_string();
let pair = generate_pair::<sr25519::Pair>(&seed);
assert!(pair.is_err());
}
#[test]
fn generate_invalid_should_fail() {
let s = "Alice";
let seed = s.to_string();
let pair = generate(&seed);
assert!(pair.is_err());
assert!(matches!(pair, Err(GeneratorError::KeyGeneration(_, _))));
}
#[test]
fn generate_work() {
let s = "Alice";
let seed = format!("//{s}");
let pair = generate(&seed).unwrap();
let sr = pair.get("sr").unwrap();
let sr_stash = pair.get("sr_stash").unwrap();
let ed = pair.get("ed").unwrap();
let ec = pair.get("ec").unwrap();
let eth = pair.get("eth").unwrap();
assert_eq!(
sr.address,
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
);
assert_eq!(
sr_stash.address,
"5GNJqTPyNqANBkUVMN1LPPrxXnFouWXoe2wNSmmEoLctxiZY"
);
assert_eq!(
ed.address,
"5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu"
);
assert_eq!(
format!("0x{}", ec.public_key),
"0x020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1"
);
assert_eq!(
format!("0x{}", eth.public_key),
"0xe04cc55ebee1cbce552f250e85c57b70b2e2625b"
)
}
}
@@ -0,0 +1,290 @@
use std::{
path::{Path, PathBuf},
vec,
};
use hex::encode;
use support::{constants::THIS_IS_A_BUG, fs::FileSystem};
use super::errors::GeneratorError;
use crate::{
generators::keystore_key_types::{parse_keystore_key_types, KeystoreKeyType},
shared::types::NodeAccounts,
ScopedFilesystem,
};
/// Generates keystore files for a node.
///
/// # Arguments
/// * `acc` - The node accounts containing the seed and public keys
/// * `node_files_path` - The path where keystore files will be created
/// * `scoped_fs` - The scoped filesystem for file operations
/// * `asset_hub_polkadot` - Whether this is for asset-hub-polkadot (affects aura key scheme)
/// * `keystore_key_types` - Optional list of key type specifications
///
/// If `keystore_key_types` is empty, all default key types will be generated.
/// Otherwise, only the specified key types will be generated.
pub async fn generate<'a, T>(
acc: &NodeAccounts,
node_files_path: impl AsRef<Path>,
scoped_fs: &ScopedFilesystem<'a, T>,
asset_hub_polkadot: bool,
keystore_key_types: Vec<&str>,
) -> Result<Vec<PathBuf>, GeneratorError>
where
T: FileSystem,
{
// Create local keystore
scoped_fs.create_dir_all(node_files_path.as_ref()).await?;
let mut filenames = vec![];
// Parse the key type specifications
let key_types = parse_keystore_key_types(&keystore_key_types, asset_hub_polkadot);
let futures: Vec<_> = key_types
.iter()
.map(|key_type| {
let filename = generate_keystore_filename(key_type, acc);
let file_path = PathBuf::from(format!(
"{}/{}",
node_files_path.as_ref().to_string_lossy(),
filename
));
let content = format!("\"{}\"", acc.seed);
(filename, scoped_fs.write(file_path, content))
})
.collect();
for (filename, future) in futures {
future.await?;
filenames.push(PathBuf::from(filename));
}
Ok(filenames)
}
/// Generates the keystore filename for a given key type.
///
/// The filename format is: `{hex_encoded_key_type}{public_key}`
fn generate_keystore_filename(key_type: &KeystoreKeyType, acc: &NodeAccounts) -> String {
let account_key = key_type.scheme.account_key();
let pk = acc
.accounts
.get(account_key)
.expect(&format!(
"Key '{}' should be set for node {THIS_IS_A_BUG}",
account_key
))
.public_key
.as_str();
format!("{}{}", encode(&key_type.key_type), pk)
}
#[cfg(test)]
mod tests {
use std::{collections::HashMap, ffi::OsString, str::FromStr};
use support::fs::in_memory::{InMemoryFile, InMemoryFileSystem};
use super::*;
use crate::shared::types::{NodeAccount, NodeAccounts};
fn create_test_accounts() -> NodeAccounts {
let mut accounts = HashMap::new();
accounts.insert(
"sr".to_string(),
NodeAccount::new("sr_address", "sr_public_key"),
);
accounts.insert(
"ed".to_string(),
NodeAccount::new("ed_address", "ed_public_key"),
);
accounts.insert(
"ec".to_string(),
NodeAccount::new("ec_address", "ec_public_key"),
);
NodeAccounts {
seed: "//Alice".to_string(),
accounts,
}
}
fn create_test_fs() -> InMemoryFileSystem {
InMemoryFileSystem::new(HashMap::from([(
OsString::from_str("/").unwrap(),
InMemoryFile::dir(),
)]))
}
#[tokio::test]
async fn generate_creates_default_keystore_files_when_no_key_types_specified() {
let accounts = create_test_accounts();
let fs = create_test_fs();
let base_dir = "/tmp/test";
let scoped_fs = ScopedFilesystem { fs: &fs, base_dir };
let key_types: Vec<&str> = vec![];
let res = generate(&accounts, "node1", &scoped_fs, false, key_types).await;
assert!(res.is_ok());
let filenames = res.unwrap();
assert!(filenames.len() > 10);
let filename_strs: Vec<String> = filenames
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect();
// Check that aura key is generated (hex of "aura" is 61757261)
assert!(filename_strs.iter().any(|f| f.starts_with("61757261")));
// Check that babe key is generated (hex of "babe" is 62616265)
assert!(filename_strs.iter().any(|f| f.starts_with("62616265")));
// Check that gran key is generated (hex of "gran" is 6772616e)
assert!(filename_strs.iter().any(|f| f.starts_with("6772616e")));
}
#[tokio::test]
async fn generate_creates_only_specified_keystore_files() {
let accounts = create_test_accounts();
let fs = create_test_fs();
let base_dir = "/tmp/test";
let scoped_fs = ScopedFilesystem { fs: &fs, base_dir };
let key_types = vec!["audi", "gran"];
let res = generate(&accounts, "node1", &scoped_fs, false, key_types).await;
assert!(res.is_ok());
let filenames = res.unwrap();
assert_eq!(filenames.len(), 2);
let filename_strs: Vec<String> = filenames
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect();
// audi uses sr scheme by default
assert!(filename_strs
.iter()
.any(|f| f.starts_with("61756469") && f.contains("sr_public_key")));
// gran uses ed scheme by default
assert!(filename_strs
.iter()
.any(|f| f.starts_with("6772616e") && f.contains("ed_public_key")));
}
#[tokio::test]
async fn generate_produces_correct_keystore_files() {
struct TestCase {
name: &'static str,
key_types: Vec<&'static str>,
asset_hub_polkadot: bool,
expected_prefix: &'static str,
expected_public_key: &'static str,
}
let test_cases = vec![
TestCase {
name: "explicit scheme override (gran_sr)",
key_types: vec!["gran_sr"],
asset_hub_polkadot: false,
expected_prefix: "6772616e", // "gran" in hex
expected_public_key: "sr_public_key",
},
TestCase {
name: "aura with asset_hub_polkadot uses ed",
key_types: vec!["aura"],
asset_hub_polkadot: true,
expected_prefix: "61757261", // "aura" in hex
expected_public_key: "ed_public_key",
},
TestCase {
name: "aura without asset_hub_polkadot uses sr",
key_types: vec!["aura"],
asset_hub_polkadot: false,
expected_prefix: "61757261", // "aura" in hex
expected_public_key: "sr_public_key",
},
TestCase {
name: "custom key type with explicit ec scheme",
key_types: vec!["cust_ec"],
asset_hub_polkadot: false,
expected_prefix: "63757374", // "cust" in hex
expected_public_key: "ec_public_key",
},
];
for tc in test_cases {
let accounts = create_test_accounts();
let fs = create_test_fs();
let scoped_fs = ScopedFilesystem {
fs: &fs,
base_dir: "/tmp/test",
};
let key_types: Vec<&str> = tc.key_types.clone();
let res = generate(
&accounts,
"node1",
&scoped_fs,
tc.asset_hub_polkadot,
key_types,
)
.await;
assert!(
res.is_ok(),
"[{}] Expected Ok but got: {:?}",
tc.name,
res.err()
);
let filenames = res.unwrap();
assert_eq!(filenames.len(), 1, "[{}] Expected 1 file", tc.name);
let filename = filenames[0].to_string_lossy().to_string();
assert!(
filename.starts_with(tc.expected_prefix),
"[{}] Expected prefix '{}', got '{}'",
tc.name,
tc.expected_prefix,
filename
);
assert!(
filename.contains(tc.expected_public_key),
"[{}] Expected public key '{}' in '{}'",
tc.name,
tc.expected_public_key,
filename
);
}
}
#[tokio::test]
async fn generate_ignores_invalid_key_specs_and_uses_defaults() {
let accounts = create_test_accounts();
let fs = create_test_fs();
let scoped_fs = ScopedFilesystem {
fs: &fs,
base_dir: "/tmp/test",
};
let key_types = vec![
"invalid", // Too long
"xxx", // Too short
"audi_xx", // Invalid sceme
];
let res = generate(&accounts, "node1", &scoped_fs, false, key_types).await;
assert!(res.is_ok());
let filenames = res.unwrap();
// Should fall back to defaults since all specs are invalid
assert!(filenames.len() > 10);
}
}
@@ -0,0 +1,282 @@
use std::{collections::HashMap, fmt::Formatter};
use serde::{Deserialize, Serialize};
/// Supported cryptographic schemes for keystore keys.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum KeyScheme {
/// Sr25519 scheme
Sr,
/// Ed25519 scheme
Ed,
/// ECDSA scheme
Ec,
}
impl KeyScheme {
/// Returns the account key suffix used in `NodeAccounts` for this scheme.
pub fn account_key(&self) -> &'static str {
match self {
KeyScheme::Sr => "sr",
KeyScheme::Ed => "ed",
KeyScheme::Ec => "ec",
}
}
}
impl std::fmt::Display for KeyScheme {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
KeyScheme::Sr => write!(f, "sr"),
KeyScheme::Ed => write!(f, "ed"),
KeyScheme::Ec => write!(f, "ec"),
}
}
}
impl TryFrom<&str> for KeyScheme {
type Error = String;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value.to_lowercase().as_str() {
"sr" => Ok(KeyScheme::Sr),
"ed" => Ok(KeyScheme::Ed),
"ec" => Ok(KeyScheme::Ec),
_ => Err(format!("Unsupported key scheme: {}", value)),
}
}
}
/// A parsed keystore key type.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeystoreKeyType {
/// The 4-character key type identifier (e.g., "aura", "babe", "gran").
pub key_type: String,
/// The cryptographic scheme to use for this key type.
pub scheme: KeyScheme,
}
impl KeystoreKeyType {
pub fn new(key_type: impl Into<String>, scheme: KeyScheme) -> Self {
Self {
key_type: key_type.into(),
scheme,
}
}
}
/// Returns the default predefined key schemes for known key types.
/// Special handling for `aura` when `is_asset_hub_polkadot` is true.
fn get_predefined_schemes(is_asset_hub_polkadot: bool) -> HashMap<&'static str, KeyScheme> {
let mut schemes = HashMap::new();
// aura has special handling for asset-hub-polkadot
if is_asset_hub_polkadot {
schemes.insert("aura", KeyScheme::Ed);
} else {
schemes.insert("aura", KeyScheme::Sr);
}
schemes.insert("babe", KeyScheme::Sr);
schemes.insert("imon", KeyScheme::Sr);
schemes.insert("gran", KeyScheme::Ed);
schemes.insert("audi", KeyScheme::Sr);
schemes.insert("asgn", KeyScheme::Sr);
schemes.insert("para", KeyScheme::Sr);
schemes.insert("beef", KeyScheme::Ec);
schemes.insert("nmbs", KeyScheme::Sr); // Nimbus
schemes.insert("rand", KeyScheme::Sr); // Randomness (Moonbeam)
schemes.insert("rate", KeyScheme::Ed); // Equilibrium rate module
schemes.insert("acco", KeyScheme::Sr);
schemes.insert("bcsv", KeyScheme::Sr); // BlockchainSrvc (StorageHub)
schemes.insert("ftsv", KeyScheme::Ed); // FileTransferSrvc (StorageHub)
schemes.insert("mixn", KeyScheme::Sr); // Mixnet
schemes
}
/// Parses a single keystore key type specification string.
///
/// Supports two formats:
/// - Short: `audi` - creates key type with predefined default scheme (defaults to `sr` if not predefined)
/// - Long: `audi_sr` - creates key type with explicit scheme
///
/// Returns `None` if the spec is invalid or doesn't match the expected format.
fn parse_key_spec(spec: &str, predefined: &HashMap<&str, KeyScheme>) -> Option<KeystoreKeyType> {
let spec = spec.trim();
// Try parsing as long form first: key_type_scheme (e.g., "audi_sr")
if let Some((key_type, scheme_str)) = spec.split_once('_') {
if key_type.len() != 4 {
return None;
}
let scheme = KeyScheme::try_from(scheme_str).ok()?;
return Some(KeystoreKeyType::new(key_type, scheme));
}
// Try parsing as short form: key_type only (e.g., "audi")
if spec.len() == 4 {
// Look up predefined scheme; default to Sr if not found
let scheme = predefined.get(spec).copied().unwrap_or(KeyScheme::Sr);
return Some(KeystoreKeyType::new(spec, scheme));
}
None
}
/// Parses a list of keystore key type specifications.
///
/// Each spec can be in short form (`audi`) or long form (`audi_sr`).
/// Invalid specs are silently ignored.
///
/// If the resulting list is empty, returns the default keystore key types.
pub fn parse_keystore_key_types<T: AsRef<str>>(
specs: &[T],
is_asset_hub_polkadot: bool,
) -> Vec<KeystoreKeyType> {
let predefined_schemes = get_predefined_schemes(is_asset_hub_polkadot);
let parsed: Vec<KeystoreKeyType> = specs
.iter()
.filter_map(|spec| parse_key_spec(spec.as_ref(), &predefined_schemes))
.collect();
if parsed.is_empty() {
get_default_keystore_key_types(is_asset_hub_polkadot)
} else {
parsed
}
}
/// Returns the default keystore key types when none are specified.
pub fn get_default_keystore_key_types(is_asset_hub_polkadot: bool) -> Vec<KeystoreKeyType> {
let predefined_schemes = get_predefined_schemes(is_asset_hub_polkadot);
let default_keys = [
"aura", "babe", "imon", "gran", "audi", "asgn", "para", "beef", "nmbs", "rand", "rate",
"mixn", "bcsv", "ftsv",
];
default_keys
.iter()
.filter_map(|key_type| {
predefined_schemes
.get(*key_type)
.map(|scheme| KeystoreKeyType::new(*key_type, *scheme))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_keystore_key_types_ignores_invalid_specs() {
let specs = vec![
"audi".to_string(),
"invalid".to_string(), // Too long - ignored
"xxx".to_string(), // Too short - ignored
"xxxx".to_string(), // Unknown key - defaults to sr
"audi_xx".to_string(), // Invalid scheme - ignored
"gran".to_string(),
];
let result = parse_keystore_key_types(&specs, false);
assert_eq!(result.len(), 3);
assert_eq!(result[1], KeystoreKeyType::new("xxxx", KeyScheme::Sr)); // Unknown defaults to sr
assert_eq!(result[2], KeystoreKeyType::new("gran", KeyScheme::Ed));
}
#[test]
fn parse_keystore_key_types_returns_specified_keys() {
let specs = vec!["audi".to_string(), "gran".to_string()];
let res = parse_keystore_key_types(&specs, false);
assert_eq!(res.len(), 2);
assert_eq!(res[0], KeystoreKeyType::new("audi", KeyScheme::Sr));
assert_eq!(res[1], KeystoreKeyType::new("gran", KeyScheme::Ed));
}
#[test]
fn parse_keystore_key_types_mixed_short_and_long_forms() {
let specs = vec![
"audi".to_string(),
"gran_sr".to_string(), // Override gran's default ed to sr
"gran".to_string(),
"beef".to_string(),
];
let res = parse_keystore_key_types(&specs, false);
assert_eq!(res.len(), 4);
assert_eq!(res[0], KeystoreKeyType::new("audi", KeyScheme::Sr));
assert_eq!(res[1], KeystoreKeyType::new("gran", KeyScheme::Sr)); // Overridden
assert_eq!(res[2], KeystoreKeyType::new("gran", KeyScheme::Ed));
assert_eq!(res[3], KeystoreKeyType::new("beef", KeyScheme::Ec));
}
#[test]
fn parse_keystore_key_types_returns_defaults_when_empty() {
let specs: Vec<String> = vec![];
let res = parse_keystore_key_types(&specs, false);
// Should return all default keys
assert!(!res.is_empty());
assert!(res.iter().any(|k| k.key_type == "aura"));
assert!(res.iter().any(|k| k.key_type == "babe"));
assert!(res.iter().any(|k| k.key_type == "gran"));
}
#[test]
fn parse_keystore_key_types_allows_custom_key_with_explicit_scheme() {
let specs = vec![
"cust_sr".to_string(), // Custom key with explicit scheme
"audi".to_string(),
];
let result = parse_keystore_key_types(&specs, false);
assert_eq!(result.len(), 2);
assert_eq!(result[0], KeystoreKeyType::new("cust", KeyScheme::Sr));
assert_eq!(result[1], KeystoreKeyType::new("audi", KeyScheme::Sr));
}
#[test]
fn full_workflow_asset_hub_polkadot() {
// For asset-hub-polkadot, aura should default to ed
let specs = vec!["aura".to_string(), "babe".to_string()];
let res = parse_keystore_key_types(&specs, true);
assert_eq!(res.len(), 2);
assert_eq!(res[0].key_type, "aura");
assert_eq!(res[0].scheme, KeyScheme::Ed); // ed for asset-hub-polkadot
assert_eq!(res[1].key_type, "babe");
assert_eq!(res[1].scheme, KeyScheme::Sr);
}
#[test]
fn full_workflow_custom_key_types() {
let specs = vec![
"aura".to_string(), // Use default scheme
"gran_sr".to_string(), // Override gran to use sr instead of ed
"cust_ec".to_string(), // Custom key type with ecdsa
];
let res = parse_keystore_key_types(&specs, false);
assert_eq!(res.len(), 3);
// aura uses default sr
assert_eq!(res[0].key_type, "aura");
assert_eq!(res[0].scheme, KeyScheme::Sr);
// gran overridden to sr
assert_eq!(res[1].key_type, "gran");
assert_eq!(res[1].scheme, KeyScheme::Sr);
// custom key with ec
assert_eq!(res[2].key_type, "cust");
assert_eq!(res[2].scheme, KeyScheme::Ec);
}
}
@@ -0,0 +1,165 @@
use std::path::{Path, PathBuf};
use configuration::types::CommandWithCustomArgs;
use provider::{
constants::NODE_CONFIG_DIR,
types::{GenerateFileCommand, GenerateFilesOptions, TransferedFile},
DynNamespace,
};
use serde::{Deserialize, Serialize};
use support::fs::FileSystem;
use uuid::Uuid;
use super::errors::GeneratorError;
use crate::ScopedFilesystem;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) enum ParaArtifactType {
Wasm,
State,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) enum ParaArtifactBuildOption {
Path(String),
Command(String),
CommandWithCustomArgs(CommandWithCustomArgs),
}
/// Parachain artifact (could be either the genesis state or genesis wasm)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParaArtifact {
artifact_type: ParaArtifactType,
build_option: ParaArtifactBuildOption,
artifact_path: Option<PathBuf>,
// image to use for building the para artifact
image: Option<String>,
}
impl ParaArtifact {
pub(crate) fn new(
artifact_type: ParaArtifactType,
build_option: ParaArtifactBuildOption,
) -> Self {
Self {
artifact_type,
build_option,
artifact_path: None,
image: None,
}
}
pub(crate) fn image(mut self, image: Option<String>) -> Self {
self.image = image;
self
}
pub(crate) fn artifact_path(&self) -> Option<&PathBuf> {
self.artifact_path.as_ref()
}
pub(crate) async fn build<'a, T>(
&mut self,
chain_spec_path: Option<impl AsRef<Path>>,
artifact_path: impl AsRef<Path>,
ns: &DynNamespace,
scoped_fs: &ScopedFilesystem<'a, T>,
maybe_output_path: Option<PathBuf>,
) -> Result<(), GeneratorError>
where
T: FileSystem,
{
let (cmd, custom_args) = match &self.build_option {
ParaArtifactBuildOption::Path(path) => {
let t = TransferedFile::new(PathBuf::from(path), artifact_path.as_ref().into());
scoped_fs.copy_files(vec![&t]).await?;
self.artifact_path = Some(artifact_path.as_ref().into());
return Ok(()); // work done!
},
ParaArtifactBuildOption::Command(cmd) => (cmd, &vec![]),
ParaArtifactBuildOption::CommandWithCustomArgs(cmd_with_custom_args) => {
(
&cmd_with_custom_args.cmd().as_str().to_string(),
cmd_with_custom_args.args(),
)
// (cmd.cmd_as_str().to_string(), cmd.1)
},
};
let generate_subcmd = match self.artifact_type {
ParaArtifactType::Wasm => "export-genesis-wasm",
ParaArtifactType::State => "export-genesis-state",
};
// TODO: replace uuid with para_id-random
let temp_name = format!("temp-{}-{}", generate_subcmd, Uuid::new_v4());
let mut args: Vec<String> = vec![generate_subcmd.into()];
let files_to_inject = if let Some(chain_spec_path) = chain_spec_path {
// TODO: we should get the full path from the scoped filesystem
let chain_spec_path_local = format!(
"{}/{}",
ns.base_dir().to_string_lossy(),
chain_spec_path.as_ref().to_string_lossy()
);
// Remote path to be injected
let chain_spec_path_in_pod = format!(
"{}/{}",
NODE_CONFIG_DIR,
chain_spec_path.as_ref().to_string_lossy()
);
// Path in the context of the node, this can be different in the context of the providers (e.g native)
let chain_spec_path_in_args = if ns.capabilities().prefix_with_full_path {
// In native
format!(
"{}/{}{}",
ns.base_dir().to_string_lossy(),
&temp_name,
&chain_spec_path_in_pod
)
} else {
chain_spec_path_in_pod.clone()
};
args.push("--chain".into());
args.push(chain_spec_path_in_args);
for custom_arg in custom_args {
match custom_arg {
configuration::types::Arg::Flag(flag) => {
args.push(flag.into());
},
configuration::types::Arg::Option(flag, flag_value) => {
args.push(flag.into());
args.push(flag_value.into());
},
configuration::types::Arg::Array(flag, values) => {
args.push(flag.into());
values.iter().for_each(|v| args.push(v.into()));
},
}
}
vec![TransferedFile::new(
chain_spec_path_local,
chain_spec_path_in_pod,
)]
} else {
vec![]
};
let artifact_path_ref = artifact_path.as_ref();
let generate_command = GenerateFileCommand::new(cmd.as_str(), artifact_path_ref).args(args);
let options = GenerateFilesOptions::with_files(
vec![generate_command],
self.image.clone(),
&files_to_inject,
maybe_output_path,
)
.temp_name(temp_name);
ns.generate_files(options).await?;
self.artifact_path = Some(artifact_path_ref.into());
Ok(())
}
}
@@ -0,0 +1,48 @@
use std::net::TcpListener;
use configuration::shared::types::Port;
use support::constants::THIS_IS_A_BUG;
use super::errors::GeneratorError;
use crate::shared::types::ParkedPort;
// TODO: (team), we want to continue support ws_port? No
enum PortTypes {
Rpc,
P2P,
Prometheus,
}
pub fn generate(port: Option<Port>) -> Result<ParkedPort, GeneratorError> {
let port = port.unwrap_or(0);
let listener = TcpListener::bind(format!("0.0.0.0:{port}"))
.map_err(|_e| GeneratorError::PortGeneration(port, "Can't bind".into()))?;
let port = listener
.local_addr()
.expect(&format!(
"We should always get the local_addr from the listener {THIS_IS_A_BUG}"
))
.port();
Ok(ParkedPort::new(port, listener))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_random() {
let port = generate(None).unwrap();
let listener = port.1.write().unwrap();
assert!(listener.is_some());
}
#[test]
fn generate_fixed_port() {
let port = generate(Some(33056)).unwrap();
let listener = port.1.write().unwrap();
assert!(listener.is_some());
assert_eq!(port.0, 33056);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,842 @@
pub mod chain_upgrade;
pub mod node;
pub mod relaychain;
pub mod teyrchain;
use std::{cell::RefCell, collections::HashMap, path::PathBuf, rc::Rc, sync::Arc, time::Duration};
use configuration::{
para_states::{Initial, Running},
shared::{helpers::generate_unique_node_name_from_names, node::EnvVar},
types::{Arg, Command, Image, Port, ValidationContext},
ParachainConfig, ParachainConfigBuilder, RegistrationStrategy,
};
use provider::{types::TransferedFile, DynNamespace, ProviderError};
use serde::Serialize;
use support::fs::FileSystem;
use tokio::sync::RwLock;
use tracing::{error, warn};
use self::{node::NetworkNode, relaychain::Relaychain, teyrchain::Parachain};
use crate::{
generators::chain_spec::ChainSpec,
network_spec::{self, NetworkSpec},
shared::{
constants::{NODE_MONITORING_FAILURE_THRESHOLD_SECONDS, NODE_MONITORING_INTERVAL_SECONDS},
macros,
types::{ChainDefaultContext, RegisterParachainOptions},
},
spawner::{self, SpawnNodeCtx},
ScopedFilesystem, ZombieRole,
};
#[derive(Serialize)]
pub struct Network<T: FileSystem> {
#[serde(skip)]
ns: DynNamespace,
#[serde(skip)]
filesystem: T,
relay: Relaychain,
initial_spec: NetworkSpec,
parachains: HashMap<u32, Vec<Parachain>>,
#[serde(skip)]
nodes_by_name: HashMap<String, NetworkNode>,
#[serde(skip)]
nodes_to_watch: Arc<RwLock<Vec<NetworkNode>>>,
}
impl<T: FileSystem> std::fmt::Debug for Network<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Network")
.field("ns", &"ns_skipped")
.field("relay", &self.relay)
.field("initial_spec", &self.initial_spec)
.field("parachains", &self.parachains)
.field("nodes_by_name", &self.nodes_by_name)
.finish()
}
}
macros::create_add_options!(AddNodeOptions {
chain_spec: Option<PathBuf>,
override_eth_key: Option<String>
});
macros::create_add_options!(AddCollatorOptions {
chain_spec: Option<PathBuf>,
chain_spec_relay: Option<PathBuf>,
override_eth_key: Option<String>
});
impl<T: FileSystem> Network<T> {
pub(crate) fn new_with_relay(
relay: Relaychain,
ns: DynNamespace,
fs: T,
initial_spec: NetworkSpec,
) -> Self {
Self {
ns,
filesystem: fs,
relay,
initial_spec,
parachains: Default::default(),
nodes_by_name: Default::default(),
nodes_to_watch: Default::default(),
}
}
// Pubic API
pub fn ns_name(&self) -> String {
self.ns.name().to_string()
}
pub fn base_dir(&self) -> Option<&str> {
self.ns.base_dir().to_str()
}
pub fn relaychain(&self) -> &Relaychain {
&self.relay
}
// Teardown the network
pub async fn destroy(self) -> Result<(), ProviderError> {
self.ns.destroy().await
}
/// Add a node to the relaychain
// The new node is added to the running network instance.
/// # Example:
/// ```rust
/// # use provider::NativeProvider;
/// # use support::{fs::local::LocalFileSystem};
/// # use zombienet_orchestrator::{errors, AddNodeOptions, Orchestrator};
/// # use configuration::NetworkConfig;
/// # async fn example() -> Result<(), errors::OrchestratorError> {
/// # let provider = NativeProvider::new(LocalFileSystem {});
/// # let orchestrator = Orchestrator::new(LocalFileSystem {}, provider);
/// # let config = NetworkConfig::load_from_toml("config.toml")?;
/// let mut network = orchestrator.spawn(config).await?;
///
/// // Create the options to add the new node
/// let opts = AddNodeOptions {
/// rpc_port: Some(9444),
/// is_validator: true,
/// ..Default::default()
/// };
///
/// network.add_node("new-node", opts).await?;
/// # Ok(())
/// # }
/// ```
pub async fn add_node(
&mut self,
name: impl Into<String>,
options: AddNodeOptions,
) -> Result<(), anyhow::Error> {
let name = generate_unique_node_name_from_names(
name,
&mut self.nodes_by_name.keys().cloned().collect(),
);
let relaychain = self.relaychain();
let chain_spec_path = if let Some(chain_spec_custom_path) = &options.chain_spec {
chain_spec_custom_path.clone()
} else {
PathBuf::from(format!(
"{}/{}.json",
self.ns.base_dir().to_string_lossy(),
relaychain.chain
))
};
let chain_context = ChainDefaultContext {
default_command: self.initial_spec.relaychain.default_command.as_ref(),
default_image: self.initial_spec.relaychain.default_image.as_ref(),
default_resources: self.initial_spec.relaychain.default_resources.as_ref(),
default_db_snapshot: self.initial_spec.relaychain.default_db_snapshot.as_ref(),
default_args: self.initial_spec.relaychain.default_args.iter().collect(),
};
let mut node_spec = network_spec::node::NodeSpec::from_ad_hoc(
&name,
options.into(),
&chain_context,
false,
false,
)?;
node_spec.available_args_output = Some(
self.initial_spec
.node_available_args_output(&node_spec, self.ns.clone())
.await?,
);
let base_dir = self.ns.base_dir().to_string_lossy();
let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
let ctx = SpawnNodeCtx {
chain_id: &relaychain.chain_id,
parachain_id: None,
chain: &relaychain.chain,
role: ZombieRole::Node,
ns: &self.ns,
scoped_fs: &scoped_fs,
parachain: None,
bootnodes_addr: &vec![],
wait_ready: true,
nodes_by_name: serde_json::to_value(&self.nodes_by_name)?,
global_settings: &self.initial_spec.global_settings,
};
let global_files_to_inject = vec![TransferedFile::new(
chain_spec_path,
PathBuf::from(format!("/cfg/{}.json", relaychain.chain)),
)];
let node = spawner::spawn_node(&node_spec, global_files_to_inject, &ctx).await?;
// TODO: register the new node as validator in the relaychain
// STEPS:
// - check balance of `stash` derivation for validator account
// - call rotate_keys on the new validator
// - call setKeys on the new validator
// if node_spec.is_validator {
// let running_node = self.relay.nodes.first().unwrap();
// // tx_helper::validator_actions::register(vec![&node], &running_node.ws_uri, None).await?;
// }
// Let's make sure node is up before adding
node.wait_until_is_up(self.initial_spec.global_settings.network_spawn_timeout())
.await?;
// Add node to relaychain data
self.add_running_node(node.clone(), None).await;
Ok(())
}
/// Add a new collator to a parachain
///
/// NOTE: if more parachains with given id available (rare corner case)
/// then it adds collator to the first parachain
///
/// # Example:
/// ```rust
/// # use provider::NativeProvider;
/// # use support::{fs::local::LocalFileSystem};
/// # use zombienet_orchestrator::{errors, AddCollatorOptions, Orchestrator};
/// # use configuration::NetworkConfig;
/// # async fn example() -> Result<(), anyhow::Error> {
/// # let provider = NativeProvider::new(LocalFileSystem {});
/// # let orchestrator = Orchestrator::new(LocalFileSystem {}, provider);
/// # let config = NetworkConfig::load_from_toml("config.toml")?;
/// let mut network = orchestrator.spawn(config).await?;
///
/// let col_opts = AddCollatorOptions {
/// command: Some("polkadot-parachain".try_into()?),
/// ..Default::default()
/// };
///
/// network.add_collator("new-col-1", col_opts, 100).await?;
/// # Ok(())
/// # }
/// ```
pub async fn add_collator(
&mut self,
name: impl Into<String>,
options: AddCollatorOptions,
para_id: u32,
) -> Result<(), anyhow::Error> {
let name = generate_unique_node_name_from_names(
name,
&mut self.nodes_by_name.keys().cloned().collect(),
);
let spec = self
.initial_spec
.parachains
.iter()
.find(|para| para.id == para_id)
.ok_or(anyhow::anyhow!(format!("parachain: {para_id} not found!")))?;
let role = if spec.is_cumulus_based {
ZombieRole::CumulusCollator
} else {
ZombieRole::Collator
};
let chain_context = ChainDefaultContext {
default_command: spec.default_command.as_ref(),
default_image: spec.default_image.as_ref(),
default_resources: spec.default_resources.as_ref(),
default_db_snapshot: spec.default_db_snapshot.as_ref(),
default_args: spec.default_args.iter().collect(),
};
let parachain = self
.parachains
.get_mut(&para_id)
.ok_or(anyhow::anyhow!(format!("parachain: {para_id} not found!")))?
.get_mut(0)
.ok_or(anyhow::anyhow!(format!("parachain: {para_id} not found!")))?;
let base_dir = self.ns.base_dir().to_string_lossy();
let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
// TODO: we want to still supporting spawn a dedicated bootnode??
let ctx = SpawnNodeCtx {
chain_id: &self.relay.chain_id,
parachain_id: parachain.chain_id.as_deref(),
chain: &self.relay.chain,
role,
ns: &self.ns,
scoped_fs: &scoped_fs,
parachain: Some(spec),
bootnodes_addr: &vec![],
wait_ready: true,
nodes_by_name: serde_json::to_value(&self.nodes_by_name)?,
global_settings: &self.initial_spec.global_settings,
};
let relaychain_spec_path = if let Some(chain_spec_custom_path) = &options.chain_spec_relay {
chain_spec_custom_path.clone()
} else {
PathBuf::from(format!(
"{}/{}.json",
self.ns.base_dir().to_string_lossy(),
self.relay.chain
))
};
let mut global_files_to_inject = vec![TransferedFile::new(
relaychain_spec_path,
PathBuf::from(format!("/cfg/{}.json", self.relay.chain)),
)];
let para_chain_spec_local_path = if let Some(para_chain_spec_custom) = &options.chain_spec {
Some(para_chain_spec_custom.clone())
} else if let Some(para_spec_path) = &parachain.chain_spec_path {
Some(PathBuf::from(format!(
"{}/{}",
self.ns.base_dir().to_string_lossy(),
para_spec_path.to_string_lossy()
)))
} else {
None
};
if let Some(para_spec_path) = para_chain_spec_local_path {
global_files_to_inject.push(TransferedFile::new(
para_spec_path,
PathBuf::from(format!("/cfg/{para_id}.json")),
));
}
let mut node_spec = network_spec::node::NodeSpec::from_ad_hoc(
name,
options.into(),
&chain_context,
true,
spec.is_evm_based,
)?;
node_spec.available_args_output = Some(
self.initial_spec
.node_available_args_output(&node_spec, self.ns.clone())
.await?,
);
let node = spawner::spawn_node(&node_spec, global_files_to_inject, &ctx).await?;
// Let's make sure node is up before adding
node.wait_until_is_up(self.initial_spec.global_settings.network_spawn_timeout())
.await?;
parachain.collators.push(node.clone());
self.add_running_node(node, None).await;
Ok(())
}
/// Get a parachain config builder from a running network
///
/// This allow you to build a new parachain config to be deployed into
/// the running network.
pub fn para_config_builder(&self) -> ParachainConfigBuilder<Initial, Running> {
let used_ports = self
.nodes_iter()
.map(|node| node.spec())
.flat_map(|spec| {
[
spec.ws_port.0,
spec.rpc_port.0,
spec.prometheus_port.0,
spec.p2p_port.0,
]
})
.collect();
let used_nodes_names = self.nodes_by_name.keys().cloned().collect();
// need to inverse logic of generate_unique_para_id
let used_para_ids = self
.parachains
.iter()
.map(|(id, paras)| (*id, paras.len().saturating_sub(1) as u8))
.collect();
let context = ValidationContext {
used_ports,
used_nodes_names,
used_para_ids,
};
let context = Rc::new(RefCell::new(context));
ParachainConfigBuilder::new_with_running(context)
}
/// Add a new parachain to the running network
///
/// # Arguments
/// * `para_config` - Parachain configuration to deploy
/// * `custom_relaychain_spec` - Optional path to a custom relaychain spec to use
/// * `custom_parchain_fs_prefix` - Optional prefix to use when artifacts are created
///
///
/// # Example:
/// ```rust
/// # use anyhow::anyhow;
/// # use provider::NativeProvider;
/// # use support::{fs::local::LocalFileSystem};
/// # use zombienet_orchestrator::{errors, AddCollatorOptions, Orchestrator};
/// # use configuration::NetworkConfig;
/// # async fn example() -> Result<(), anyhow::Error> {
/// # let provider = NativeProvider::new(LocalFileSystem {});
/// # let orchestrator = Orchestrator::new(LocalFileSystem {}, provider);
/// # let config = NetworkConfig::load_from_toml("config.toml")?;
/// let mut network = orchestrator.spawn(config).await?;
/// let para_config = network
/// .para_config_builder()
/// .with_id(100)
/// .with_default_command("polkadot-parachain")
/// .with_collator(|c| c.with_name("col-100-1"))
/// .build()
/// .map_err(|_e| anyhow!("Building config"))?;
///
/// network.add_parachain(&para_config, None, None).await?;
///
/// # Ok(())
/// # }
/// ```
pub async fn add_parachain(
&mut self,
para_config: &ParachainConfig,
custom_relaychain_spec: Option<PathBuf>,
custom_parchain_fs_prefix: Option<String>,
) -> Result<(), anyhow::Error> {
let base_dir = self.ns.base_dir().to_string_lossy().to_string();
let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
let mut global_files_to_inject = vec![];
// get relaychain id
let relay_chain_id = if let Some(custom_path) = custom_relaychain_spec {
// use this file as relaychain spec
global_files_to_inject.push(TransferedFile::new(
custom_path.clone(),
PathBuf::from(format!("/cfg/{}.json", self.relaychain().chain)),
));
let content = std::fs::read_to_string(custom_path)?;
ChainSpec::chain_id_from_spec(&content)?
} else {
global_files_to_inject.push(TransferedFile::new(
PathBuf::from(format!(
"{}/{}",
scoped_fs.base_dir,
self.relaychain().chain_spec_path.to_string_lossy()
)),
PathBuf::from(format!("/cfg/{}.json", self.relaychain().chain)),
));
self.relay.chain_id.clone()
};
let mut para_spec = network_spec::teyrchain::TeyrchainSpec::from_config(
para_config,
relay_chain_id.as_str().try_into()?,
)?;
let chain_spec_raw_path = para_spec
.build_chain_spec(&relay_chain_id, &self.ns, &scoped_fs)
.await?;
// Para artifacts
let para_path_prefix = if let Some(custom_prefix) = custom_parchain_fs_prefix {
custom_prefix
} else {
para_spec.id.to_string()
};
scoped_fs.create_dir(&para_path_prefix).await?;
// create wasm/state
para_spec
.genesis_state
.build(
chain_spec_raw_path.as_ref(),
format!("{}/genesis-state", &para_path_prefix),
&self.ns,
&scoped_fs,
None,
)
.await?;
para_spec
.genesis_wasm
.build(
chain_spec_raw_path.as_ref(),
format!("{}/para_spec-wasm", &para_path_prefix),
&self.ns,
&scoped_fs,
None,
)
.await?;
let parachain =
Parachain::from_spec(&para_spec, &global_files_to_inject, &scoped_fs).await?;
let parachain_id = parachain.chain_id.clone();
// Create `ctx` for spawn the nodes
let ctx_para = SpawnNodeCtx {
parachain: Some(&para_spec),
parachain_id: parachain_id.as_deref(),
role: if para_spec.is_cumulus_based {
ZombieRole::CumulusCollator
} else {
ZombieRole::Collator
},
bootnodes_addr: &para_config
.bootnodes_addresses()
.iter()
.map(|&a| a.to_string())
.collect(),
chain_id: &self.relaychain().chain_id,
chain: &self.relaychain().chain,
ns: &self.ns,
scoped_fs: &scoped_fs,
wait_ready: false,
nodes_by_name: serde_json::to_value(&self.nodes_by_name)?,
global_settings: &self.initial_spec.global_settings,
};
// Register the parachain to the running network
let first_node_url = self
.relaychain()
.nodes
.first()
.ok_or(anyhow::anyhow!(
"At least one node of the relaychain should be running"
))?
.ws_uri();
if para_config.registration_strategy() == Some(&RegistrationStrategy::UsingExtrinsic) {
let register_para_options = RegisterParachainOptions {
id: parachain.para_id,
// This needs to resolve correctly
wasm_path: para_spec
.genesis_wasm
.artifact_path()
.ok_or(anyhow::anyhow!(
"artifact path for wasm must be set at this point",
))?
.to_path_buf(),
state_path: para_spec
.genesis_state
.artifact_path()
.ok_or(anyhow::anyhow!(
"artifact path for state must be set at this point",
))?
.to_path_buf(),
node_ws_url: first_node_url.to_string(),
onboard_as_para: para_spec.onboard_as_parachain,
seed: None, // TODO: Seed is passed by?
finalization: false,
};
Parachain::register(register_para_options, &scoped_fs).await?;
}
// Spawn the nodes
let spawning_tasks = para_spec
.collators
.iter()
.map(|node| spawner::spawn_node(node, parachain.files_to_inject.clone(), &ctx_para));
let running_nodes = futures::future::try_join_all(spawning_tasks).await?;
// Let's make sure nodes are up before adding them
let waiting_tasks = running_nodes.iter().map(|node| {
node.wait_until_is_up(self.initial_spec.global_settings.network_spawn_timeout())
});
let _ = futures::future::try_join_all(waiting_tasks).await?;
let running_para_id = parachain.para_id;
self.add_para(parachain);
for node in running_nodes {
self.add_running_node(node, Some(running_para_id)).await;
}
Ok(())
}
/// Register a parachain, which has already been added to the network (with manual registration
/// strategy)
///
/// # Arguments
/// * `para_id` - Parachain Id
///
///
/// # Example:
/// ```rust
/// # use anyhow::anyhow;
/// # use provider::NativeProvider;
/// # use support::{fs::local::LocalFileSystem};
/// # use zombienet_orchestrator::Orchestrator;
/// # use configuration::{NetworkConfig, NetworkConfigBuilder, RegistrationStrategy};
/// # async fn example() -> Result<(), anyhow::Error> {
/// # let provider = NativeProvider::new(LocalFileSystem {});
/// # let orchestrator = Orchestrator::new(LocalFileSystem {}, provider);
/// # let config = NetworkConfigBuilder::new()
/// # .with_relaychain(|r| {
/// # r.with_chain("rococo-local")
/// # .with_default_command("polkadot")
/// # .with_node(|node| node.with_name("alice"))
/// # })
/// # .with_parachain(|p| {
/// # p.with_id(100)
/// # .with_registration_strategy(RegistrationStrategy::Manual)
/// # .with_default_command("test-parachain")
/// # .with_collator(|n| n.with_name("dave").validator(false))
/// # })
/// # .build()
/// # .map_err(|_e| anyhow!("Building config"))?;
/// let mut network = orchestrator.spawn(config).await?;
///
/// network.register_parachain(100).await?;
///
/// # Ok(())
/// # }
/// ```
pub async fn register_parachain(&mut self, para_id: u32) -> Result<(), anyhow::Error> {
let para = self
.initial_spec
.parachains
.iter()
.find(|p| p.id == para_id)
.ok_or(anyhow::anyhow!(
"no parachain with id = {para_id} available",
))?;
let para_genesis_config = para.get_genesis_config()?;
let first_node_url = self
.relaychain()
.nodes
.first()
.ok_or(anyhow::anyhow!(
"At least one node of the relaychain should be running"
))?
.ws_uri();
let register_para_options: RegisterParachainOptions = RegisterParachainOptions {
id: para_id,
// This needs to resolve correctly
wasm_path: para_genesis_config.wasm_path.clone(),
state_path: para_genesis_config.state_path.clone(),
node_ws_url: first_node_url.to_string(),
onboard_as_para: para_genesis_config.as_parachain,
seed: None, // TODO: Seed is passed by?
finalization: false,
};
let base_dir = self.ns.base_dir().to_string_lossy().to_string();
let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
Parachain::register(register_para_options, &scoped_fs).await?;
Ok(())
}
// deregister and stop the collator?
// remove_parachain()
pub fn get_node(&self, name: impl Into<String>) -> Result<&NetworkNode, anyhow::Error> {
let name = name.into();
if let Some(node) = self.nodes_iter().find(|&n| n.name == name) {
return Ok(node);
}
let list = self
.nodes_iter()
.map(|n| &n.name)
.cloned()
.collect::<Vec<_>>()
.join(", ");
Err(anyhow::anyhow!(
"can't find node with name: {name:?}, should be one of {list}"
))
}
pub fn get_node_mut(
&mut self,
name: impl Into<String>,
) -> Result<&mut NetworkNode, anyhow::Error> {
let name = name.into();
self.nodes_iter_mut()
.find(|n| n.name == name)
.ok_or(anyhow::anyhow!("can't find node with name: {name:?}"))
}
pub fn nodes(&self) -> Vec<&NetworkNode> {
self.nodes_by_name.values().collect::<Vec<&NetworkNode>>()
}
pub async fn detach(&self) {
self.ns.detach().await
}
// Internal API
pub(crate) async fn add_running_node(&mut self, node: NetworkNode, para_id: Option<u32>) {
if let Some(para_id) = para_id {
if let Some(para) = self.parachains.get_mut(&para_id).and_then(|p| p.get_mut(0)) {
para.collators.push(node.clone());
} else {
// is the first node of the para, let create the entry
unreachable!()
}
} else {
self.relay.nodes.push(node.clone());
}
// TODO: we should hold a ref to the node in the vec in the future.
node.set_is_running(true);
let node_name = node.name.clone();
self.nodes_by_name.insert(node_name, node.clone());
self.nodes_to_watch.write().await.push(node);
}
pub(crate) fn add_para(&mut self, para: Parachain) {
self.parachains.entry(para.para_id).or_default().push(para);
}
pub fn name(&self) -> &str {
self.ns.name()
}
/// Get a first parachain from the list of the parachains with specified id.
/// NOTE!
/// Usually the list will contain only one parachain.
/// Multiple parachains with the same id is a corner case.
/// If this is the case then one can get such parachain with
/// `parachain_by_unique_id()` method
///
/// # Arguments
/// * `para_id` - Parachain Id
pub fn parachain(&self, para_id: u32) -> Option<&Parachain> {
self.parachains.get(&para_id)?.first()
}
/// Get a parachain by its unique id.
///
/// This is particularly useful if there are multiple parachains
/// with the same id (this is a rare corner case).
///
/// # Arguments
/// * `unique_id` - unique id of the parachain
pub fn parachain_by_unique_id(&self, unique_id: impl AsRef<str>) -> Option<&Parachain> {
self.parachains
.values()
.flat_map(|p| p.iter())
.find(|p| p.unique_id == unique_id.as_ref())
}
pub fn parachains(&self) -> Vec<&Parachain> {
self.parachains.values().flatten().collect()
}
pub(crate) fn nodes_iter(&self) -> impl Iterator<Item = &NetworkNode> {
self.relay.nodes.iter().chain(
self.parachains
.values()
.flat_map(|p| p.iter())
.flat_map(|p| &p.collators),
)
}
pub(crate) fn nodes_iter_mut(&mut self) -> impl Iterator<Item = &mut NetworkNode> {
self.relay.nodes.iter_mut().chain(
self.parachains
.values_mut()
.flat_map(|p| p.iter_mut())
.flat_map(|p| &mut p.collators),
)
}
/// Waits given number of seconds until all nodes in the network report that they are
/// up and running.
///
/// # Arguments
/// * `timeout_secs` - The number of seconds to wait.
///
/// # Returns
/// * `Ok()` if the node is up before timeout occured.
/// * `Err(e)` if timeout or other error occurred while waiting.
pub async fn wait_until_is_up(&self, timeout_secs: u64) -> Result<(), anyhow::Error> {
let handles = self
.nodes_iter()
.map(|node| node.wait_until_is_up(timeout_secs));
futures::future::try_join_all(handles).await?;
Ok(())
}
pub(crate) fn spawn_watching_task(&self) {
let nodes_to_watch = Arc::clone(&self.nodes_to_watch);
let ns = Arc::clone(&self.ns);
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(NODE_MONITORING_INTERVAL_SECONDS)).await;
let all_running = {
let guard = nodes_to_watch.read().await;
let nodes = guard.iter().filter(|n| n.is_running()).collect::<Vec<_>>();
let all_running =
futures::future::try_join_all(nodes.iter().map(|n| {
n.wait_until_is_up(NODE_MONITORING_FAILURE_THRESHOLD_SECONDS)
}))
.await;
// Re-check `is_running` to make sure we don't kill the network unnecessarily
if nodes.iter().any(|n| !n.is_running()) {
continue;
} else {
all_running
}
};
if let Err(e) = all_running {
warn!("\n\t🧟 One of the nodes crashed: {e}. tearing the network down...");
if let Err(e) = ns.destroy().await {
error!("an error occurred during network teardown: {}", e);
}
std::process::exit(1);
}
}
});
}
pub(crate) fn set_parachains(&mut self, parachains: HashMap<u32, Vec<Parachain>>) {
self.parachains = parachains;
}
pub(crate) fn insert_node(&mut self, node: NetworkNode) {
self.nodes_by_name.insert(node.name.clone(), node);
}
}
@@ -0,0 +1,41 @@
use std::str::FromStr;
use anyhow::anyhow;
use async_trait::async_trait;
use pezkuwi_subxt_signer::{sr25519::Keypair, SecretUri};
use super::node::NetworkNode;
use crate::{shared::types::RuntimeUpgradeOptions, tx_helper};
#[async_trait]
pub trait ChainUpgrade {
/// Perform a runtime upgrade (with sudo)
///
/// This call 'System.set_code_without_checks' wrapped in
/// 'Sudo.sudo_unchecked_weight'
async fn runtime_upgrade(&self, options: RuntimeUpgradeOptions) -> Result<(), anyhow::Error>;
/// Perform a runtime upgrade (with sudo), inner call with the node pass as arg.
///
/// This call 'System.set_code_without_checks' wrapped in
/// 'Sudo.sudo_unchecked_weight'
async fn perform_runtime_upgrade(
&self,
node: &NetworkNode,
options: RuntimeUpgradeOptions,
) -> Result<(), anyhow::Error> {
let sudo = if let Some(possible_seed) = options.seed {
Keypair::from_secret_key(possible_seed)
.map_err(|_| anyhow!("seed should return a Keypair"))?
} else {
let uri = SecretUri::from_str("//Alice")?;
Keypair::from_uri(&uri).map_err(|_| anyhow!("'//Alice' should return a Keypair"))?
};
let wasm_data = options.wasm.get_asset().await?;
tx_helper::runtime_upgrade::upgrade(node, &wasm_data, &sudo).await?;
Ok(())
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,75 @@
use std::path::PathBuf;
use anyhow::anyhow;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use super::node::NetworkNode;
use crate::{
network::chain_upgrade::ChainUpgrade, shared::types::RuntimeUpgradeOptions,
utils::default_as_empty_vec,
};
#[derive(Debug, Serialize, Deserialize)]
pub struct Relaychain {
pub(crate) chain: String,
pub(crate) chain_id: String,
pub(crate) chain_spec_path: PathBuf,
#[serde(default, deserialize_with = "default_as_empty_vec")]
pub(crate) nodes: Vec<NetworkNode>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct RawRelaychain {
#[serde(flatten)]
pub(crate) inner: Relaychain,
pub(crate) nodes: serde_json::Value,
}
#[async_trait]
impl ChainUpgrade for Relaychain {
async fn runtime_upgrade(&self, options: RuntimeUpgradeOptions) -> Result<(), anyhow::Error> {
// check if the node is valid first
let node = if let Some(node_name) = &options.node_name {
if let Some(node) = self
.nodes()
.into_iter()
.find(|node| node.name() == node_name)
{
node
} else {
return Err(anyhow!("Node: {node_name} is not part of the set of nodes"));
}
} else {
// take the first node
if let Some(node) = self.nodes().first() {
node
} else {
return Err(anyhow!("chain doesn't have any node!"));
}
};
self.perform_runtime_upgrade(node, options).await
}
}
impl Relaychain {
pub(crate) fn new(chain: String, chain_id: String, chain_spec_path: PathBuf) -> Self {
Self {
chain,
chain_id,
chain_spec_path,
nodes: Default::default(),
}
}
// Public API
pub fn nodes(&self) -> Vec<&NetworkNode> {
self.nodes.iter().collect()
}
/// Get chain name
pub fn chain(&self) -> &str {
&self.chain
}
}
@@ -0,0 +1,330 @@
use std::{
path::{Path, PathBuf},
str::FromStr,
};
use anyhow::anyhow;
use async_trait::async_trait;
use pezkuwi_subxt::{dynamic::Value, tx::TxStatus, BizinikiwConfig, OnlineClient};
use pezkuwi_subxt_signer::{sr25519::Keypair, SecretUri};
use provider::types::TransferedFile;
use serde::{Deserialize, Serialize};
use support::{constants::THIS_IS_A_BUG, fs::FileSystem, net::wait_ws_ready};
use tracing::info;
use super::{chain_upgrade::ChainUpgrade, node::NetworkNode};
use crate::{
network_spec::teyrchain::TeyrchainSpec,
shared::types::{RegisterParachainOptions, RuntimeUpgradeOptions},
tx_helper::client::get_client_from_url,
utils::default_as_empty_vec,
ScopedFilesystem,
};
#[derive(Debug, Serialize, Deserialize)]
pub struct Parachain {
pub(crate) chain: Option<String>,
pub(crate) para_id: u32,
// unique_id is internally used to allow multiple parachains with the same id
// See `ParachainConfig` for more details
pub(crate) unique_id: String,
pub(crate) chain_id: Option<String>,
pub(crate) chain_spec_path: Option<PathBuf>,
#[serde(default, deserialize_with = "default_as_empty_vec")]
pub(crate) collators: Vec<NetworkNode>,
pub(crate) files_to_inject: Vec<TransferedFile>,
pub(crate) bootnodes_addresses: Vec<multiaddr::Multiaddr>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct RawParachain {
#[serde(flatten)]
pub(crate) inner: Parachain,
pub(crate) collators: serde_json::Value,
}
#[async_trait]
impl ChainUpgrade for Parachain {
async fn runtime_upgrade(&self, options: RuntimeUpgradeOptions) -> Result<(), anyhow::Error> {
// check if the node is valid first
let node = if let Some(node_name) = &options.node_name {
if let Some(node) = self
.collators()
.into_iter()
.find(|node| node.name() == node_name)
{
node
} else {
return Err(anyhow!("Node: {node_name} is not part of the set of nodes"));
}
} else {
// take the first node
if let Some(node) = self.collators().first() {
node
} else {
return Err(anyhow!("chain doesn't have any node!"));
}
};
self.perform_runtime_upgrade(node, options).await
}
}
impl Parachain {
pub(crate) fn new(para_id: u32, unique_id: impl Into<String>) -> Self {
Self {
chain: None,
para_id,
unique_id: unique_id.into(),
chain_id: None,
chain_spec_path: None,
collators: Default::default(),
files_to_inject: Default::default(),
bootnodes_addresses: vec![],
}
}
pub(crate) fn with_chain_spec(
para_id: u32,
unique_id: impl Into<String>,
chain_id: impl Into<String>,
chain_spec_path: impl AsRef<Path>,
) -> Self {
Self {
para_id,
unique_id: unique_id.into(),
chain: None,
chain_id: Some(chain_id.into()),
chain_spec_path: Some(chain_spec_path.as_ref().into()),
collators: Default::default(),
files_to_inject: Default::default(),
bootnodes_addresses: vec![],
}
}
pub(crate) async fn from_spec(
para: &TeyrchainSpec,
files_to_inject: &[TransferedFile],
scoped_fs: &ScopedFilesystem<'_, impl FileSystem>,
) -> Result<Self, anyhow::Error> {
let mut para_files_to_inject = files_to_inject.to_owned();
// parachain id is used for the keystore
let mut parachain = if let Some(chain_spec) = para.chain_spec.as_ref() {
let id = chain_spec.read_chain_id(scoped_fs).await?;
// add the spec to global files to inject
let spec_name = chain_spec.chain_spec_name();
let base = PathBuf::from_str(scoped_fs.base_dir)?;
para_files_to_inject.push(TransferedFile::new(
base.join(format!("{spec_name}.json")),
PathBuf::from(format!("/cfg/{}.json", para.id)),
));
let raw_path = chain_spec
.raw_path()
.ok_or(anyhow::anyhow!("chain-spec path should be set by now.",))?;
let mut running_para =
Parachain::with_chain_spec(para.id, &para.unique_id, id, raw_path);
if let Some(chain_name) = chain_spec.chain_name() {
running_para.chain = Some(chain_name.to_string());
}
running_para
} else {
Parachain::new(para.id, &para.unique_id)
};
parachain.bootnodes_addresses = para.bootnodes_addresses().into_iter().cloned().collect();
parachain.files_to_inject = para_files_to_inject;
Ok(parachain)
}
pub async fn register(
options: RegisterParachainOptions,
scoped_fs: &ScopedFilesystem<'_, impl FileSystem>,
) -> Result<(), anyhow::Error> {
info!("Registering parachain: {:?}", options);
// get the seed
let sudo: Keypair;
if let Some(possible_seed) = options.seed {
sudo = Keypair::from_secret_key(possible_seed)
.expect(&format!("seed should return a Keypair {THIS_IS_A_BUG}"));
} else {
let uri = SecretUri::from_str("//Alice")?;
sudo = Keypair::from_uri(&uri)?;
}
let genesis_state = scoped_fs
.read_to_string(options.state_path)
.await
.expect(&format!(
"State Path should be ok by this point {THIS_IS_A_BUG}"
));
let wasm_data = scoped_fs
.read_to_string(options.wasm_path)
.await
.expect(&format!(
"Wasm Path should be ok by this point {THIS_IS_A_BUG}"
));
wait_ws_ready(options.node_ws_url.as_str())
.await
.map_err(|_| {
anyhow::anyhow!(
"Error waiting for ws to be ready, at {}",
options.node_ws_url.as_str()
)
})?;
let api: OnlineClient<BizinikiwConfig> = get_client_from_url(&options.node_ws_url).await?;
let schedule_para = pezkuwi_subxt::dynamic::tx(
"ParasSudoWrapper",
"sudo_schedule_para_initialize",
vec![
Value::primitive(options.id.into()),
Value::named_composite([
(
"genesis_head",
Value::from_bytes(hex::decode(&genesis_state[2..])?),
),
(
"validation_code",
Value::from_bytes(hex::decode(&wasm_data[2..])?),
),
("para_kind", Value::bool(options.onboard_as_para)),
]),
],
);
let sudo_call =
pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![schedule_para.into_value()]);
// TODO: uncomment below and fix the sign and submit (and follow afterwards until
// finalized block) to register the parachain
let mut tx = api
.tx()
.sign_and_submit_then_watch_default(&sudo_call, &sudo)
.await?;
// Below we use the low level API to replicate the `wait_for_in_block` behaviour
// which was removed in subxt 0.33.0. See https://github.com/paritytech/subxt/pull/1237.
while let Some(status) = tx.next().await {
match status? {
TxStatus::InBestBlock(tx_in_block) | TxStatus::InFinalizedBlock(tx_in_block) => {
let _result = tx_in_block.wait_for_success().await?;
info!("In block: {:#?}", tx_in_block.block_hash());
},
TxStatus::Error { message }
| TxStatus::Invalid { message }
| TxStatus::Dropped { message } => {
return Err(anyhow::format_err!("Error submitting tx: {message}"));
},
_ => continue,
}
}
Ok(())
}
pub fn para_id(&self) -> u32 {
self.para_id
}
pub fn unique_id(&self) -> &str {
self.unique_id.as_str()
}
pub fn chain_id(&self) -> Option<&str> {
self.chain_id.as_deref()
}
pub fn collators(&self) -> Vec<&NetworkNode> {
self.collators.iter().collect()
}
pub fn bootnodes_addresses(&self) -> Vec<&multiaddr::Multiaddr> {
self.bootnodes_addresses.iter().collect()
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
#[test]
fn create_with_is_works() {
let para = Parachain::new(100, "100");
// only para_id and unique_id should be set
assert_eq!(para.para_id, 100);
assert_eq!(para.unique_id, "100");
assert_eq!(para.chain_id, None);
assert_eq!(para.chain, None);
assert_eq!(para.chain_spec_path, None);
}
#[test]
fn create_with_chain_spec_works() {
let para = Parachain::with_chain_spec(100, "100", "rococo-local", "/tmp/rococo-local.json");
assert_eq!(para.para_id, 100);
assert_eq!(para.unique_id, "100");
assert_eq!(para.chain_id, Some("rococo-local".to_string()));
assert_eq!(para.chain, None);
assert_eq!(
para.chain_spec_path,
Some(PathBuf::from("/tmp/rococo-local.json"))
);
}
#[tokio::test]
async fn create_with_para_spec_works() {
use configuration::ParachainConfigBuilder;
use crate::network_spec::teyrchain::TeyrchainSpec;
let bootnode_addresses = vec!["/ip4/10.41.122.55/tcp/45421"];
let para_config = ParachainConfigBuilder::new(Default::default())
.with_id(100)
.cumulus_based(false)
.with_default_command("adder-collator")
.with_raw_bootnodes_addresses(bootnode_addresses.clone())
.with_collator(|c| c.with_name("col"))
.build()
.unwrap();
let para_spec =
TeyrchainSpec::from_config(&para_config, "rococo-local".try_into().unwrap()).unwrap();
let fs = support::fs::in_memory::InMemoryFileSystem::new(HashMap::default());
let scoped_fs = ScopedFilesystem {
fs: &fs,
base_dir: "/tmp/some",
};
let files = vec![TransferedFile::new(
PathBuf::from("/tmp/some"),
PathBuf::from("/tmp/some"),
)];
let para = Parachain::from_spec(&para_spec, &files, &scoped_fs)
.await
.unwrap();
println!("{para:#?}");
assert_eq!(para.para_id, 100);
assert_eq!(para.unique_id, "100");
assert_eq!(para.chain_id, None);
assert_eq!(para.chain, None);
// one file should be added.
assert_eq!(para.files_to_inject.len(), 1);
assert_eq!(
para.bootnodes_addresses()
.iter()
.map(|addr| addr.to_string())
.collect::<Vec<_>>(),
bootnode_addresses
);
}
}
@@ -0,0 +1,2 @@
pub mod metrics;
pub mod verifier;
@@ -0,0 +1,62 @@
use std::collections::HashMap;
use async_trait::async_trait;
use reqwest::Url;
#[async_trait]
pub trait MetricsHelper {
async fn metric(&self, metric_name: &str) -> Result<f64, anyhow::Error>;
async fn metric_with_url(
metric: impl AsRef<str> + Send,
endpoint: impl Into<Url> + Send,
) -> Result<f64, anyhow::Error>;
}
pub struct Metrics {
endpoint: Url,
}
impl Metrics {
fn new(endpoint: impl Into<Url>) -> Self {
Self {
endpoint: endpoint.into(),
}
}
async fn fetch_metrics(
endpoint: impl AsRef<str>,
) -> Result<HashMap<String, f64>, anyhow::Error> {
let response = reqwest::get(endpoint.as_ref()).await?;
Ok(prom_metrics_parser::parse(&response.text().await?)?)
}
fn get_metric(
metrics_map: HashMap<String, f64>,
metric_name: &str,
) -> Result<f64, anyhow::Error> {
let treat_not_found_as_zero = true;
if let Some(val) = metrics_map.get(metric_name) {
Ok(*val)
} else if treat_not_found_as_zero {
Ok(0_f64)
} else {
Err(anyhow::anyhow!("MetricNotFound: {metric_name}"))
}
}
}
#[async_trait]
impl MetricsHelper for Metrics {
async fn metric(&self, metric_name: &str) -> Result<f64, anyhow::Error> {
let metrics_map = Metrics::fetch_metrics(self.endpoint.as_str()).await?;
Metrics::get_metric(metrics_map, metric_name)
}
async fn metric_with_url(
metric_name: impl AsRef<str> + Send,
endpoint: impl Into<Url> + Send,
) -> Result<f64, anyhow::Error> {
let metrics_map = Metrics::fetch_metrics(endpoint.into()).await?;
Metrics::get_metric(metrics_map, metric_name.as_ref())
}
}
@@ -0,0 +1,34 @@
use std::time::Duration;
use tokio::time::timeout;
use tracing::trace;
use crate::network::node::NetworkNode;
pub(crate) async fn verify_nodes(nodes: &[&NetworkNode]) -> Result<(), anyhow::Error> {
timeout(Duration::from_secs(90), check_nodes(nodes))
.await
.map_err(|_| anyhow::anyhow!("one or more nodes are not ready!"))
}
// TODO: we should inject in someway the logic to make the request
// in order to allow us to `mock` and easily test this.
// maybe moved to the provider with a NodeStatus, and some helpers like wait_running, wait_ready, etc... ? to be discussed
async fn check_nodes(nodes: &[&NetworkNode]) {
loop {
let tasks: Vec<_> = nodes
.iter()
.map(|node| {
trace!("🔎 checking node: {} ", node.name);
reqwest::get(node.prometheus_uri.clone())
})
.collect();
let all_ready = futures::future::try_join_all(tasks).await;
if all_ready.is_ok() {
return;
}
tokio::time::sleep(Duration::from_millis(1000)).await;
}
}
@@ -0,0 +1,330 @@
use std::{
collections::{hash_map::Entry, HashMap},
sync::Arc,
};
use configuration::{GlobalSettings, HrmpChannelConfig, NetworkConfig};
use futures::future::try_join_all;
use provider::{DynNamespace, ProviderError, ProviderNamespace};
use serde::{Deserialize, Serialize};
use support::{constants::THIS_IS_A_BUG, fs::FileSystem};
use tracing::{debug, trace};
use crate::{errors::OrchestratorError, ScopedFilesystem};
pub mod node;
pub mod relaychain;
pub mod teyrchain;
use self::{node::NodeSpec, relaychain::RelaychainSpec, teyrchain::TeyrchainSpec};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkSpec {
/// Relaychain configuration.
pub(crate) relaychain: RelaychainSpec,
/// Parachains configurations.
pub(crate) parachains: Vec<TeyrchainSpec>,
/// HRMP channels configurations.
pub(crate) hrmp_channels: Vec<HrmpChannelConfig>,
/// Global settings
pub(crate) global_settings: GlobalSettings,
}
impl NetworkSpec {
pub async fn from_config(
network_config: &NetworkConfig,
) -> Result<NetworkSpec, OrchestratorError> {
let mut errs = vec![];
let relaychain = RelaychainSpec::from_config(network_config.relaychain())?;
let mut parachains = vec![];
// TODO: move to `fold` or map+fold
for para_config in network_config.parachains() {
match TeyrchainSpec::from_config(para_config, relaychain.chain.clone()) {
Ok(para) => parachains.push(para),
Err(err) => errs.push(err),
}
}
if errs.is_empty() {
Ok(NetworkSpec {
relaychain,
parachains,
hrmp_channels: network_config
.hrmp_channels()
.into_iter()
.cloned()
.collect(),
global_settings: network_config.global_settings().clone(),
})
} else {
let errs_str = errs
.into_iter()
.map(|e| e.to_string())
.collect::<Vec<String>>()
.join("\n");
Err(OrchestratorError::InvalidConfig(errs_str))
}
}
pub async fn populate_nodes_available_args(
&mut self,
ns: Arc<dyn ProviderNamespace + Send + Sync>,
) -> Result<(), OrchestratorError> {
let network_nodes = self.collect_network_nodes();
let mut image_command_to_nodes_mapping =
Self::create_image_command_to_nodes_mapping(network_nodes);
let available_args_outputs =
Self::retrieve_all_nodes_available_args_output(ns, &image_command_to_nodes_mapping)
.await?;
Self::update_nodes_available_args_output(
&mut image_command_to_nodes_mapping,
available_args_outputs,
);
Ok(())
}
//
pub async fn node_available_args_output(
&self,
node_spec: &NodeSpec,
ns: Arc<dyn ProviderNamespace + Send + Sync>,
) -> Result<String, ProviderError> {
// try to find a node that use the same combination of image/cmd
let cmp_fn = |ad_hoc: &&NodeSpec| -> bool {
ad_hoc.image == node_spec.image && ad_hoc.command == node_spec.command
};
// check if we already had computed the args output for this cmd/[image]
let node = self.relaychain.nodes.iter().find(cmp_fn);
let node = if let Some(node) = node {
Some(node)
} else {
let node = self
.parachains
.iter()
.find_map(|para| para.collators.iter().find(cmp_fn));
node
};
let output = if let Some(node) = node {
node.available_args_output.clone().expect(&format!(
"args_output should be set for running nodes {THIS_IS_A_BUG}"
))
} else {
// we need to compute the args output
let image = node_spec
.image
.as_ref()
.map(|image| image.as_str().to_string());
let command = node_spec.command.as_str().to_string();
ns.get_node_available_args((command, image)).await?
};
Ok(output)
}
pub fn relaychain(&self) -> &RelaychainSpec {
&self.relaychain
}
pub fn relaychain_mut(&mut self) -> &mut RelaychainSpec {
&mut self.relaychain
}
pub fn parachains_iter(&self) -> impl Iterator<Item = &TeyrchainSpec> {
self.parachains.iter()
}
pub fn parachains_iter_mut(&mut self) -> impl Iterator<Item = &mut TeyrchainSpec> {
self.parachains.iter_mut()
}
pub fn set_global_settings(&mut self, global_settings: GlobalSettings) {
self.global_settings = global_settings;
}
pub async fn build_parachain_artifacts<'a, T: FileSystem>(
&mut self,
ns: DynNamespace,
scoped_fs: &ScopedFilesystem<'a, T>,
relaychain_id: &str,
base_dir_exists: bool,
) -> Result<(), anyhow::Error> {
for para in self.parachains.iter_mut() {
let chain_spec_raw_path = para.build_chain_spec(relaychain_id, &ns, scoped_fs).await?;
trace!("creating dirs for {}", &para.unique_id);
if base_dir_exists {
scoped_fs.create_dir_all(&para.unique_id).await?;
} else {
scoped_fs.create_dir(&para.unique_id).await?;
};
trace!("created dirs for {}", &para.unique_id);
// create wasm/state
para.genesis_state
.build(
chain_spec_raw_path.clone(),
format!("{}/genesis-state", para.unique_id),
&ns,
scoped_fs,
None,
)
.await?;
debug!("parachain genesis state built!");
para.genesis_wasm
.build(
chain_spec_raw_path,
format!("{}/genesis-wasm", para.unique_id),
&ns,
scoped_fs,
None,
)
.await?;
debug!("parachain genesis wasm built!");
}
Ok(())
}
// collect mutable references to all nodes from relaychain and parachains
fn collect_network_nodes(&mut self) -> Vec<&mut NodeSpec> {
vec![
self.relaychain.nodes.iter_mut().collect::<Vec<_>>(),
self.parachains
.iter_mut()
.flat_map(|para| para.collators.iter_mut())
.collect(),
]
.into_iter()
.flatten()
.collect::<Vec<_>>()
}
// initialize the mapping of all possible node image/commands to corresponding nodes
fn create_image_command_to_nodes_mapping(
network_nodes: Vec<&mut NodeSpec>,
) -> HashMap<(Option<String>, String), Vec<&mut NodeSpec>> {
network_nodes.into_iter().fold(
HashMap::new(),
|mut acc: HashMap<(Option<String>, String), Vec<&mut node::NodeSpec>>, node| {
// build mapping key using image and command if image is present or command only
let key = node
.image
.as_ref()
.map(|image| {
(
Some(image.as_str().to_string()),
node.command.as_str().to_string(),
)
})
.unwrap_or_else(|| (None, node.command.as_str().to_string()));
// append the node to the vector of nodes for this image/command tuple
if let Entry::Vacant(entry) = acc.entry(key.clone()) {
entry.insert(vec![node]);
} else {
acc.get_mut(&key).unwrap().push(node);
}
acc
},
)
}
async fn retrieve_all_nodes_available_args_output(
ns: Arc<dyn ProviderNamespace + Send + Sync>,
image_command_to_nodes_mapping: &HashMap<(Option<String>, String), Vec<&mut NodeSpec>>,
) -> Result<Vec<(Option<String>, String, String)>, OrchestratorError> {
try_join_all(
image_command_to_nodes_mapping
.keys()
.map(|(image, command)| {
let ns = ns.clone();
let image = image.clone();
let command = command.clone();
async move {
// get node available args output from image/command
let available_args = ns
.get_node_available_args((command.clone(), image.clone()))
.await?;
debug!(
"retrieved available args for image: {:?}, command: {}",
image, command
);
// map the result to include image and command
Ok::<_, OrchestratorError>((image, command, available_args))
}
})
.collect::<Vec<_>>(),
)
.await
}
fn update_nodes_available_args_output(
image_command_to_nodes_mapping: &mut HashMap<(Option<String>, String), Vec<&mut NodeSpec>>,
available_args_outputs: Vec<(Option<String>, String, String)>,
) {
for (image, command, available_args_output) in available_args_outputs {
let nodes = image_command_to_nodes_mapping
.get_mut(&(image, command))
.expect(&format!(
"node image/command key should exist {THIS_IS_A_BUG}"
));
for node in nodes {
node.available_args_output = Some(available_args_output.clone());
}
}
}
}
#[cfg(test)]
mod tests {
#[tokio::test]
async fn small_network_config_get_spec() {
use configuration::NetworkConfigBuilder;
use super::*;
let config = NetworkConfigBuilder::new()
.with_relaychain(|r| {
r.with_chain("rococo-local")
.with_default_command("polkadot")
.with_validator(|node| node.with_name("alice"))
.with_fullnode(|node| node.with_name("bob").with_command("polkadot1"))
})
.with_parachain(|p| {
p.with_id(100)
.with_default_command("adder-collator")
.with_collator(|c| c.with_name("collator1"))
})
.build()
.unwrap();
let network_spec = NetworkSpec::from_config(&config).await.unwrap();
let alice = network_spec.relaychain.nodes.first().unwrap();
let bob = network_spec.relaychain.nodes.get(1).unwrap();
assert_eq!(alice.command.as_str(), "polkadot");
assert_eq!(bob.command.as_str(), "polkadot1");
assert!(alice.is_validator);
assert!(!bob.is_validator);
// paras
assert_eq!(network_spec.parachains.len(), 1);
let para_100 = network_spec.parachains.first().unwrap();
assert_eq!(para_100.id, 100);
}
}
@@ -0,0 +1,356 @@
use std::path::PathBuf;
use configuration::shared::{
node::{EnvVar, NodeConfig},
resources::Resources,
types::{Arg, AssetLocation, Command, Image},
};
use multiaddr::Multiaddr;
use provider::types::Port;
use serde::{Deserialize, Serialize};
use support::constants::THIS_IS_A_BUG;
use crate::{
errors::OrchestratorError,
generators,
network::AddNodeOptions,
shared::{
macros,
types::{ChainDefaultContext, NodeAccount, NodeAccounts, ParkedPort},
},
AddCollatorOptions,
};
macros::create_add_options!(AddNodeSpecOpts {
override_eth_key: Option<String>
});
macro_rules! impl_from_for_add_node_opts {
($struct:ident) => {
impl From<$struct> for AddNodeSpecOpts {
fn from(value: $struct) -> Self {
Self {
image: value.image,
command: value.command,
subcommand: value.subcommand,
args: value.args,
env: value.env,
is_validator: value.is_validator,
rpc_port: value.rpc_port,
prometheus_port: value.prometheus_port,
p2p_port: value.p2p_port,
override_eth_key: value.override_eth_key,
}
}
}
};
}
impl_from_for_add_node_opts!(AddNodeOptions);
impl_from_for_add_node_opts!(AddCollatorOptions);
/// A node configuration, with fine-grained configuration options.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NodeSpec {
// Node name (should be unique or an index will be appended).
pub(crate) name: String,
/// Node key, used for compute the p2p identity.
pub(crate) key: String,
// libp2p local identity
pub(crate) peer_id: String,
/// Accounts to be injected in the keystore.
pub(crate) accounts: NodeAccounts,
/// Image to run (only podman/k8s). Override the default.
pub(crate) image: Option<Image>,
/// Command to run the node. Override the default.
pub(crate) command: Command,
/// Optional subcommand for the node.
pub(crate) subcommand: Option<Command>,
/// Arguments to use for node. Appended to default.
pub(crate) args: Vec<Arg>,
// The help command output containing the available arguments.
pub(crate) available_args_output: Option<String>,
/// Wether the node is a validator.
pub(crate) is_validator: bool,
/// Whether the node keys must be added to invulnerables.
pub(crate) is_invulnerable: bool,
/// Whether the node is a bootnode.
pub(crate) is_bootnode: bool,
/// Node initial balance present in genesis.
pub(crate) initial_balance: u128,
/// Environment variables to set (inside pod for podman/k8s, inside shell for native).
pub(crate) env: Vec<EnvVar>,
/// List of node's bootnodes addresses to use. Appended to default.
pub(crate) bootnodes_addresses: Vec<Multiaddr>,
/// Default resources. Override the default.
pub(crate) resources: Option<Resources>,
/// Websocket port to use.
pub(crate) ws_port: ParkedPort,
/// RPC port to use.
pub(crate) rpc_port: ParkedPort,
/// Prometheus port to use.
pub(crate) prometheus_port: ParkedPort,
/// P2P port to use.
pub(crate) p2p_port: ParkedPort,
/// libp2p cert hash to use with `webrtc` transport.
pub(crate) p2p_cert_hash: Option<String>,
/// Database snapshot. Override the default.
pub(crate) db_snapshot: Option<AssetLocation>,
/// P2P port to use by full node if this is the case
pub(crate) full_node_p2p_port: Option<ParkedPort>,
/// Prometheus port to use by full node if this is the case
pub(crate) full_node_prometheus_port: Option<ParkedPort>,
/// Optionally specify a log path for the node
pub(crate) node_log_path: Option<PathBuf>,
/// Optionally specify a keystore path for the node
pub(crate) keystore_path: Option<PathBuf>,
/// Keystore key types to generate.
/// Supports short form (e.g., "audi") using predefined schemas,
/// or long form (e.g., "audi_sr") with explicit schema (sr, ed, ec).
pub(crate) keystore_key_types: Vec<String>,
}
impl NodeSpec {
pub fn from_config(
node_config: &NodeConfig,
chain_context: &ChainDefaultContext,
full_node_present: bool,
evm_based: bool,
) -> Result<Self, OrchestratorError> {
// Check first if the image is set at node level, then try with the default
let image = node_config.image().or(chain_context.default_image).cloned();
// Check first if the command is set at node level, then try with the default
let command = if let Some(cmd) = node_config.command() {
cmd.clone()
} else if let Some(cmd) = chain_context.default_command {
cmd.clone()
} else {
return Err(OrchestratorError::InvalidNodeConfig(
node_config.name().into(),
"command".to_string(),
));
};
let subcommand = node_config.subcommand().cloned();
// If `args` is set at `node` level use them
// otherwise use the default_args (can be empty).
let args: Vec<Arg> = if node_config.args().is_empty() {
chain_context
.default_args
.iter()
.map(|x| x.to_owned().clone())
.collect()
} else {
node_config.args().into_iter().cloned().collect()
};
let (key, peer_id) = generators::generate_node_identity(node_config.name())?;
let mut name = node_config.name().to_string();
let seed = format!("//{}{name}", name.remove(0).to_uppercase());
let accounts = generators::generate_node_keys(&seed)?;
let mut accounts = NodeAccounts { seed, accounts };
if evm_based {
if let Some(session_key) = node_config.override_eth_key() {
accounts
.accounts
.insert("eth".into(), NodeAccount::new(session_key, session_key));
}
}
let db_snapshot = match (node_config.db_snapshot(), chain_context.default_db_snapshot) {
(Some(db_snapshot), _) => Some(db_snapshot),
(None, Some(db_snapshot)) => Some(db_snapshot),
_ => None,
};
let (full_node_p2p_port, full_node_prometheus_port) = if full_node_present {
(
Some(generators::generate_node_port(None)?),
Some(generators::generate_node_port(None)?),
)
} else {
(None, None)
};
Ok(Self {
name: node_config.name().to_string(),
key,
peer_id,
image,
command,
subcommand,
args,
available_args_output: None,
is_validator: node_config.is_validator(),
is_invulnerable: node_config.is_invulnerable(),
is_bootnode: node_config.is_bootnode(),
initial_balance: node_config.initial_balance(),
env: node_config.env().into_iter().cloned().collect(),
bootnodes_addresses: node_config
.bootnodes_addresses()
.into_iter()
.cloned()
.collect(),
resources: node_config.resources().cloned(),
p2p_cert_hash: node_config.p2p_cert_hash().map(str::to_string),
db_snapshot: db_snapshot.cloned(),
accounts,
ws_port: generators::generate_node_port(node_config.ws_port())?,
rpc_port: generators::generate_node_port(node_config.rpc_port())?,
prometheus_port: generators::generate_node_port(node_config.prometheus_port())?,
p2p_port: generators::generate_node_port(node_config.p2p_port())?,
full_node_p2p_port,
full_node_prometheus_port,
node_log_path: node_config.node_log_path().cloned(),
keystore_path: node_config.keystore_path().cloned(),
keystore_key_types: node_config
.keystore_key_types()
.into_iter()
.map(str::to_string)
.collect(),
})
}
pub fn from_ad_hoc(
name: impl Into<String>,
options: AddNodeSpecOpts,
chain_context: &ChainDefaultContext,
full_node_present: bool,
evm_based: bool,
) -> Result<Self, OrchestratorError> {
// Check first if the image is set at node level, then try with the default
let image = if let Some(img) = options.image {
Some(img.clone())
} else {
chain_context.default_image.cloned()
};
let name = name.into();
// Check first if the command is set at node level, then try with the default
let command = if let Some(cmd) = options.command {
cmd.clone()
} else if let Some(cmd) = chain_context.default_command {
cmd.clone()
} else {
return Err(OrchestratorError::InvalidNodeConfig(
name,
"command".to_string(),
));
};
let subcommand = options.subcommand.clone();
// If `args` is set at `node` level use them
// otherwise use the default_args (can be empty).
let args: Vec<Arg> = if options.args.is_empty() {
chain_context
.default_args
.iter()
.map(|x| x.to_owned().clone())
.collect()
} else {
options.args
};
let (key, peer_id) = generators::generate_node_identity(&name)?;
let mut name_capitalized = name.clone();
let seed = format!(
"//{}{name_capitalized}",
name_capitalized.remove(0).to_uppercase()
);
let accounts = generators::generate_node_keys(&seed)?;
let mut accounts = NodeAccounts { seed, accounts };
if evm_based {
if let Some(session_key) = options.override_eth_key.as_ref() {
accounts
.accounts
.insert("eth".into(), NodeAccount::new(session_key, session_key));
}
}
let (full_node_p2p_port, full_node_prometheus_port) = if full_node_present {
(
Some(generators::generate_node_port(None)?),
Some(generators::generate_node_port(None)?),
)
} else {
(None, None)
};
//
Ok(Self {
name,
key,
peer_id,
image,
command,
subcommand,
args,
available_args_output: None,
is_validator: options.is_validator,
is_invulnerable: false,
is_bootnode: false,
initial_balance: 0,
env: options.env,
bootnodes_addresses: vec![],
resources: None,
p2p_cert_hash: None,
db_snapshot: None,
accounts,
// should be deprecated now!
ws_port: generators::generate_node_port(None)?,
rpc_port: generators::generate_node_port(options.rpc_port)?,
prometheus_port: generators::generate_node_port(options.prometheus_port)?,
p2p_port: generators::generate_node_port(options.p2p_port)?,
full_node_p2p_port,
full_node_prometheus_port,
node_log_path: None,
keystore_path: None,
keystore_key_types: vec![],
})
}
pub(crate) fn supports_arg(&self, arg: impl AsRef<str>) -> bool {
self.available_args_output
.as_ref()
.expect(&format!(
"available args should be present at this point {THIS_IS_A_BUG}"
))
.contains(arg.as_ref())
}
pub fn command(&self) -> &str {
self.command.as_str()
}
}
@@ -0,0 +1,181 @@
use std::collections::{HashMap, HashSet};
use configuration::{
shared::{
helpers::generate_unique_node_name_from_names,
resources::Resources,
types::{Arg, AssetLocation, Chain, Command, Image},
},
types::JsonOverrides,
NodeConfig, RelaychainConfig,
};
use serde::{Deserialize, Serialize};
use support::replacer::apply_replacements;
use super::node::NodeSpec;
use crate::{
errors::OrchestratorError,
generators::chain_spec::{ChainSpec, Context},
shared::{constants::DEFAULT_CHAIN_SPEC_TPL_COMMAND, types::ChainDefaultContext},
};
/// A relaychain configuration spec
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RelaychainSpec {
/// Chain to use (e.g. rococo-local).
pub(crate) chain: Chain,
/// Default command to run the node. Can be overridden on each node.
pub(crate) default_command: Option<Command>,
/// Default image to use (only podman/k8s). Can be overridden on each node.
pub(crate) default_image: Option<Image>,
/// Default resources. Can be overridden on each node.
pub(crate) default_resources: Option<Resources>,
/// Default database snapshot. Can be overridden on each node.
pub(crate) default_db_snapshot: Option<AssetLocation>,
/// Default arguments to use in nodes. Can be overridden on each node.
pub(crate) default_args: Vec<Arg>,
// chain_spec_path: Option<AssetLocation>,
pub(crate) chain_spec: ChainSpec,
/// Set the count of nominators to generator (used with PoS networks).
pub(crate) random_nominators_count: u32,
/// Set the max nominators value (used with PoS networks).
pub(crate) max_nominations: u8,
/// Genesis overrides as JSON value.
pub(crate) runtime_genesis_patch: Option<serde_json::Value>,
/// Wasm override path/url to use.
pub(crate) wasm_override: Option<AssetLocation>,
/// Nodes to run.
pub(crate) nodes: Vec<NodeSpec>,
/// Raw chain-spec override path, url or inline json to use.
pub(crate) raw_spec_override: Option<JsonOverrides>,
}
impl RelaychainSpec {
pub fn from_config(config: &RelaychainConfig) -> Result<RelaychainSpec, OrchestratorError> {
// Relaychain main command to use, in order:
// set as `default_command` or
// use the command of the first node.
// If non of those is set, return an error.
let main_cmd = config
.default_command()
.or(config.nodes().first().and_then(|node| node.command()))
.ok_or(OrchestratorError::InvalidConfig(
"Relaychain, either default_command or first node with a command needs to be set."
.to_string(),
))?;
// TODO: internally we use image as String
let main_image = config
.default_image()
.or(config.nodes().first().and_then(|node| node.image()))
.map(|image| image.as_str().to_string());
let replacements = HashMap::from([
("disableBootnodes", "--disable-default-bootnode"),
("mainCommand", main_cmd.as_str()),
]);
let tmpl = if let Some(tmpl) = config.chain_spec_command() {
apply_replacements(tmpl, &replacements)
} else {
apply_replacements(DEFAULT_CHAIN_SPEC_TPL_COMMAND, &replacements)
};
let chain_spec = ChainSpec::new(config.chain().as_str(), Context::Relay)
.set_chain_name(config.chain().as_str())
.command(
tmpl.as_str(),
config.chain_spec_command_is_local(),
config.chain_spec_command_output_path(),
)
.image(main_image.clone());
// Add asset location if present
let chain_spec = if let Some(chain_spec_path) = config.chain_spec_path() {
chain_spec.asset_location(chain_spec_path.clone())
} else {
chain_spec
};
// add chain-spec runtime if present
let chain_spec = if let Some(chain_spec_runtime) = config.chain_spec_runtime() {
chain_spec.runtime(chain_spec_runtime.clone())
} else {
chain_spec
};
// build the `node_specs`
let chain_context = ChainDefaultContext {
default_command: config.default_command(),
default_image: config.default_image(),
default_resources: config.default_resources(),
default_db_snapshot: config.default_db_snapshot(),
default_args: config.default_args(),
};
let mut nodes: Vec<NodeConfig> = config.nodes().into_iter().cloned().collect();
nodes.extend(
config
.group_node_configs()
.into_iter()
.flat_map(|node_group| node_group.expand_group_configs()),
);
let mut names = HashSet::new();
let (nodes, mut errs) = nodes
.iter()
.map(|node_config| NodeSpec::from_config(node_config, &chain_context, false, false))
.fold((vec![], vec![]), |(mut nodes, mut errs), result| {
match result {
Ok(mut node) => {
let unique_name =
generate_unique_node_name_from_names(node.name, &mut names);
node.name = unique_name;
nodes.push(node);
},
Err(err) => errs.push(err),
}
(nodes, errs)
});
if !errs.is_empty() {
// TODO: merge errs, maybe return something like Result<Sometype, Vec<OrchestratorError>>
return Err(errs.swap_remove(0));
}
Ok(RelaychainSpec {
chain: config.chain().clone(),
default_command: config.default_command().cloned(),
default_image: config.default_image().cloned(),
default_resources: config.default_resources().cloned(),
default_db_snapshot: config.default_db_snapshot().cloned(),
wasm_override: config.wasm_override().cloned(),
default_args: config.default_args().into_iter().cloned().collect(),
chain_spec,
random_nominators_count: config.random_nominators_count().unwrap_or(0),
max_nominations: config.max_nominations().unwrap_or(24),
runtime_genesis_patch: config.runtime_genesis_patch().cloned(),
nodes,
raw_spec_override: config.raw_spec_override().cloned(),
})
}
pub fn chain_spec(&self) -> &ChainSpec {
&self.chain_spec
}
pub fn chain_spec_mut(&mut self) -> &mut ChainSpec {
&mut self.chain_spec
}
}
@@ -0,0 +1,386 @@
use std::{
collections::{HashMap, HashSet},
path::PathBuf,
};
use configuration::{
shared::{helpers::generate_unique_node_name_from_names, resources::Resources},
types::{Arg, AssetLocation, Chain, Command, Image, JsonOverrides},
NodeConfig, ParachainConfig, RegistrationStrategy,
};
use provider::DynNamespace;
use serde::{Deserialize, Serialize};
use support::{fs::FileSystem, replacer::apply_replacements};
use tracing::debug;
use super::node::NodeSpec;
use crate::{
errors::OrchestratorError,
generators::{
chain_spec::{ChainSpec, Context, ParaGenesisConfig},
para_artifact::*,
},
shared::{constants::DEFAULT_CHAIN_SPEC_TPL_COMMAND, types::ChainDefaultContext},
ScopedFilesystem,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TeyrchainSpec {
// `name` of the parachain (used in some corner cases)
// name: Option<Chain>,
/// Parachain id
pub(crate) id: u32,
/// Unique id of the parachain, in the patter of <para_id>-<n>
/// where the suffix is only present if more than one parachain is set with the same id
pub(crate) unique_id: String,
/// Default command to run the node. Can be overridden on each node.
pub(crate) default_command: Option<Command>,
/// Default image to use (only podman/k8s). Can be overridden on each node.
pub(crate) default_image: Option<Image>,
/// Default resources. Can be overridden on each node.
pub(crate) default_resources: Option<Resources>,
/// Default database snapshot. Can be overridden on each node.
pub(crate) default_db_snapshot: Option<AssetLocation>,
/// Default arguments to use in nodes. Can be overridden on each node.
pub(crate) default_args: Vec<Arg>,
/// Chain-spec, only needed by cumulus based paras
pub(crate) chain_spec: Option<ChainSpec>,
/// Do not automatically assign a bootnode role if no nodes are marked as bootnodes.
pub(crate) no_default_bootnodes: bool,
/// Registration strategy to use
pub(crate) registration_strategy: RegistrationStrategy,
/// Onboard as parachain or parathread
pub(crate) onboard_as_parachain: bool,
/// Is the parachain cumulus-based
pub(crate) is_cumulus_based: bool,
/// Is the parachain evm-based
pub(crate) is_evm_based: bool,
/// Initial balance
pub(crate) initial_balance: u128,
/// Genesis state (head) to register the parachain
pub(crate) genesis_state: ParaArtifact,
/// Genesis WASM to register the parachain
pub(crate) genesis_wasm: ParaArtifact,
/// Genesis overrides as JSON value.
pub(crate) genesis_overrides: Option<serde_json::Value>,
/// Wasm override path/url to use.
pub(crate) wasm_override: Option<AssetLocation>,
/// Collators to spawn
pub(crate) collators: Vec<NodeSpec>,
/// Raw chain-spec override path, url or inline json to use.
pub(crate) raw_spec_override: Option<JsonOverrides>,
/// Bootnodes addresses to use for the parachain nodes
pub(crate) bootnodes_addresses: Vec<multiaddr::Multiaddr>,
}
impl TeyrchainSpec {
pub fn from_config(
config: &ParachainConfig,
relay_chain: Chain,
) -> Result<TeyrchainSpec, OrchestratorError> {
let main_cmd = if let Some(cmd) = config.default_command() {
cmd
} else if let Some(first_node) = config.collators().first() {
let Some(cmd) = first_node.command() else {
return Err(OrchestratorError::InvalidConfig(format!("Parachain {}, either default_command or command in the first node needs to be set.", config.id())));
};
cmd
} else {
return Err(OrchestratorError::InvalidConfig(format!(
"Parachain {}, without nodes and default_command isn't set.",
config.id()
)));
};
// TODO: internally we use image as String
let main_image = config
.default_image()
.or(config.collators().first().and_then(|node| node.image()))
.map(|image| image.as_str().to_string());
let chain_spec = if config.is_cumulus_based() {
// we need a chain-spec
let chain_name = if let Some(chain_name) = config.chain() {
chain_name.as_str()
} else {
""
};
let chain_spec_builder = if chain_name.is_empty() {
// if the chain don't have name use the unique_id for the name of the file
ChainSpec::new(
config.unique_id().to_string(),
Context::Para {
relay_chain,
para_id: config.id(),
},
)
} else {
let chain_spec_file_name = if config.unique_id().contains('-') {
&format!("{}-{}", chain_name, config.unique_id())
} else {
chain_name
};
ChainSpec::new(
chain_spec_file_name,
Context::Para {
relay_chain,
para_id: config.id(),
},
)
};
let chain_spec_builder = chain_spec_builder.set_chain_name(chain_name);
let replacements = HashMap::from([
("disableBootnodes", "--disable-default-bootnode"),
("mainCommand", main_cmd.as_str()),
]);
let tmpl = if let Some(tmpl) = config.chain_spec_command() {
apply_replacements(tmpl, &replacements)
} else {
apply_replacements(DEFAULT_CHAIN_SPEC_TPL_COMMAND, &replacements)
};
let chain_spec = chain_spec_builder
.command(
tmpl.as_str(),
config.chain_spec_command_is_local(),
config.chain_spec_command_output_path(),
)
.image(main_image.clone());
let chain_spec = if let Some(chain_spec_path) = config.chain_spec_path() {
chain_spec.asset_location(chain_spec_path.clone())
} else {
chain_spec
};
// add chain-spec runtime if present
let chain_spec = if let Some(chain_spec_runtime) = config.chain_spec_runtime() {
chain_spec.runtime(chain_spec_runtime.clone())
} else {
chain_spec
};
Some(chain_spec)
} else {
None
};
// build the `node_specs`
let chain_context = ChainDefaultContext {
default_command: config.default_command(),
default_image: config.default_image(),
default_resources: config.default_resources(),
default_db_snapshot: config.default_db_snapshot(),
default_args: config.default_args(),
};
// We want to track the errors for all the nodes and report them ones
let mut errs: Vec<OrchestratorError> = Default::default();
let mut collators: Vec<NodeSpec> = Default::default();
let mut nodes: Vec<NodeConfig> = config.collators().into_iter().cloned().collect();
nodes.extend(
config
.group_collators_configs()
.into_iter()
.flat_map(|node_group| node_group.expand_group_configs()),
);
let mut names = HashSet::new();
for node_config in nodes {
match NodeSpec::from_config(&node_config, &chain_context, true, config.is_evm_based()) {
Ok(mut node) => {
let unique_name = generate_unique_node_name_from_names(node.name, &mut names);
node.name = unique_name;
collators.push(node)
},
Err(err) => errs.push(err),
}
}
let genesis_state = if let Some(path) = config.genesis_state_path() {
ParaArtifact::new(
ParaArtifactType::State,
ParaArtifactBuildOption::Path(path.to_string()),
)
} else {
let cmd = if let Some(cmd) = config.genesis_state_generator() {
cmd.cmd()
} else {
main_cmd
};
ParaArtifact::new(
ParaArtifactType::State,
ParaArtifactBuildOption::Command(cmd.as_str().into()),
)
.image(main_image.clone())
};
let genesis_wasm = if let Some(path) = config.genesis_wasm_path() {
ParaArtifact::new(
ParaArtifactType::Wasm,
ParaArtifactBuildOption::Path(path.to_string()),
)
} else {
let cmd = if let Some(cmd) = config.genesis_wasm_generator() {
cmd.as_str()
} else {
main_cmd.as_str()
};
ParaArtifact::new(
ParaArtifactType::Wasm,
ParaArtifactBuildOption::Command(cmd.into()),
)
.image(main_image.clone())
};
let para_spec = TeyrchainSpec {
id: config.id(),
// ensure unique id is set at this point, if not just set to the para_id
unique_id: if config.unique_id().is_empty() {
config.id().to_string()
} else {
config.unique_id().to_string()
},
default_command: config.default_command().cloned(),
default_image: config.default_image().cloned(),
default_resources: config.default_resources().cloned(),
default_db_snapshot: config.default_db_snapshot().cloned(),
wasm_override: config.wasm_override().cloned(),
default_args: config.default_args().into_iter().cloned().collect(),
chain_spec,
no_default_bootnodes: config.no_default_bootnodes(),
registration_strategy: config
.registration_strategy()
.unwrap_or(&RegistrationStrategy::InGenesis)
.clone(),
onboard_as_parachain: config.onboard_as_parachain(),
is_cumulus_based: config.is_cumulus_based(),
is_evm_based: config.is_evm_based(),
initial_balance: config.initial_balance(),
genesis_state,
genesis_wasm,
genesis_overrides: config.genesis_overrides().cloned(),
collators,
raw_spec_override: config.raw_spec_override().cloned(),
bootnodes_addresses: config.bootnodes_addresses().into_iter().cloned().collect(),
};
Ok(para_spec)
}
pub fn registration_strategy(&self) -> &RegistrationStrategy {
&self.registration_strategy
}
pub fn get_genesis_config(&self) -> Result<ParaGenesisConfig<&PathBuf>, OrchestratorError> {
let genesis_config = ParaGenesisConfig {
state_path: self.genesis_state.artifact_path().ok_or(
OrchestratorError::InvariantError(
"artifact path for state must be set at this point",
),
)?,
wasm_path: self.genesis_wasm.artifact_path().ok_or(
OrchestratorError::InvariantError(
"artifact path for wasm must be set at this point",
),
)?,
id: self.id,
as_parachain: self.onboard_as_parachain,
};
Ok(genesis_config)
}
pub fn id(&self) -> u32 {
self.id
}
pub fn chain_spec(&self) -> Option<&ChainSpec> {
self.chain_spec.as_ref()
}
pub fn chain_spec_mut(&mut self) -> Option<&mut ChainSpec> {
self.chain_spec.as_mut()
}
/// Build parachain chain-spec
///
/// This function customize the chain-spec (if is possible) and build the raw version
/// of the chain-spec.
pub(crate) async fn build_chain_spec<'a, T>(
&mut self,
relay_chain_id: &str,
ns: &DynNamespace,
scoped_fs: &ScopedFilesystem<'a, T>,
) -> Result<Option<PathBuf>, anyhow::Error>
where
T: FileSystem,
{
let cloned = self.clone();
let chain_spec_raw_path = if let Some(chain_spec) = self.chain_spec.as_mut() {
debug!("parachain chain-spec building!");
chain_spec.build(ns, scoped_fs).await?;
debug!("parachain chain-spec built!");
chain_spec
.customize_para(&cloned, relay_chain_id, scoped_fs)
.await?;
debug!("parachain chain-spec customized!");
chain_spec
.build_raw(ns, scoped_fs, Some(relay_chain_id.try_into()?))
.await?;
debug!("parachain chain-spec raw built!");
// override wasm if needed
if let Some(ref wasm_override) = self.wasm_override {
chain_spec.override_code(scoped_fs, wasm_override).await?;
}
// override raw spec if needed
if let Some(ref raw_spec_override) = self.raw_spec_override {
chain_spec
.override_raw_spec(scoped_fs, raw_spec_override)
.await?;
}
let chain_spec_raw_path =
chain_spec
.raw_path()
.ok_or(OrchestratorError::InvariantError(
"chain-spec raw path should be set now",
))?;
Some(chain_spec_raw_path.to_path_buf())
} else {
None
};
Ok(chain_spec_raw_path)
}
/// Get the bootnodes addresses for the parachain spec
pub(crate) fn bootnodes_addresses(&self) -> Vec<&multiaddr::Multiaddr> {
self.bootnodes_addresses.iter().collect()
}
}
@@ -0,0 +1,3 @@
pub mod constants;
pub mod macros;
pub mod types;
@@ -0,0 +1,17 @@
/// Prometheus exporter default port
pub const PROMETHEUS_PORT: u16 = 9615;
/// Prometheus exporter default port in collator full-node
pub const FULL_NODE_PROMETHEUS_PORT: u16 = 9616;
/// JSON-RPC server (ws)
pub const RPC_PORT: u16 = 9944;
// JSON-RPC server (http, used by old versions)
pub const RPC_HTTP_PORT: u16 = 9933;
// P2P default port
pub const P2P_PORT: u16 = 30333;
// default command template to build chain-spec
pub const DEFAULT_CHAIN_SPEC_TPL_COMMAND: &str =
"{{mainCommand}} build-spec --chain {{chainName}} {{disableBootnodes}}";
// interval to determine how often to run node liveness checks
pub const NODE_MONITORING_INTERVAL_SECONDS: u64 = 15;
// how long to wait before a node is considered unresponsive
pub const NODE_MONITORING_FAILURE_THRESHOLD_SECONDS: u64 = 5;
@@ -0,0 +1,32 @@
macro_rules! create_add_options {
($struct:ident {$( $field:ident:$type:ty ),*}) =>{
#[derive(Default, Debug, Clone)]
pub struct $struct {
/// Image to run the node
pub image: Option<Image>,
/// Command to run the node
pub command: Option<Command>,
/// Subcommand for the node
pub subcommand: Option<Command>,
/// Arguments to pass to the node
pub args: Vec<Arg>,
/// Env vars to set
pub env: Vec<EnvVar>,
/// Make the node a validator
///
/// This implies `--validator` or `--collator`
pub is_validator: bool,
/// RPC port to use, if None a random one will be set
pub rpc_port: Option<Port>,
/// Prometheus port to use, if None a random one will be set
pub prometheus_port: Option<Port>,
/// P2P port to use, if None a random one will be set
pub p2p_port: Option<Port>,
$(
pub $field: $type,
)*
}
};
}
pub(crate) use create_add_options;
@@ -0,0 +1,99 @@
use std::{
collections::HashMap,
net::TcpListener,
path::PathBuf,
sync::{Arc, RwLock},
};
use configuration::shared::{
resources::Resources,
types::{Arg, AssetLocation, Command, Image, Port},
};
use serde::{Deserialize, Serialize};
pub type Accounts = HashMap<String, NodeAccount>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NodeAccount {
pub address: String,
pub public_key: String,
}
impl NodeAccount {
pub fn new(addr: impl Into<String>, pk: impl Into<String>) -> Self {
Self {
address: addr.into(),
public_key: pk.into(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct NodeAccounts {
pub seed: String,
pub accounts: Accounts,
}
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct ParkedPort(
pub(crate) Port,
#[serde(skip)] pub(crate) Arc<RwLock<Option<TcpListener>>>,
);
impl ParkedPort {
pub(crate) fn new(port: u16, listener: TcpListener) -> ParkedPort {
let listener = Arc::new(RwLock::new(Some(listener)));
ParkedPort(port, listener)
}
pub(crate) fn drop_listener(&self) {
// drop the listener will allow the running node to start listenen connections
let mut l = self.1.write().unwrap();
*l = None;
}
}
#[derive(Debug, Clone, Default)]
pub struct ChainDefaultContext<'a> {
pub default_command: Option<&'a Command>,
pub default_image: Option<&'a Image>,
pub default_resources: Option<&'a Resources>,
pub default_db_snapshot: Option<&'a AssetLocation>,
pub default_args: Vec<&'a Arg>,
}
#[derive(Debug, Clone)]
pub struct RegisterParachainOptions {
pub id: u32,
pub wasm_path: PathBuf,
pub state_path: PathBuf,
pub node_ws_url: String,
pub onboard_as_para: bool,
pub seed: Option<[u8; 32]>,
pub finalization: bool,
}
pub struct RuntimeUpgradeOptions {
/// Location of the wasm file (could be either a local file or an url)
pub wasm: AssetLocation,
/// Name of the node to use as rpc endpoint
pub node_name: Option<String>,
/// Seed to use to sign and submit (default to //Alice)
pub seed: Option<[u8; 32]>,
}
impl RuntimeUpgradeOptions {
pub fn new(wasm: AssetLocation) -> Self {
Self {
wasm,
node_name: None,
seed: None,
}
}
}
#[derive(Debug, Clone)]
pub struct ParachainGenesisArgs {
pub genesis_head: String,
pub validation_code: String,
pub parachain: bool,
}
@@ -0,0 +1,305 @@
use std::{collections::HashMap, path::PathBuf};
use anyhow::Context;
use configuration::GlobalSettings;
use provider::{
constants::{LOCALHOST, NODE_CONFIG_DIR, NODE_DATA_DIR, NODE_RELAY_DATA_DIR, P2P_PORT},
shared::helpers::running_in_ci,
types::{SpawnNodeOptions, TransferedFile},
DynNamespace,
};
use support::{
constants::THIS_IS_A_BUG, fs::FileSystem, replacer::apply_running_network_replacements,
};
use tracing::info;
use crate::{
generators,
network::node::NetworkNode,
network_spec::{node::NodeSpec, teyrchain::TeyrchainSpec},
shared::constants::{FULL_NODE_PROMETHEUS_PORT, PROMETHEUS_PORT, RPC_PORT},
ScopedFilesystem, ZombieRole,
};
#[derive(Clone)]
pub struct SpawnNodeCtx<'a, T: FileSystem> {
/// Relaychain id, from the chain-spec (e.g rococo_local_testnet)
pub(crate) chain_id: &'a str,
// Parachain id, from the chain-spec (e.g local_testnet)
pub(crate) parachain_id: Option<&'a str>,
/// Relaychain chain name (e.g rococo-local)
pub(crate) chain: &'a str,
/// Role of the node in the network
pub(crate) role: ZombieRole,
/// Ref to the namespace
pub(crate) ns: &'a DynNamespace,
/// Ref to an scoped filesystem (encapsulate fs actions inside the ns directory)
pub(crate) scoped_fs: &'a ScopedFilesystem<'a, T>,
/// Ref to a parachain (used to spawn collators)
pub(crate) parachain: Option<&'a TeyrchainSpec>,
/// The string representation of the bootnode address to pass to nodes
pub(crate) bootnodes_addr: &'a Vec<String>,
/// Flag to wait node is ready or not
/// Ready state means we can query Prometheus internal server
pub(crate) wait_ready: bool,
/// A json representation of the running nodes with their names as 'key'
pub(crate) nodes_by_name: serde_json::Value,
/// A ref to the global settings
pub(crate) global_settings: &'a GlobalSettings,
}
pub async fn spawn_node<'a, T>(
node: &NodeSpec,
mut files_to_inject: Vec<TransferedFile>,
ctx: &SpawnNodeCtx<'a, T>,
) -> Result<NetworkNode, anyhow::Error>
where
T: FileSystem,
{
let mut created_paths = vec![];
// Create and inject the keystore IFF
// - The node is validator in the relaychain
// - The node is collator (encoded as validator) and the parachain is cumulus_based
// (parachain_id) should be set then.
if node.is_validator && (ctx.parachain.is_none() || ctx.parachain_id.is_some()) {
// Generate keystore for node
let node_files_path = if let Some(para) = ctx.parachain {
para.id.to_string()
} else {
node.name.clone()
};
let asset_hub_polkadot = ctx
.parachain_id
.map(|id| id.starts_with("asset-hub-polkadot"))
.unwrap_or_default();
let keystore_key_types = node.keystore_key_types.iter().map(String::as_str).collect();
let key_filenames = generators::generate_node_keystore(
&node.accounts,
&node_files_path,
ctx.scoped_fs,
asset_hub_polkadot,
keystore_key_types,
)
.await
.unwrap();
// Paths returned are relative to the base dir, we need to convert into
// fullpaths to inject them in the nodes.
let remote_keystore_chain_id = if let Some(id) = ctx.parachain_id {
id
} else {
ctx.chain_id
};
let keystore_path = node.keystore_path.clone().unwrap_or(PathBuf::from(format!(
"/data/chains/{remote_keystore_chain_id}/keystore",
)));
for key_filename in key_filenames {
let f = TransferedFile::new(
PathBuf::from(format!(
"{}/{}/{}",
ctx.ns.base_dir().to_string_lossy(),
node_files_path,
key_filename.to_string_lossy()
)),
keystore_path.join(key_filename),
);
files_to_inject.push(f);
}
created_paths.push(keystore_path);
}
let base_dir = format!("{}/{}", ctx.ns.base_dir().to_string_lossy(), &node.name);
let (cfg_path, data_path, relay_data_path) = if !ctx.ns.capabilities().prefix_with_full_path {
(
NODE_CONFIG_DIR.into(),
NODE_DATA_DIR.into(),
NODE_RELAY_DATA_DIR.into(),
)
} else {
let cfg_path = format!("{}{NODE_CONFIG_DIR}", &base_dir);
let data_path = format!("{}{NODE_DATA_DIR}", &base_dir);
let relay_data_path = format!("{}{NODE_RELAY_DATA_DIR}", &base_dir);
(cfg_path, data_path, relay_data_path)
};
let gen_opts = generators::GenCmdOptions {
relay_chain_name: ctx.chain,
cfg_path: &cfg_path, // TODO: get from provider/ns
data_path: &data_path, // TODO: get from provider
relay_data_path: &relay_data_path, // TODO: get from provider
use_wrapper: false, // TODO: get from provider
bootnode_addr: ctx.bootnodes_addr.clone(),
use_default_ports_in_cmd: ctx.ns.capabilities().use_default_ports_in_cmd,
// IFF the provider require an image (e.g k8s) we know this is not native
is_native: !ctx.ns.capabilities().requires_image,
};
let mut collator_full_node_prom_port: Option<u16> = None;
let mut collator_full_node_prom_port_external: Option<u16> = None;
let (program, args) = match ctx.role {
// Collator should be `non-cumulus` one (e.g adder/undying)
ZombieRole::Node | ZombieRole::Collator => {
let maybe_para_id = ctx.parachain.map(|para| para.id);
generators::generate_node_command(node, gen_opts, maybe_para_id)
},
ZombieRole::CumulusCollator => {
let para = ctx.parachain.expect(&format!(
"parachain must be part of the context {THIS_IS_A_BUG}"
));
collator_full_node_prom_port = node.full_node_prometheus_port.as_ref().map(|p| p.0);
generators::generate_node_command_cumulus(node, gen_opts, para.id)
},
_ => unreachable!(), /* TODO: do we need those?
* ZombieRole::Bootnode => todo!(),
* ZombieRole::Companion => todo!(), */
};
// apply running networ replacements
let args: Vec<String> = args
.iter()
.map(|arg| apply_running_network_replacements(arg, &ctx.nodes_by_name))
.collect();
info!(
"🚀 {}, spawning.... with command: {} {}",
node.name,
program,
args.join(" ")
);
let ports = if ctx.ns.capabilities().use_default_ports_in_cmd {
// should use default ports to as internal
[
(P2P_PORT, node.p2p_port.0),
(RPC_PORT, node.rpc_port.0),
(PROMETHEUS_PORT, node.prometheus_port.0),
]
} else {
[
(P2P_PORT, P2P_PORT),
(RPC_PORT, RPC_PORT),
(PROMETHEUS_PORT, PROMETHEUS_PORT),
]
};
let spawn_ops = SpawnNodeOptions::new(node.name.clone(), program)
.args(args)
.env(
node.env
.iter()
.map(|var| (var.name.clone(), var.value.clone())),
)
.injected_files(files_to_inject)
.created_paths(created_paths)
.db_snapshot(node.db_snapshot.clone())
.port_mapping(HashMap::from(ports))
.node_log_path(node.node_log_path.clone());
let spawn_ops = if let Some(image) = node.image.as_ref() {
spawn_ops.image(image.as_str())
} else {
spawn_ops
};
// Drops the port parking listeners before spawn
node.ws_port.drop_listener();
node.p2p_port.drop_listener();
node.rpc_port.drop_listener();
node.prometheus_port.drop_listener();
if let Some(port) = &node.full_node_p2p_port {
port.drop_listener();
}
if let Some(port) = &node.full_node_prometheus_port {
port.drop_listener();
}
let running_node = ctx.ns.spawn_node(&spawn_ops).await.with_context(|| {
format!(
"Failed to spawn node: {} with opts: {:#?}",
node.name, spawn_ops
)
})?;
let mut ip_to_use = if let Some(local_ip) = ctx.global_settings.local_ip() {
*local_ip
} else {
LOCALHOST
};
let (rpc_port_external, prometheus_port_external, p2p_external);
if running_in_ci() && ctx.ns.provider_name() == "k8s" {
// running kubernets in ci require to use ip and default port
(rpc_port_external, prometheus_port_external, p2p_external) =
(RPC_PORT, PROMETHEUS_PORT, P2P_PORT);
collator_full_node_prom_port_external = Some(FULL_NODE_PROMETHEUS_PORT);
ip_to_use = running_node.ip().await?;
} else {
// Create port-forward iff we are not in CI or provider doesn't use the default ports (native)
let ports = futures::future::try_join_all(vec![
running_node.create_port_forward(node.rpc_port.0, RPC_PORT),
running_node.create_port_forward(node.prometheus_port.0, PROMETHEUS_PORT),
])
.await?;
(rpc_port_external, prometheus_port_external, p2p_external) = (
ports[0].unwrap_or(node.rpc_port.0),
ports[1].unwrap_or(node.prometheus_port.0),
// p2p don't need port-fwd
node.p2p_port.0,
);
if let Some(full_node_prom_port) = collator_full_node_prom_port {
let port_fwd = running_node
.create_port_forward(full_node_prom_port, FULL_NODE_PROMETHEUS_PORT)
.await?;
collator_full_node_prom_port_external = Some(port_fwd.unwrap_or(full_node_prom_port));
}
}
let multiaddr = generators::generate_node_bootnode_addr(
&node.peer_id,
&running_node.ip().await?,
p2p_external,
running_node.args().as_ref(),
&node.p2p_cert_hash,
)?;
let ws_uri = format!("ws://{ip_to_use}:{rpc_port_external}");
let prometheus_uri = format!("http://{ip_to_use}:{prometheus_port_external}/metrics");
info!("🚀 {}, should be running now", node.name);
info!(
"💻 {}: direct link (pjs) https://polkadot.js.org/apps/?rpc={ws_uri}#/explorer",
node.name
);
info!(
"💻 {}: direct link (papi) https://dev.papi.how/explorer#networkId=custom&endpoint={ws_uri}",
node.name
);
info!("📊 {}: metrics link {prometheus_uri}", node.name);
if let Some(full_node_prom_port) = collator_full_node_prom_port_external {
info!(
"📊 {}: collator full-node metrics link http://{}:{}/metrics",
node.name, ip_to_use, full_node_prom_port
);
}
info!("📓 logs cmd: {}", running_node.log_cmd());
Ok(NetworkNode::new(
node.name.clone(),
ws_uri,
prometheus_uri,
multiaddr,
node.clone(),
running_node,
))
}
@@ -0,0 +1,2 @@
pub mod client;
pub mod runtime_upgrade;
@@ -0,0 +1,43 @@
use pezkuwi_subxt::{backend::rpc::RpcClient, OnlineClient};
#[async_trait::async_trait]
pub trait ClientFromUrl: Sized {
async fn from_secure_url(url: &str) -> Result<Self, pezkuwi_subxt::Error>;
async fn from_insecure_url(url: &str) -> Result<Self, pezkuwi_subxt::Error>;
}
#[async_trait::async_trait]
impl<Config: pezkuwi_subxt::Config + Send + Sync> ClientFromUrl for OnlineClient<Config> {
async fn from_secure_url(url: &str) -> Result<Self, pezkuwi_subxt::Error> {
Self::from_url(url).await.map_err(Into::into)
}
async fn from_insecure_url(url: &str) -> Result<Self, pezkuwi_subxt::Error> {
Self::from_insecure_url(url).await.map_err(Into::into)
}
}
#[async_trait::async_trait]
impl ClientFromUrl for RpcClient {
async fn from_secure_url(url: &str) -> Result<Self, pezkuwi_subxt::Error> {
Self::from_url(url)
.await
.map_err(pezkuwi_subxt::Error::from)
}
async fn from_insecure_url(url: &str) -> Result<Self, pezkuwi_subxt::Error> {
Self::from_insecure_url(url)
.await
.map_err(pezkuwi_subxt::Error::from)
}
}
pub async fn get_client_from_url<T: ClientFromUrl + Send>(
url: &str,
) -> Result<T, pezkuwi_subxt::Error> {
if pezkuwi_subxt::utils::url_is_secure(url)? {
T::from_secure_url(url).await
} else {
T::from_insecure_url(url).await
}
}
@@ -0,0 +1,69 @@
use pezkuwi_subxt::{dynamic::Value, tx::TxStatus, BizinikiwConfig, OnlineClient};
use pezkuwi_subxt_signer::sr25519::Keypair;
use tracing::{debug, info};
use crate::network::node::NetworkNode;
pub async fn upgrade(
node: &NetworkNode,
wasm_data: &[u8],
sudo: &Keypair,
) -> Result<(), anyhow::Error> {
debug!(
"Upgrading runtime, using node: {} with endpoting {}",
node.name, node.ws_uri
);
let api: OnlineClient<BizinikiwConfig> = node.wait_client().await?;
let upgrade = pezkuwi_subxt::dynamic::tx(
"System",
"set_code_without_checks",
vec![Value::from_bytes(wasm_data)],
);
let sudo_call = pezkuwi_subxt::dynamic::tx(
"Sudo",
"sudo_unchecked_weight",
vec![
upgrade.into_value(),
Value::named_composite([
("ref_time", Value::primitive(1.into())),
("proof_size", Value::primitive(1.into())),
]),
],
);
let mut tx = api
.tx()
.sign_and_submit_then_watch_default(&sudo_call, sudo)
.await?;
// Below we use the low level API to replicate the `wait_for_in_block` behaviour
// which was removed in subxt 0.33.0. See https://github.com/paritytech/subxt/pull/1237.
while let Some(status) = tx.next().await {
let status = status?;
match &status {
TxStatus::InBestBlock(tx_in_block) | TxStatus::InFinalizedBlock(tx_in_block) => {
let _result = tx_in_block.wait_for_success().await?;
let block_status = if status.as_finalized().is_some() {
"Finalized"
} else {
"Best"
};
info!(
"[{}] In block: {:#?}",
block_status,
tx_in_block.block_hash()
);
},
TxStatus::Error { message }
| TxStatus::Invalid { message }
| TxStatus::Dropped { message } => {
return Err(anyhow::format_err!("Error submitting tx: {message}"));
},
_ => continue,
}
}
Ok(())
}
@@ -0,0 +1,8 @@
use serde::Deserializer;
pub fn default_as_empty_vec<'de, D, T>(_deserializer: D) -> Result<Vec<T>, D::Error>
where
D: Deserializer<'de>,
{
Ok(Vec::new())
}