configs: validate DA subnet ids and handle empty network layouts

This commit is contained in:
andrussal 2025-12-18 14:40:49 +01:00
parent a582c00692
commit 540ef9f9c2
5 changed files with 153 additions and 79 deletions

1
Cargo.lock generated
View File

@ -7215,6 +7215,7 @@ dependencies = [
"rand 0.8.5",
"serde",
"subnetworks-assignations",
"thiserror 2.0.17",
"time",
"tracing",
]

View File

@ -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 }

View File

@ -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<GeneralConsensusConfig> {
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<GeneralConsensusConfig> {
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<ServiceNote>,
da_notes: &mut Vec<ServiceNote>,
) -> Vec<Utxo> {
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<Utxo>,
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<ServiceNote>,
utxos: &mut Vec<Utxo>,
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<Utxo>, wallet: &WalletConfig) -> Vec<Utxo> {
for account in &wallet.accounts {
utxos.push(Utxo {

View File

@ -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<GeneralDaConfig> {
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<Vec<GeneralDaConfig>, 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<u16, HashSet<PeerId>> = 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, &params, &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, &params, &ports).unwrap_err();
assert!(matches!(err, DaConfigError::PortsLenMismatch { .. }));
}
}

View File

@ -94,6 +94,10 @@ fn initial_peers_by_network_layout(
swarm_configs: &[SwarmConfig],
network_params: &NetworkParams,
) -> Vec<Vec<Multiaddr>> {
if swarm_configs.is_empty() {
return Vec::new();
}
let mut all_initial_peers = vec![];
match network_params.libp2p_network_layout {