configs: make consensus/genesis generation fallible

This commit is contained in:
andrussal 2025-12-18 22:12:28 +01:00
parent c46f815293
commit 17180d8c37
5 changed files with 191 additions and 90 deletions

View File

@ -8,6 +8,8 @@ use super::{
#[derive(Debug, Error)]
pub enum BaseConfigError {
#[error(transparent)]
Consensus(#[from] consensus::ConsensusConfigError),
#[error(transparent)]
Da(#[from] da::DaConfigError),
#[error(transparent)]
@ -36,7 +38,7 @@ pub fn build_base_configs(
ids,
consensus_params,
wallet_config,
),
)?,
bootstrap_configs: bootstrap::create_bootstrap_configs(
ids,
SHORT_PROLONGED_BOOTSTRAP_PERIOD,

View File

@ -29,6 +29,18 @@ use num_bigint::BigUint;
use super::wallet::{WalletAccount, WalletConfig};
#[derive(Debug, thiserror::Error)]
pub enum ConsensusConfigError {
#[error("failed to build genesis inscription signer: {message}")]
InscriptionSigner { message: String },
#[error("failed to build genesis transaction: {message}")]
GenesisTx { message: String },
#[error("failed to build ledger config: {message}")]
LedgerConfig { message: String },
#[error("failed to sign genesis declarations: {message}")]
DeclarationSignature { message: String },
}
#[derive(Clone)]
pub struct ConsensusParams {
pub n_participants: usize,
@ -43,31 +55,18 @@ impl ConsensusParams {
#[must_use]
pub fn default_for_participants(n_participants: usize) -> Self {
let active_slot_coeff = env::var(Self::CONSENSUS_ACTIVE_SLOT_COEFF_VAR)
.map(|s| {
f64::from_str(&s).unwrap_or_else(|err| {
panic!(
"invalid {}='{}' (expected a float in (0.0, 1.0]): {err}",
Self::CONSENSUS_ACTIVE_SLOT_COEFF_VAR,
s
)
})
})
.ok()
.and_then(|raw| f64::from_str(&raw).ok())
.filter(|value| (0.0..=1.0).contains(value) && *value > 0.0)
.unwrap_or(Self::DEFAULT_ACTIVE_SLOT_COEFF);
assert!(
(0.0..=1.0).contains(&active_slot_coeff) && active_slot_coeff > 0.0,
"{} must be in (0.0, 1.0], got {}",
Self::CONSENSUS_ACTIVE_SLOT_COEFF_VAR,
active_slot_coeff
);
Self {
n_participants,
// by setting the slot coeff to 1, we also increase the probability of multiple blocks
// (forks) being produced in the same slot (epoch). Setting the security
// parameter to some value > 1 ensures nodes have some time to sync before
// deciding on the longest chain.
security_param: NonZero::new(10).unwrap(),
security_param: unsafe { NonZero::new_unchecked(10) },
// a block should be produced (on average) every slot
active_slot_coeff,
}
@ -116,13 +115,17 @@ pub struct ServiceNote {
pub output_index: usize,
}
fn create_genesis_tx(utxos: &[Utxo]) -> GenesisTx {
fn create_genesis_tx(utxos: &[Utxo]) -> Result<GenesisTx, ConsensusConfigError> {
// Create a genesis inscription op (similar to config.yaml)
let inscription = InscriptionOp {
channel_id: ChannelId::from([0; 32]),
inscription: vec![103, 101, 110, 101, 115, 105, 115], // "genesis" in bytes
parent: MsgId::root(),
signer: Ed25519PublicKey::from_bytes(&[0; 32]).unwrap(),
signer: Ed25519PublicKey::from_bytes(&[0; 32]).map_err(|err| {
ConsensusConfigError::InscriptionSigner {
message: err.to_string(),
}
})?,
};
// Create ledger transaction with the utxos as outputs
@ -143,15 +146,19 @@ fn create_genesis_tx(utxos: &[Utxo]) -> GenesisTx {
};
// Wrap in GenesisTx
GenesisTx::from_tx(signed_mantle_tx).expect("Invalid genesis transaction")
GenesisTx::from_tx(signed_mantle_tx).map_err(|err| ConsensusConfigError::GenesisTx {
message: err.to_string(),
})
}
fn build_ledger_config(consensus_params: &ConsensusParams) -> nomos_ledger::Config {
nomos_ledger::Config {
fn build_ledger_config(
consensus_params: &ConsensusParams,
) -> Result<nomos_ledger::Config, ConsensusConfigError> {
Ok(nomos_ledger::Config {
epoch_config: EpochConfig {
epoch_stake_distribution_stabilization: NonZero::new(3).unwrap(),
epoch_period_nonce_buffer: NonZero::new(3).unwrap(),
epoch_period_nonce_stabilization: NonZero::new(4).unwrap(),
epoch_stake_distribution_stabilization: unsafe { NonZero::new_unchecked(3) },
epoch_period_nonce_buffer: unsafe { NonZero::new_unchecked(3) },
epoch_period_nonce_stabilization: unsafe { NonZero::new_unchecked(4) },
},
consensus_config: cryptarchia_engine::Config {
security_param: consensus_params.security_param,
@ -189,14 +196,18 @@ fn build_ledger_config(consensus_params: &ConsensusParams) -> nomos_ledger::Conf
},
service_rewards_params: nomos_ledger::mantle::sdp::ServiceRewardsParameters {
blend: nomos_ledger::mantle::sdp::rewards::blend::RewardsParameters {
rounds_per_session: NonZeroU64::new(10).unwrap(),
message_frequency_per_round: NonNegativeF64::try_from(1.0).unwrap(),
num_blend_layers: NonZeroU64::new(3).unwrap(),
minimum_network_size: NonZeroU64::new(1).unwrap(),
rounds_per_session: unsafe { NonZeroU64::new_unchecked(10) },
message_frequency_per_round: NonNegativeF64::try_from(1.0).map_err(|_| {
ConsensusConfigError::LedgerConfig {
message: "message_frequency_per_round must be non-negative".to_owned(),
}
})?,
num_blend_layers: unsafe { NonZeroU64::new_unchecked(3) },
minimum_network_size: unsafe { NonZeroU64::new_unchecked(1) },
},
},
},
}
})
}
#[must_use]
@ -204,7 +215,7 @@ pub fn create_consensus_configs(
ids: &[[u8; 32]],
consensus_params: &ConsensusParams,
wallet: &WalletConfig,
) -> Vec<GeneralConsensusConfig> {
) -> Result<Vec<GeneralConsensusConfig>, ConsensusConfigError> {
let mut leader_keys = Vec::new();
let mut blend_notes = Vec::new();
let mut da_notes = Vec::new();
@ -216,10 +227,10 @@ pub fn create_consensus_configs(
&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);
let genesis_tx = create_genesis_tx(&utxos)?;
let ledger_config = build_ledger_config(consensus_params)?;
leader_keys
Ok(leader_keys
.into_iter()
.map(|(pk, sk)| GeneralConsensusConfig {
leader_config: LeaderConfig { pk, sk },
@ -230,7 +241,7 @@ pub fn create_consensus_configs(
blend_notes: blend_notes.clone(),
wallet_accounts: wallet.accounts.clone(),
})
.collect()
.collect())
}
fn create_utxos_for_leader_and_services(
@ -324,12 +335,16 @@ fn append_wallet_utxos(mut utxos: Vec<Utxo>, wallet: &WalletConfig) -> Vec<Utxo>
pub fn create_genesis_tx_with_declarations(
ledger_tx: LedgerTx,
providers: Vec<ProviderInfo>,
) -> GenesisTx {
) -> Result<GenesisTx, ConsensusConfigError> {
let inscription = InscriptionOp {
channel_id: ChannelId::from([0; 32]),
inscription: vec![103, 101, 110, 101, 115, 105, 115], // "genesis" in bytes
parent: MsgId::root(),
signer: Ed25519PublicKey::from_bytes(&[0; 32]).unwrap(),
signer: Ed25519PublicKey::from_bytes(&[0; 32]).map_err(|err| {
ConsensusConfigError::InscriptionSigner {
message: err.to_string(),
}
})?,
};
let ledger_tx_hash = ledger_tx.hash();
@ -365,7 +380,9 @@ pub fn create_genesis_tx_with_declarations(
for provider in providers {
let zk_sig =
ZkKey::multi_sign(&[provider.note.sk, provider.zk_sk], mantle_tx_hash.as_ref())
.unwrap();
.map_err(|err| ConsensusConfigError::DeclarationSignature {
message: format!("{err:?}"),
})?;
let ed25519_sig = provider
.provider_sk
.sign_payload(mantle_tx_hash.as_signing_bytes().as_ref());
@ -382,5 +399,7 @@ pub fn create_genesis_tx_with_declarations(
ledger_tx_proof: ZkSignature::new(CompressedGroth16Proof::from_bytes(&[0u8; 128])),
};
GenesisTx::from_tx(signed_mantle_tx).expect("Invalid genesis transaction")
GenesisTx::from_tx(signed_mantle_tx).map_err(|err| ConsensusConfigError::GenesisTx {
message: err.to_string(),
})
}

View File

@ -9,8 +9,12 @@ pub mod time;
pub mod tracing;
pub mod wallet;
use std::cmp;
use blend::GeneralBlendConfig;
use consensus::{GeneralConsensusConfig, ProviderInfo, create_genesis_tx_with_declarations};
use consensus::{
ConsensusConfigError, GeneralConsensusConfig, ProviderInfo, create_genesis_tx_with_declarations,
};
use da::GeneralDaConfig;
use key_management_system_service::{backend::preload::PreloadKMSBackendSettings, keys::Key};
use network::GeneralNetworkConfig;
@ -38,6 +42,29 @@ use crate::{
},
};
#[derive(Debug, thiserror::Error)]
pub enum GeneralConfigError {
#[error("participant count must be > 0")]
EmptyParticipants,
#[error("blend core subset {blend_core} exceeds participants {participants}")]
BlendCoreSubsetTooLarge {
blend_core: usize,
participants: usize,
},
#[error("failed to allocate a free UDP port for {label}")]
PortAllocationFailed { label: &'static str },
#[error(transparent)]
Invariants(#[from] crate::topology::invariants::TopologyInvariantError),
#[error(transparent)]
Consensus(#[from] ConsensusConfigError),
#[error(transparent)]
Network(#[from] network::NetworkConfigError),
#[error(transparent)]
Da(#[from] da::DaConfigError),
#[error(transparent)]
Api(#[from] api::ApiConfigError),
}
#[derive(Clone)]
pub struct GeneralConfig {
pub api_config: GeneralApiConfig,
@ -51,79 +78,91 @@ pub struct GeneralConfig {
pub kms_config: PreloadKMSBackendSettings,
}
#[must_use]
pub fn create_general_configs(n_nodes: usize) -> Vec<GeneralConfig> {
pub fn create_general_configs(n_nodes: usize) -> Result<Vec<GeneralConfig>, GeneralConfigError> {
create_general_configs_with_network(n_nodes, &NetworkParams::default())
}
#[must_use]
pub fn create_general_configs_with_network(
n_nodes: usize,
network_params: &NetworkParams,
) -> Vec<GeneralConfig> {
) -> Result<Vec<GeneralConfig>, GeneralConfigError> {
create_general_configs_with_blend_core_subset(n_nodes, n_nodes, network_params)
}
#[must_use]
pub fn create_general_configs_with_blend_core_subset(
n_nodes: usize,
// TODO: Instead of this, define a config struct for each node.
// That would be also useful for non-even token distributions: https://github.com/logos-co/nomos/issues/1888
n_blend_core_nodes: usize,
network_params: &NetworkParams,
) -> Vec<GeneralConfig> {
assert!(
n_blend_core_nodes <= n_nodes,
"n_blend_core_nodes({n_blend_core_nodes}) must be less than or equal to n_nodes({n_nodes})",
);
) -> Result<Vec<GeneralConfig>, GeneralConfigError> {
if n_nodes == 0 {
return Err(GeneralConfigError::EmptyParticipants);
}
if n_blend_core_nodes > n_nodes {
return Err(GeneralConfigError::BlendCoreSubsetTooLarge {
blend_core: n_blend_core_nodes,
participants: n_nodes,
});
}
// Blend relies on each node declaring a different ZK public key, so we need
// different IDs to generate different keys.
let mut ids: Vec<_> = (0..n_nodes).map(|i| [i as u8; 32]).collect();
let mut da_ports = vec![];
let mut blend_ports = vec![];
let mut da_ports = Vec::with_capacity(n_nodes);
let mut blend_ports = Vec::with_capacity(n_nodes);
for id in &mut ids {
thread_rng().fill(id);
da_ports.push(get_available_udp_port().unwrap());
blend_ports.push(get_available_udp_port().unwrap());
da_ports.push(
get_available_udp_port()
.ok_or(GeneralConfigError::PortAllocationFailed { label: "DA" })?,
);
blend_ports.push(
get_available_udp_port()
.ok_or(GeneralConfigError::PortAllocationFailed { label: "Blend" })?,
);
}
validate_generated_vectors(n_nodes, &ids, &da_ports, &blend_ports)
.expect("invalid generated test topology inputs");
validate_generated_vectors(n_nodes, &ids, &da_ports, &blend_ports)?;
let consensus_params = ConsensusParams::default_for_participants(n_nodes);
let mut consensus_configs =
consensus::create_consensus_configs(&ids, &consensus_params, &WalletConfig::default());
consensus::create_consensus_configs(&ids, &consensus_params, &WalletConfig::default())?;
let bootstrap_config =
bootstrap::create_bootstrap_configs(&ids, SHORT_PROLONGED_BOOTSTRAP_PERIOD);
let network_configs =
network::create_network_configs(&ids, network_params).expect("network config generation");
let da_configs =
da::try_create_da_configs(&ids, &DaParams::default(), &da_ports).expect("DA configs");
let api_configs = api::create_api_configs(&ids).expect("api config generation");
let network_configs = network::create_network_configs(&ids, network_params)?;
let da_configs = da::try_create_da_configs(&ids, &DaParams::default(), &da_ports)?;
let api_configs = api::create_api_configs(&ids)?;
let blend_configs = blend::create_blend_configs(&ids, &blend_ports);
let tracing_configs = tracing::create_tracing_configs(&ids);
let time_config = time::default_time_config();
let providers: Vec<_> = blend_configs
let Some(first_consensus) = consensus_configs.first() else {
return Err(GeneralConfigError::EmptyParticipants);
};
let mut providers = Vec::with_capacity(cmp::min(n_blend_core_nodes, blend_configs.len()));
for (i, blend_conf) in blend_configs
.iter()
.enumerate()
.take(n_blend_core_nodes)
.map(|(i, blend_conf)| ProviderInfo {
.take(cmp::min(n_blend_core_nodes, blend_configs.len()))
{
let note = first_consensus
.blend_notes
.get(i)
.ok_or(GeneralConfigError::EmptyParticipants)?
.clone();
providers.push(ProviderInfo {
service_type: ServiceType::BlendNetwork,
provider_sk: blend_conf.signer.clone(),
zk_sk: blend_conf.secret_zk_key.clone(),
locator: Locator(blend_conf.backend_core.listening_address.clone()),
note: consensus_configs[0].blend_notes[i].clone(),
})
.collect();
let ledger_tx = consensus_configs[0]
.genesis_tx
.mantle_tx()
.ledger_tx
.clone();
let genesis_tx = create_genesis_tx_with_declarations(ledger_tx, providers);
note,
});
}
let ledger_tx = first_consensus.genesis_tx.mantle_tx().ledger_tx.clone();
let genesis_tx = create_genesis_tx_with_declarations(ledger_tx, providers)?;
for c in &mut consensus_configs {
c.genesis_tx = genesis_tx.clone();
}
@ -150,21 +189,54 @@ pub fn create_general_configs_with_blend_core_subset(
})
.collect();
let mut general_configs = vec![];
let mut general_configs = Vec::with_capacity(n_nodes);
for i in 0..n_nodes {
let api_config = api_configs
.get(i)
.ok_or(GeneralConfigError::EmptyParticipants)?
.clone();
let consensus_config = consensus_configs
.get(i)
.ok_or(GeneralConfigError::EmptyParticipants)?
.clone();
let bootstrapping_config = bootstrap_config
.get(i)
.ok_or(GeneralConfigError::EmptyParticipants)?
.clone();
let da_config = da_configs
.get(i)
.ok_or(GeneralConfigError::EmptyParticipants)?
.clone();
let network_config = network_configs
.get(i)
.ok_or(GeneralConfigError::EmptyParticipants)?
.clone();
let blend_config = blend_configs
.get(i)
.ok_or(GeneralConfigError::EmptyParticipants)?
.clone();
let tracing_config = tracing_configs
.get(i)
.ok_or(GeneralConfigError::EmptyParticipants)?
.clone();
let kms_config = kms_configs
.get(i)
.ok_or(GeneralConfigError::EmptyParticipants)?
.clone();
general_configs.push(GeneralConfig {
api_config: api_configs[i].clone(),
consensus_config: consensus_configs[i].clone(),
bootstrapping_config: bootstrap_config[i].clone(),
da_config: da_configs[i].clone(),
network_config: network_configs[i].clone(),
blend_config: blend_configs[i].clone(),
tracing_config: tracing_configs[i].clone(),
api_config,
consensus_config,
bootstrapping_config,
da_config,
network_config,
blend_config,
tracing_config,
time_config: time_config.clone(),
kms_config: kms_configs[i].clone(),
kms_config,
});
}
general_configs
Ok(general_configs)
}

View File

@ -309,9 +309,15 @@ impl TopologyBuilder {
.mantle_tx()
.ledger_tx
.clone();
let genesis_tx = create_genesis_tx_with_declarations(ledger_tx, providers);
for c in &mut consensus_configs {
c.genesis_tx = genesis_tx.clone();
match create_genesis_tx_with_declarations(ledger_tx, providers) {
Ok(genesis_tx) => {
for c in &mut consensus_configs {
c.genesis_tx = genesis_tx.clone();
}
}
Err(err) => {
tracing::error!(error = ?err, "failed to build genesis declarations; using base genesis transaction");
}
}
let kms_configs =

View File

@ -9,7 +9,7 @@ use testing_framework_config::topology::configs::{
GeneralConfig,
api::GeneralApiConfig,
base::{BaseConfigError, BaseConfigs, build_base_configs},
consensus::{ConsensusParams, create_genesis_tx_with_declarations},
consensus::{ConsensusConfigError, ConsensusParams, create_genesis_tx_with_declarations},
da::DaParams,
network::NetworkParams,
time::default_time_config,
@ -60,6 +60,8 @@ pub enum NodeConfigBuildError {
Providers(#[from] ProviderBuildError),
#[error(transparent)]
Base(#[from] BaseConfigError),
#[error(transparent)]
Genesis(#[from] ConsensusConfigError),
#[error("failed to allocate an available UDP port")]
PortAllocFailed,
#[error("failed to parse multiaddr '{value}': {message}")]
@ -143,7 +145,7 @@ pub fn try_create_node_configs(
.get(0)
.ok_or(NodeConfigBuildError::MissingConsensusConfig)?;
let ledger_tx = first_consensus.genesis_tx.mantle_tx().ledger_tx.clone();
let genesis_tx = create_genesis_tx_with_declarations(ledger_tx, providers);
let genesis_tx = create_genesis_tx_with_declarations(ledger_tx, providers)?;
for c in &mut consensus_configs {
c.genesis_tx = genesis_tx.clone();