From 540ef9f9c298e63f96a3b6a91142e8ec0d619971 Mon Sep 17 00:00:00 2001 From: andrussal Date: Thu, 18 Dec 2025 14:40:49 +0100 Subject: [PATCH] configs: validate DA subnet ids and handle empty network layouts --- Cargo.lock | 1 + testing-framework/configs/Cargo.toml | 1 + .../configs/src/topology/configs/consensus.rs | 156 +++++++++--------- .../configs/src/topology/configs/da.rs | 70 +++++++- .../configs/src/topology/configs/network.rs | 4 + 5 files changed, 153 insertions(+), 79 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 99a0dc8..fe50031 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7215,6 +7215,7 @@ dependencies = [ "rand 0.8.5", "serde", "subnetworks-assignations", + "thiserror 2.0.17", "time", "tracing", ] diff --git a/testing-framework/configs/Cargo.toml b/testing-framework/configs/Cargo.toml index aee02f7..de34106 100644 --- a/testing-framework/configs/Cargo.toml +++ b/testing-framework/configs/Cargo.toml @@ -41,6 +41,7 @@ num-bigint = { version = "0.4", default-features = false } rand = { workspace = true } serde = { workspace = true, features = ["derive"] } subnetworks-assignations = { workspace = true } +thiserror = { workspace = true } time = { version = "0.3", default-features = true } tracing = { workspace = true } diff --git a/testing-framework/configs/src/topology/configs/consensus.rs b/testing-framework/configs/src/topology/configs/consensus.rs index 85da316..28fdc47 100644 --- a/testing-framework/configs/src/topology/configs/consensus.rs +++ b/testing-framework/configs/src/topology/configs/consensus.rs @@ -146,25 +146,8 @@ fn create_genesis_tx(utxos: &[Utxo]) -> GenesisTx { GenesisTx::from_tx(signed_mantle_tx).expect("Invalid genesis transaction") } -#[must_use] -pub fn create_consensus_configs( - ids: &[[u8; 32]], - consensus_params: &ConsensusParams, - wallet: &WalletConfig, -) -> Vec { - let mut leader_keys = Vec::new(); - let mut blend_notes = Vec::new(); - let mut da_notes = Vec::new(); - - let utxos = create_utxos_for_leader_and_services( - ids, - &mut leader_keys, - &mut blend_notes, - &mut da_notes, - ); - let utxos = append_wallet_utxos(utxos, wallet); - let genesis_tx = create_genesis_tx(&utxos); - let ledger_config = nomos_ledger::Config { +fn build_ledger_config(consensus_params: &ConsensusParams) -> nomos_ledger::Config { + nomos_ledger::Config { epoch_config: EpochConfig { epoch_stake_distribution_stabilization: NonZero::new(3).unwrap(), epoch_period_nonce_buffer: NonZero::new(3).unwrap(), @@ -213,7 +196,28 @@ pub fn create_consensus_configs( }, }, }, - }; + } +} + +#[must_use] +pub fn create_consensus_configs( + ids: &[[u8; 32]], + consensus_params: &ConsensusParams, + wallet: &WalletConfig, +) -> Vec { + let mut leader_keys = Vec::new(); + let mut blend_notes = Vec::new(); + let mut da_notes = Vec::new(); + + let utxos = create_utxos_for_leader_and_services( + ids, + &mut leader_keys, + &mut blend_notes, + &mut da_notes, + ); + let utxos = append_wallet_utxos(utxos, wallet); + let genesis_tx = create_genesis_tx(&utxos); + let ledger_config = build_ledger_config(consensus_params); leader_keys .into_iter() @@ -235,17 +239,6 @@ fn create_utxos_for_leader_and_services( blend_notes: &mut Vec, da_notes: &mut Vec, ) -> Vec { - let derive_key_material = |prefix: &[u8], id_bytes: &[u8]| -> [u8; 16] { - let mut sk_data = [0; 16]; - let prefix_len = prefix.len(); - - sk_data[..prefix_len].copy_from_slice(prefix); - let remaining_len = 16 - prefix_len; - sk_data[prefix_len..].copy_from_slice(&id_bytes[..remaining_len]); - - sk_data - }; - let mut utxos = Vec::new(); // Assume output index which will be set by the ledger tx. @@ -253,55 +246,68 @@ fn create_utxos_for_leader_and_services( // Create notes for leader, Blend and DA declarations. for &id in ids { - let sk_leader_data = derive_key_material(b"ld", &id); - let sk_leader = UnsecuredZkKey::from(BigUint::from_bytes_le(&sk_leader_data)); - let pk_leader = sk_leader.to_public_key(); - leader_keys.push((pk_leader, sk_leader)); - utxos.push(Utxo { - note: Note::new(1_000, pk_leader), - tx_hash: BigUint::from(0u8).into(), - output_index: 0, - }); - output_index += 1; - - let sk_da_data = derive_key_material(b"da", &id); - let sk_da = ZkKey::from(BigUint::from_bytes_le(&sk_da_data)); - let pk_da = sk_da.to_public_key(); - let note_da = Note::new(1, pk_da); - da_notes.push(ServiceNote { - pk: pk_da, - sk: sk_da, - note: note_da, - output_index, - }); - utxos.push(Utxo { - note: note_da, - tx_hash: BigUint::from(0u8).into(), - output_index: 0, - }); - output_index += 1; - - let sk_blend_data = derive_key_material(b"bn", &id); - let sk_blend = ZkKey::from(BigUint::from_bytes_le(&sk_blend_data)); - let pk_blend = sk_blend.to_public_key(); - let note_blend = Note::new(1, pk_blend); - blend_notes.push(ServiceNote { - pk: pk_blend, - sk: sk_blend, - note: note_blend, - output_index, - }); - utxos.push(Utxo { - note: note_blend, - tx_hash: BigUint::from(0u8).into(), - output_index: 0, - }); - output_index += 1; + output_index = push_leader_utxo(id, leader_keys, &mut utxos, output_index); + output_index = push_service_note(b"da", id, da_notes, &mut utxos, output_index); + output_index = push_service_note(b"bn", id, blend_notes, &mut utxos, output_index); } utxos } +fn derive_key_material(prefix: &[u8], id_bytes: &[u8; 32]) -> [u8; 16] { + let mut sk_data = [0; 16]; + let prefix_len = prefix.len(); + + sk_data[..prefix_len].copy_from_slice(prefix); + let remaining_len = 16 - prefix_len; + sk_data[prefix_len..].copy_from_slice(&id_bytes[..remaining_len]); + + sk_data +} + +fn push_leader_utxo( + id: [u8; 32], + leader_keys: &mut Vec<(ZkPublicKey, UnsecuredZkKey)>, + utxos: &mut Vec, + output_index: usize, +) -> usize { + let sk_data = derive_key_material(b"ld", &id); + let sk = UnsecuredZkKey::from(BigUint::from_bytes_le(&sk_data)); + let pk = sk.to_public_key(); + leader_keys.push((pk, sk)); + utxos.push(Utxo { + note: Note::new(1_000, pk), + tx_hash: BigUint::from(0u8).into(), + output_index: 0, + }); + output_index + 1 +} + +fn push_service_note( + prefix: &[u8], + id: [u8; 32], + notes: &mut Vec, + utxos: &mut Vec, + output_index: usize, +) -> usize { + let sk_data = derive_key_material(prefix, &id); + let sk = ZkKey::from(BigUint::from_bytes_le(&sk_data)); + let pk = sk.to_public_key(); + let note = Note::new(1, pk); + notes.push(ServiceNote { + pk, + sk, + note, + output_index, + }); + utxos.push(Utxo { + note, + tx_hash: BigUint::from(0u8).into(), + output_index: 0, + }); + output_index + 1 +} + fn append_wallet_utxos(mut utxos: Vec, wallet: &WalletConfig) -> Vec { for account in &wallet.accounts { utxos.push(Utxo { diff --git a/testing-framework/configs/src/topology/configs/da.rs b/testing-framework/configs/src/topology/configs/da.rs index fa8dc79..1b96b37 100644 --- a/testing-framework/configs/src/topology/configs/da.rs +++ b/testing-framework/configs/src/topology/configs/da.rs @@ -18,6 +18,7 @@ use nomos_node::NomosDaMembership; use num_bigint::BigUint; use rand::random; use subnetworks_assignations::{MembershipCreator as _, MembershipHandler as _}; +use thiserror::Error; use tracing::warn; use crate::secret_key_to_peer_id; @@ -168,9 +169,43 @@ pub fn create_da_configs( da_params: &DaParams, ports: &[u16], ) -> Vec { + try_create_da_configs(ids, da_params, ports).expect("failed to build DA configs") +} + +#[derive(Debug, Error)] +pub enum DaConfigError { + #[error("DA ports length mismatch (ids={ids}, ports={ports})")] + PortsLenMismatch { ids: usize, ports: usize }, + #[error( + "DA subnetwork size too large for u16 subnetwork ids (effective_subnetwork_size={effective_subnetwork_size}, max={max})" + )] + SubnetworkTooLarge { + effective_subnetwork_size: usize, + max: usize, + }, +} + +pub fn try_create_da_configs( + ids: &[[u8; 32]], + da_params: &DaParams, + ports: &[u16], +) -> Result, DaConfigError> { // Let the subnetwork size track the participant count so tiny local topologies // can form a membership. let effective_subnetwork_size = da_params.subnetwork_size.max(ids.len().max(1)); + let max_subnetworks = u16::MAX as usize + 1; + if effective_subnetwork_size > max_subnetworks { + return Err(DaConfigError::SubnetworkTooLarge { + effective_subnetwork_size, + max: max_subnetworks, + }); + } + if ports.len() < ids.len() { + return Err(DaConfigError::PortsLenMismatch { + ids: ids.len(), + ports: ports.len(), + }); + } let mut node_keys = vec![]; let mut peer_ids = vec![]; let mut listening_addresses = vec![]; @@ -199,7 +234,7 @@ pub fn create_da_configs( let mut assignations: HashMap> = HashMap::new(); if peer_ids.is_empty() { for id in 0..effective_subnetwork_size { - assignations.insert(u16::try_from(id).unwrap_or_default(), HashSet::new()); + assignations.insert(id as u16, HashSet::new()); } } else { let mut sorted_peers = peer_ids.clone(); @@ -214,14 +249,15 @@ pub fn create_da_configs( members.insert(*peer); } } - assignations.insert(u16::try_from(id).unwrap_or_default(), members); + assignations.insert(id as u16, members); } } template.init(SessionNumber::default(), assignations) }; - ids.iter() + Ok(ids + .iter() .zip(node_keys) .enumerate() .map(|(i, (id, node_key))| { @@ -267,5 +303,31 @@ pub fn create_da_configs( retry_commitments_limit: da_params.retry_commitments_limit, } }) - .collect() + .collect()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn try_create_da_configs_rejects_subnetwork_overflow() { + let ids = vec![[1u8; 32]]; + let ports = vec![12345u16]; + let mut params = DaParams::default(); + params.subnetwork_size = u16::MAX as usize + 2; + + let err = try_create_da_configs(&ids, ¶ms, &ports).unwrap_err(); + assert!(matches!(err, DaConfigError::SubnetworkTooLarge { .. })); + } + + #[test] + fn try_create_da_configs_rejects_port_mismatch() { + let ids = vec![[1u8; 32], [2u8; 32]]; + let ports = vec![12345u16]; + let params = DaParams::default(); + + let err = try_create_da_configs(&ids, ¶ms, &ports).unwrap_err(); + assert!(matches!(err, DaConfigError::PortsLenMismatch { .. })); + } } diff --git a/testing-framework/configs/src/topology/configs/network.rs b/testing-framework/configs/src/topology/configs/network.rs index 4dba250..db2b820 100644 --- a/testing-framework/configs/src/topology/configs/network.rs +++ b/testing-framework/configs/src/topology/configs/network.rs @@ -94,6 +94,10 @@ fn initial_peers_by_network_layout( swarm_configs: &[SwarmConfig], network_params: &NetworkParams, ) -> Vec> { + if swarm_configs.is_empty() { + return Vec::new(); + } + let mut all_initial_peers = vec![]; match network_params.libp2p_network_layout {