use std::{ collections::{HashMap, HashSet}, sync::Mutex, }; use logos_blockchain_executor::config::Config as ExecutorConfig; use nomos_node::Config as ValidatorConfig; use testing_framework_config::topology::configs::{consensus, time}; use testing_framework_core::{ nodes::{ ApiClient, executor::{Executor, create_executor_config}, validator::{Validator, create_validator_config}, }, scenario::{DynError, NodeControlHandle, StartNodeOptions, StartedNode}, topology::{ generation::{GeneratedTopology, NodeRole, find_expected_peer_counts}, utils::multiaddr_port, }, }; use thiserror::Error; mod config; mod state; use config::build_general_config_for; use state::LocalDynamicState; use testing_framework_core::scenario::NodeClients; #[derive(Debug, Error)] pub enum LocalDynamicError { #[error("failed to generate node config: {source}")] Config { #[source] source: testing_framework_config::topology::configs::GeneralConfigError, }, #[error("failed to spawn node: {source}")] Spawn { #[source] source: testing_framework_core::nodes::common::node::SpawnNodeError, }, #[error("{message}")] InvalidArgument { message: String }, #[error("{message}")] PortAllocation { message: String }, } pub struct LocalDynamicNodes { descriptors: GeneratedTopology, base_consensus: consensus::GeneralConsensusConfig, base_time: time::GeneralTimeConfig, node_clients: NodeClients, seed: LocalDynamicSeed, state: Mutex, } #[derive(Clone, Default)] pub struct LocalDynamicSeed { pub validator_count: usize, pub executor_count: usize, pub peer_ports: Vec, pub peer_ports_by_name: HashMap, } impl LocalDynamicSeed { #[must_use] pub fn from_topology(descriptors: &GeneratedTopology) -> Self { let peer_ports = descriptors .nodes() .map(|node| node.network_port()) .collect::>(); let peer_ports_by_name = descriptors .validators() .iter() .map(|node| (format!("validator-{}", node.index()), node.network_port())) .chain( descriptors .executors() .iter() .map(|node| (format!("executor-{}", node.index()), node.network_port())), ) .collect(); Self { validator_count: descriptors.validators().len(), executor_count: descriptors.executors().len(), peer_ports, peer_ports_by_name, } } } pub(crate) struct ReadinessNode { pub(crate) label: String, pub(crate) expected_peers: Option, pub(crate) api: ApiClient, } impl LocalDynamicNodes { pub fn new(descriptors: GeneratedTopology, node_clients: NodeClients) -> Self { Self::new_with_seed(descriptors, node_clients, LocalDynamicSeed::default()) } pub fn new_with_seed( descriptors: GeneratedTopology, node_clients: NodeClients, seed: LocalDynamicSeed, ) -> Self { let base_node = descriptors .validators() .first() .or_else(|| descriptors.executors().first()) .expect("generated topology must include at least one node"); let base_consensus = base_node.general.consensus_config.clone(); let base_time = base_node.general.time_config.clone(); let state = LocalDynamicState { validator_count: seed.validator_count, executor_count: seed.executor_count, peer_ports: seed.peer_ports.clone(), peer_ports_by_name: seed.peer_ports_by_name.clone(), clients_by_name: HashMap::new(), validators: Vec::new(), executors: Vec::new(), }; Self { descriptors, base_consensus, base_time, node_clients, seed, state: Mutex::new(state), } } #[must_use] pub fn node_client(&self, name: &str) -> Option { let state = self.state.lock().expect("local dynamic lock poisoned"); state.clients_by_name.get(name).cloned() } pub fn stop_all(&self) { let mut state = self.state.lock().expect("local dynamic lock poisoned"); state.validators.clear(); state.executors.clear(); state.peer_ports.clone_from(&self.seed.peer_ports); state .peer_ports_by_name .clone_from(&self.seed.peer_ports_by_name); state.clients_by_name.clear(); state.validator_count = self.seed.validator_count; state.executor_count = self.seed.executor_count; self.node_clients.clear(); } pub async fn start_validator_with( &self, name: &str, options: StartNodeOptions, ) -> Result { self.start_node(NodeRole::Validator, name, options).await } pub async fn start_executor_with( &self, name: &str, options: StartNodeOptions, ) -> Result { self.start_node(NodeRole::Executor, name, options).await } pub(crate) fn readiness_nodes(&self) -> Vec { let state = self.state.lock().expect("local dynamic lock poisoned"); let listen_ports = state .validators .iter() .map(|node| node.config().network.backend.swarm.port) .chain( state .executors .iter() .map(|node| node.config().network.backend.swarm.port), ) .collect::>(); let initial_peer_ports = state .validators .iter() .map(|node| { node.config() .network .backend .initial_peers .iter() .filter_map(multiaddr_port) .collect::>() }) .chain(state.executors.iter().map(|node| { node.config() .network .backend .initial_peers .iter() .filter_map(multiaddr_port) .collect::>() })) .collect::>(); let expected_peer_counts = find_expected_peer_counts(&listen_ports, &initial_peer_ports); state .validators .iter() .enumerate() .map(|(idx, node)| ReadinessNode { label: format!( "validator#{idx}@{}", node.config().network.backend.swarm.port ), expected_peers: expected_peer_counts.get(idx).copied(), api: node.api().clone(), }) .chain(state.executors.iter().enumerate().map(|(idx, node)| { let global_idx = state.validators.len() + idx; ReadinessNode { label: format!( "executor#{idx}@{}", node.config().network.backend.swarm.port ), expected_peers: expected_peer_counts.get(global_idx).copied(), api: node.api().clone(), } })) .collect::>() } async fn start_node( &self, role: NodeRole, name: &str, options: StartNodeOptions, ) -> Result { let (peer_ports, peer_ports_by_name, node_name, index) = { let state = self.state.lock().expect("local dynamic lock poisoned"); let (index, role_label) = match role { NodeRole::Validator => (state.validator_count, "validator"), NodeRole::Executor => (state.executor_count, "executor"), }; let label = if name.trim().is_empty() { format!("{role_label}-{index}") } else { format!("{role_label}-{name}") }; if state.peer_ports_by_name.contains_key(&label) { return Err(LocalDynamicError::InvalidArgument { message: format!("node name '{label}' already exists"), }); } ( state.peer_ports.clone(), state.peer_ports_by_name.clone(), label, index, ) }; let (general_config, network_port) = build_general_config_for( &self.descriptors, &self.base_consensus, &self.base_time, role, index, &peer_ports_by_name, &options, &peer_ports, )?; let api_client = match role { NodeRole::Validator => { let config = create_validator_config(general_config); self.spawn_and_register_validator(&node_name, network_port, config) .await? } NodeRole::Executor => { let config = create_executor_config(general_config); self.spawn_and_register_executor(&node_name, network_port, config) .await? } }; Ok(StartedNode { name: node_name, role, api: api_client, }) } async fn spawn_and_register_validator( &self, node_name: &str, network_port: u16, config: ValidatorConfig, ) -> Result { let node = Validator::spawn(config, node_name) .await .map_err(|source| LocalDynamicError::Spawn { source })?; let client = node.api().clone(); self.node_clients.add_validator(client.clone()); let mut state = self.state.lock().expect("local dynamic lock poisoned"); state.register_validator(node_name, network_port, client.clone(), node); Ok(client) } async fn spawn_and_register_executor( &self, node_name: &str, network_port: u16, config: ExecutorConfig, ) -> Result { let node = Executor::spawn(config, node_name) .await .map_err(|source| LocalDynamicError::Spawn { source })?; let client = node.api().clone(); self.node_clients.add_executor(client.clone()); let mut state = self.state.lock().expect("local dynamic lock poisoned"); state.register_executor(node_name, network_port, client.clone(), node); Ok(client) } } #[async_trait::async_trait] impl NodeControlHandle for LocalDynamicNodes { async fn restart_validator(&self, _index: usize) -> Result<(), DynError> { Err("local deployer does not support restart_validator".into()) } async fn restart_executor(&self, _index: usize) -> Result<(), DynError> { Err("local deployer does not support restart_executor".into()) } async fn start_validator(&self, name: &str) -> Result { self.start_validator_with(name, StartNodeOptions::default()) .await .map_err(|err| err.into()) } async fn start_executor(&self, name: &str) -> Result { self.start_executor_with(name, StartNodeOptions::default()) .await .map_err(|err| err.into()) } async fn start_validator_with( &self, name: &str, options: StartNodeOptions, ) -> Result { self.start_validator_with(name, options) .await .map_err(|err| err.into()) } async fn start_executor_with( &self, name: &str, options: StartNodeOptions, ) -> Result { self.start_executor_with(name, options) .await .map_err(|err| err.into()) } fn node_client(&self, name: &str) -> Option { self.node_client(name) } }