Skip to main content

fedimint_server/config/
mod.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::net::SocketAddr;
3use std::sync::Arc;
4use std::time::{Duration, Instant};
5
6use anyhow::{Context, bail, format_err};
7use bitcoin::hashes::sha256;
8pub use fedimint_core::config::{
9    ClientConfig, FederationId, GlobalClientConfig, JsonWithKind, ModuleInitRegistry, P2PMessage,
10    PeerUrl, ServerModuleConfig, ServerModuleConsensusConfig, TypedServerModuleConfig,
11};
12use fedimint_core::core::{ModuleInstanceId, ModuleKind};
13use fedimint_core::envs::{is_env_var_set, is_running_in_test_env};
14use fedimint_core::module::registry::ModuleRegistry;
15use fedimint_core::module::{
16    ApiVersion, CORE_CONSENSUS_VERSION, CoreConsensusVersion, MultiApiVersion,
17    SupportedApiVersionsSummary, SupportedCoreApiVersions, SupportedModuleApiVersions,
18};
19use fedimint_core::net::peers::{DynP2PConnections, Recipient};
20use fedimint_core::setup_code::{PeerEndpoints, PeerSetupCode};
21use fedimint_core::task::sleep;
22use fedimint_core::util::SafeUrl;
23use fedimint_core::{NumPeersExt, PeerId, secp256k1, timing};
24use fedimint_logging::LOG_NET_PEER_DKG;
25use fedimint_server_core::config::PeerHandleOpsExt as _;
26use fedimint_server_core::{
27    ConfigGenModuleArgs, DynServerModule, DynServerModuleInit, ServerModuleInitRegistry,
28};
29use futures::future::select_all;
30use hex::{FromHex, ToHex};
31use peer_handle::PeerHandle;
32use rand::rngs::OsRng;
33use secp256k1::{PublicKey, Secp256k1, SecretKey};
34use serde::{Deserialize, Serialize};
35use tokio::select;
36use tokio_rustls::rustls;
37use tracing::{error, info, warn};
38
39use crate::fedimint_core::encoding::Encodable;
40use crate::net::p2p::P2PStatusReceivers;
41use crate::net::p2p_connector::TlsConfig;
42
43pub mod dkg;
44pub mod dkg_g1;
45pub mod dkg_g2;
46pub mod io;
47pub mod peer_handle;
48pub mod setup;
49
50/// The default maximum open connections the API can handle
51pub const DEFAULT_MAX_CLIENT_CONNECTIONS: u32 = 1000;
52
53/// Consensus broadcast settings that result in 3 minutes session time
54const DEFAULT_BROADCAST_ROUND_DELAY_MS: u16 = 50;
55const DEFAULT_BROADCAST_ROUNDS_PER_SESSION: u16 = 3600;
56
57fn default_broadcast_rounds_per_session() -> u16 {
58    DEFAULT_BROADCAST_ROUNDS_PER_SESSION
59}
60
61/// Consensus broadcast settings that result in 10 seconds session time
62const DEFAULT_TEST_BROADCAST_ROUND_DELAY_MS: u16 = 50;
63const DEFAULT_TEST_BROADCAST_ROUNDS_PER_SESSION: u16 = 200;
64
65#[allow(clippy::unsafe_derive_deserialize)] // clippy fires on `select!` https://github.com/rust-lang/rust-clippy/issues/13062
66#[derive(Debug, Clone, Serialize, Deserialize)]
67/// All the serializable configuration for the fedimint server
68pub struct ServerConfig {
69    /// Contains all configuration that needs to be the same for every server
70    pub consensus: ServerConfigConsensus,
71    /// Contains all configuration that is locally configurable and not secret
72    pub local: ServerConfigLocal,
73    /// Contains all configuration that will be encrypted such as private key
74    /// material
75    pub private: ServerConfigPrivate,
76}
77
78impl ServerConfig {
79    pub fn iter_module_instances(
80        &self,
81    ) -> impl Iterator<Item = (ModuleInstanceId, &ModuleKind)> + '_ {
82        self.consensus.iter_module_instances()
83    }
84
85    pub(crate) fn supported_api_versions_summary(
86        modules: &BTreeMap<ModuleInstanceId, ServerModuleConsensusConfig>,
87        module_registry: &ModuleRegistry<DynServerModule>,
88    ) -> SupportedApiVersionsSummary {
89        SupportedApiVersionsSummary {
90            core: Self::supported_api_versions(),
91            modules: modules
92                .iter()
93                .map(|(&id, config)| {
94                    let module = module_registry.get_expect(id);
95                    (
96                        id,
97                        SupportedModuleApiVersions {
98                            core_consensus: CORE_CONSENSUS_VERSION,
99                            module_consensus: config.version,
100                            api: module.supported_api_versions(),
101                        },
102                    )
103                })
104                .collect(),
105        }
106    }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct ServerConfigPrivate {
111    /// Optional secret key for our websocket p2p endpoint
112    pub tls_key: Option<String>,
113    /// Optional secret key for our iroh api endpoint
114    #[serde(default)]
115    pub iroh_api_sk: Option<iroh::SecretKey>,
116    /// Optional secret key for our iroh p2p endpoint
117    #[serde(default)]
118    pub iroh_p2p_sk: Option<iroh::SecretKey>,
119    /// Secret key for the atomic broadcast to sign messages
120    pub broadcast_secret_key: SecretKey,
121    /// Secret material from modules
122    pub modules: BTreeMap<ModuleInstanceId, JsonWithKind>,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize, Encodable)]
126pub struct ServerConfigConsensus {
127    /// The normalized `x.y.z` release version used for consensus config
128    /// checksums
129    pub code_version: String,
130    /// Agreed on core consensus version
131    pub version: CoreConsensusVersion,
132    /// Public keys for the atomic broadcast to authenticate messages
133    pub broadcast_public_keys: BTreeMap<PeerId, PublicKey>,
134    /// Number of rounds per session.
135    #[serde(default = "default_broadcast_rounds_per_session")]
136    pub broadcast_rounds_per_session: u16,
137    /// Network addresses and names for all peer APIs
138    pub api_endpoints: BTreeMap<PeerId, PeerUrl>,
139    /// Public keys for all iroh api and p2p endpoints
140    #[serde(default)]
141    pub iroh_endpoints: BTreeMap<PeerId, PeerIrohEndpoints>,
142    /// Certs for TLS communication, required for peer authentication
143    pub tls_certs: BTreeMap<PeerId, String>,
144    /// All configuration that needs to be the same for modules
145    pub modules: BTreeMap<ModuleInstanceId, ServerModuleConsensusConfig>,
146    /// Additional config the federation wants to transmit to the clients
147    pub meta: BTreeMap<String, String>,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, Encodable)]
151pub struct PeerIrohEndpoints {
152    /// The peer's name
153    pub name: String,
154    /// Public key for our iroh api endpoint
155    pub api_pk: iroh::PublicKey,
156    /// Public key for our iroh p2p endpoint
157    pub p2p_pk: iroh::PublicKey,
158}
159
160pub fn legacy_consensus_config_hash(cfg: &ServerConfigConsensus) -> sha256::Hash {
161    #[derive(Encodable)]
162    struct LegacyServerConfigConsensusHashMap {
163        code_version: String,
164        version: CoreConsensusVersion,
165        broadcast_public_keys: BTreeMap<PeerId, PublicKey>,
166        broadcast_rounds_per_session: u16,
167        api_endpoints: BTreeMap<PeerId, PeerUrl>,
168        tls_certs: BTreeMap<PeerId, String>,
169        modules: BTreeMap<ModuleInstanceId, ServerModuleConsensusConfig>,
170        meta: BTreeMap<String, String>,
171    }
172
173    LegacyServerConfigConsensusHashMap {
174        code_version: cfg.code_version.clone(),
175        version: cfg.version,
176        broadcast_public_keys: cfg.broadcast_public_keys.clone(),
177        broadcast_rounds_per_session: cfg.broadcast_rounds_per_session,
178        api_endpoints: cfg.api_endpoints.clone(),
179        tls_certs: cfg.tls_certs.clone(),
180        modules: cfg.modules.clone(),
181        meta: cfg.meta.clone(),
182    }
183    .consensus_hash_sha256()
184}
185
186// FIXME: (@leonardo) Should this have another field for the expected transport
187// ? (e.g. clearnet/tor/...)
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct ServerConfigLocal {
190    /// Network addresses and names for all p2p connections
191    pub p2p_endpoints: BTreeMap<PeerId, PeerUrl>,
192    /// Our peer id (generally should not change)
193    pub identity: PeerId,
194    /// How many API connections we will accept
195    pub max_connections: u32,
196    /// Influences the atomic broadcast ordering latency, should be higher than
197    /// the expected latency between peers so everyone can get proposed
198    /// consensus items confirmed. This is only relevant for byzantine
199    /// faults.
200    pub broadcast_round_delay_ms: u16,
201}
202
203/// All the info we configure prior to config gen starting
204#[derive(Debug, Clone)]
205pub struct ConfigGenSettings {
206    /// Bind address for our P2P connection (both iroh and tcp/tls)
207    pub p2p_bind: SocketAddr,
208    /// Bind address for our API
209    pub api_bind: SocketAddr,
210    /// Bind address for our UI connection (always http)
211    pub ui_bind: SocketAddr,
212    /// URL for our P2P connection
213    pub p2p_url: Option<SafeUrl>,
214    /// URL for our API connection
215    pub api_url: Option<SafeUrl>,
216    /// Enable iroh for networking
217    pub enable_iroh: bool,
218    /// Optional URL of the Iroh DNS server
219    pub iroh_dns: Option<SafeUrl>,
220    /// Optional URLs of the Iroh relays to register on
221    pub iroh_relays: Vec<SafeUrl>,
222    /// Bitcoin network for the federation
223    pub network: bitcoin::Network,
224    /// Available modules that can be enabled during setup
225    pub available_modules: BTreeSet<ModuleKind>,
226    /// Modules that should be enabled by default in the setup UI
227    pub default_modules: BTreeSet<ModuleKind>,
228}
229
230#[derive(Debug, Clone)]
231/// All the parameters necessary for generating the `ServerConfig` during setup
232///
233/// * Guardians can create the parameters using a setup UI or CLI tool
234/// * Used for distributed or trusted config generation
235pub struct ConfigGenParams {
236    /// Our own peer id
237    pub identity: PeerId,
238    /// Our TLS certificate private key
239    pub tls_key: Option<Arc<rustls::pki_types::PrivateKeyDer<'static>>>,
240    /// Optional secret key for our iroh api endpoint
241    pub iroh_api_sk: Option<iroh::SecretKey>,
242    /// Optional secret key for our iroh p2p endpoint
243    pub iroh_p2p_sk: Option<iroh::SecretKey>,
244    /// Endpoints of all servers
245    pub peers: BTreeMap<PeerId, PeerSetupCode>,
246    /// Guardian-defined key-value pairs that will be passed to the client
247    pub meta: BTreeMap<String, String>,
248    /// Whether to disable base fees for this federation
249    pub disable_base_fees: bool,
250    /// Modules enabled by the leader during setup
251    pub enabled_modules: BTreeSet<ModuleKind>,
252    /// Bitcoin network for this federation
253    pub network: bitcoin::Network,
254}
255
256impl ServerConfigConsensus {
257    pub fn api_endpoints(&self) -> BTreeMap<PeerId, PeerUrl> {
258        if self.iroh_endpoints.is_empty() {
259            self.api_endpoints.clone()
260        } else {
261            self.iroh_endpoints
262                .iter()
263                .map(|(peer, endpoints)| {
264                    let url = PeerUrl {
265                        name: endpoints.name.clone(),
266                        url: SafeUrl::parse(&format!("iroh://{}", endpoints.api_pk))
267                            .expect("Failed to parse iroh url"),
268                    };
269
270                    (*peer, url)
271                })
272                .collect()
273        }
274    }
275
276    pub fn iter_module_instances(
277        &self,
278    ) -> impl Iterator<Item = (ModuleInstanceId, &ModuleKind)> + '_ {
279        self.modules.iter().map(|(k, v)| (*k, &v.kind))
280    }
281
282    pub fn to_client_config(
283        &self,
284        module_config_gens: &ModuleInitRegistry<DynServerModuleInit>,
285    ) -> Result<ClientConfig, anyhow::Error> {
286        let client = ClientConfig {
287            global: GlobalClientConfig {
288                api_endpoints: self.api_endpoints(),
289                broadcast_public_keys: Some(self.broadcast_public_keys.clone()),
290                consensus_version: self.version,
291                meta: self.meta.clone(),
292            },
293            modules: self
294                .modules
295                .iter()
296                .map(|(k, v)| {
297                    let r#gen = module_config_gens
298                        .get(&v.kind)
299                        .ok_or_else(|| format_err!("Module gen kind={} not found", v.kind))?;
300                    Ok((*k, r#gen.get_client_config(*k, v)?))
301                })
302                .collect::<anyhow::Result<BTreeMap<_, _>>>()?,
303        };
304        Ok(client)
305    }
306}
307
308impl ServerConfig {
309    /// Api versions supported by this server
310    pub fn supported_api_versions() -> SupportedCoreApiVersions {
311        SupportedCoreApiVersions {
312            core_consensus: CORE_CONSENSUS_VERSION,
313            api: MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 9 }])
314                .expect("not version conflicts"),
315        }
316    }
317    /// Creates a new config from the results of a trusted or distributed key
318    /// setup
319    pub fn from(
320        params: ConfigGenParams,
321        identity: PeerId,
322        broadcast_public_keys: BTreeMap<PeerId, PublicKey>,
323        broadcast_secret_key: SecretKey,
324        modules: BTreeMap<ModuleInstanceId, ServerModuleConfig>,
325        code_version: String,
326    ) -> Self {
327        let consensus = ServerConfigConsensus {
328            code_version: fedimint_core::version::release_version(&code_version).to_owned(),
329            version: CORE_CONSENSUS_VERSION,
330            broadcast_public_keys,
331            broadcast_rounds_per_session: if is_running_in_test_env() {
332                DEFAULT_TEST_BROADCAST_ROUNDS_PER_SESSION
333            } else {
334                DEFAULT_BROADCAST_ROUNDS_PER_SESSION
335            },
336            api_endpoints: params.api_urls(),
337            iroh_endpoints: params.iroh_endpoints(),
338            tls_certs: params.tls_certs(),
339            modules: modules
340                .iter()
341                .map(|(peer, cfg)| (*peer, cfg.consensus.clone()))
342                .collect(),
343            meta: params.meta.clone(),
344        };
345
346        let local = ServerConfigLocal {
347            p2p_endpoints: params.p2p_urls(),
348            identity,
349            max_connections: DEFAULT_MAX_CLIENT_CONNECTIONS,
350            broadcast_round_delay_ms: if is_running_in_test_env() {
351                DEFAULT_TEST_BROADCAST_ROUND_DELAY_MS
352            } else {
353                DEFAULT_BROADCAST_ROUND_DELAY_MS
354            },
355        };
356
357        let private = ServerConfigPrivate {
358            tls_key: params
359                .tls_key
360                .map(|key| key.secret_der().to_vec().encode_hex()),
361            iroh_api_sk: params.iroh_api_sk,
362            iroh_p2p_sk: params.iroh_p2p_sk,
363            broadcast_secret_key,
364            modules: modules
365                .iter()
366                .map(|(peer, cfg)| (*peer, cfg.private.clone()))
367                .collect(),
368        };
369
370        Self {
371            consensus,
372            local,
373            private,
374        }
375    }
376
377    pub fn calculate_federation_id(&self) -> FederationId {
378        FederationId(self.consensus.api_endpoints().consensus_hash())
379    }
380
381    /// Constructs a module config by name
382    pub fn get_module_config_typed<T: TypedServerModuleConfig>(
383        &self,
384        id: ModuleInstanceId,
385    ) -> anyhow::Result<T> {
386        let private = Self::get_module_cfg_by_instance_id(&self.private.modules, id)?;
387        let consensus = self
388            .consensus
389            .modules
390            .get(&id)
391            .ok_or_else(|| format_err!("Typed module {id} not found"))?
392            .clone();
393        let module = ServerModuleConfig::from(private, consensus);
394
395        module.to_typed()
396    }
397    pub fn get_module_id_by_kind(
398        &self,
399        kind: impl Into<ModuleKind>,
400    ) -> anyhow::Result<ModuleInstanceId> {
401        let kind = kind.into();
402        Ok(*self
403            .consensus
404            .modules
405            .iter()
406            .find(|(_, v)| v.kind == kind)
407            .ok_or_else(|| format_err!("Module {kind} not found"))?
408            .0)
409    }
410
411    /// Constructs a module config by id
412    pub fn get_module_config(&self, id: ModuleInstanceId) -> anyhow::Result<ServerModuleConfig> {
413        let private = Self::get_module_cfg_by_instance_id(&self.private.modules, id)?;
414        let consensus = self
415            .consensus
416            .modules
417            .get(&id)
418            .ok_or_else(|| format_err!("Module config {id} not found"))?
419            .clone();
420        Ok(ServerModuleConfig::from(private, consensus))
421    }
422
423    fn get_module_cfg_by_instance_id(
424        json: &BTreeMap<ModuleInstanceId, JsonWithKind>,
425        id: ModuleInstanceId,
426    ) -> anyhow::Result<JsonWithKind> {
427        Ok(json
428            .get(&id)
429            .ok_or_else(|| format_err!("Module cfg {id} not found"))
430            .cloned()?
431            .with_fixed_empty_value())
432    }
433
434    pub fn validate_config(
435        &self,
436        identity: &PeerId,
437        module_config_gens: &ServerModuleInitRegistry,
438    ) -> anyhow::Result<()> {
439        let endpoints = self.consensus.api_endpoints().clone();
440        let consensus = self.consensus.clone();
441        let private = self.private.clone();
442
443        let my_public_key = private.broadcast_secret_key.public_key(&Secp256k1::new());
444
445        if Some(&my_public_key) != consensus.broadcast_public_keys.get(identity) {
446            bail!("Broadcast secret key doesn't match corresponding public key");
447        }
448        if endpoints.keys().max().copied().map(PeerId::to_usize) != Some(endpoints.len() - 1) {
449            bail!("Peer ids are not indexed from 0");
450        }
451        if endpoints.keys().min().copied() != Some(PeerId::from(0)) {
452            bail!("Peer ids are not indexed from 0");
453        }
454
455        for (module_id, module_kind) in &self
456            .consensus
457            .modules
458            .iter()
459            .map(|(id, config)| Ok((*id, config.kind.clone())))
460            .collect::<anyhow::Result<BTreeSet<_>>>()?
461        {
462            module_config_gens
463                .get(module_kind)
464                .ok_or_else(|| format_err!("module config gen not found {module_kind}"))?
465                .validate_config(identity, self.get_module_config(*module_id)?)?;
466        }
467
468        Ok(())
469    }
470
471    pub fn trusted_dealer_gen(
472        params: &BTreeMap<PeerId, ConfigGenParams>,
473        registry: &ServerModuleInitRegistry,
474        code_version_str: &str,
475    ) -> BTreeMap<PeerId, Self> {
476        let peer0 = &params[&PeerId::from(0)];
477
478        let mut broadcast_pks = BTreeMap::new();
479        let mut broadcast_sks = BTreeMap::new();
480        for peer_id in peer0.peer_ids() {
481            let (broadcast_sk, broadcast_pk) = secp256k1::generate_keypair(&mut OsRng);
482            broadcast_pks.insert(peer_id, broadcast_pk);
483            broadcast_sks.insert(peer_id, broadcast_sk);
484        }
485
486        let args = ConfigGenModuleArgs {
487            network: peer0.network,
488            disable_base_fees: peer0.disable_base_fees,
489        };
490
491        // Use legacy module ordering for backwards compatibility tests
492        let use_legacy_order = is_env_var_set("FM_BACKWARDS_COMPATIBILITY_TEST");
493        let module_iter: Vec<_> = if use_legacy_order {
494            registry.iter_legacy_order()
495        } else {
496            registry.iter().collect()
497        };
498
499        let module_configs: BTreeMap<_, _> = module_iter
500            .into_iter()
501            .filter(|(kind, _)| peer0.enabled_modules.contains(kind))
502            .enumerate()
503            .map(|(module_id, (_kind, module_init))| {
504                (
505                    module_id as ModuleInstanceId,
506                    module_init.trusted_dealer_gen(&peer0.peer_ids(), &args),
507                )
508            })
509            .collect();
510
511        let server_config: BTreeMap<_, _> = peer0
512            .peer_ids()
513            .iter()
514            .map(|&id| {
515                let config = ServerConfig::from(
516                    params[&id].clone(),
517                    id,
518                    broadcast_pks.clone(),
519                    *broadcast_sks.get(&id).expect("We created this entry"),
520                    module_configs
521                        .iter()
522                        .map(|(module_id, cfgs)| (*module_id, cfgs[&id].clone()))
523                        .collect(),
524                    code_version_str.to_string(),
525                );
526                (id, config)
527            })
528            .collect();
529
530        server_config
531    }
532
533    /// Runs the distributed key gen algorithm
534    pub async fn distributed_gen(
535        params: &ConfigGenParams,
536        registry: ServerModuleInitRegistry,
537        code_version_str: String,
538        connections: DynP2PConnections<P2PMessage>,
539        mut p2p_status_receivers: P2PStatusReceivers,
540    ) -> anyhow::Result<Self> {
541        let _timing /* logs on drop */ = timing::TimeReporter::new("distributed-gen").info();
542
543        // in case we are running by ourselves, avoid DKG
544        if params.peer_ids().len() == 1 {
545            let server = Self::trusted_dealer_gen(
546                &BTreeMap::from([(params.identity, params.clone())]),
547                &registry,
548                &code_version_str,
549            );
550
551            return Ok(server[&params.identity].clone());
552        }
553
554        info!(
555            target: LOG_NET_PEER_DKG,
556            "Waiting for all p2p connections to open..."
557        );
558
559        loop {
560            let mut pending_connection_receivers: Vec<_> = p2p_status_receivers
561                .iter_mut()
562                .filter_map(|(p, r)| {
563                    r.mark_unchanged();
564                    r.borrow().connected.is_none().then_some((*p, r.clone()))
565                })
566                .collect();
567
568            if pending_connection_receivers.is_empty() {
569                break;
570            }
571
572            let disconnected_peers = pending_connection_receivers
573                .iter()
574                .map(|(peer, receiver)| {
575                    let last_error = receiver.borrow().last_error.clone();
576
577                    (*peer, last_error)
578                })
579                .collect::<Vec<_>>();
580
581            info!(
582                target: LOG_NET_PEER_DKG,
583                pending = ?disconnected_peers,
584                "Waiting for all p2p connections to open..."
585            );
586
587            select! {
588                _ = select_all(pending_connection_receivers.iter_mut().map(|r| Box::pin(r.1.changed()))) => {}
589                () = sleep(Duration::from_secs(10)) => {}
590            }
591        }
592
593        let checksum = params.peers.consensus_hash_sha256();
594
595        info!(
596            target: LOG_NET_PEER_DKG,
597            "Comparing connection codes checksum {checksum}..."
598        );
599
600        connections.send(Recipient::Everyone, P2PMessage::Checksum(checksum));
601
602        for peer in params
603            .peer_ids()
604            .into_iter()
605            .filter(|p| *p != params.identity)
606        {
607            let peer_message = receive_from_peer_with_progress(
608                &connections,
609                peer,
610                "connection code checksum message",
611            )
612            .await?;
613
614            if peer_message != P2PMessage::Checksum(checksum) {
615                error!(
616                    target: LOG_NET_PEER_DKG,
617                    expected = ?P2PMessage::Checksum(checksum),
618                    received = ?peer_message,
619                    "Peer {peer} has sent invalid connection code checksum message"
620                );
621
622                bail!("Peer {peer} has sent invalid connection code checksum message");
623            }
624
625            info!(
626                target: LOG_NET_PEER_DKG,
627                "Peer {peer} has sent valid connection code checksum message"
628            );
629        }
630
631        info!(
632            target: LOG_NET_PEER_DKG,
633            "Running config generation..."
634        );
635
636        let handle = PeerHandle::new(
637            params.peer_ids().to_num_peers(),
638            params.identity,
639            &connections,
640        );
641
642        let (broadcast_sk, broadcast_pk) = secp256k1::generate_keypair(&mut OsRng);
643
644        let broadcast_public_keys = handle.exchange_encodable(broadcast_pk).await?;
645
646        let args = ConfigGenModuleArgs {
647            network: params.network,
648            disable_base_fees: params.disable_base_fees,
649        };
650
651        // Use legacy module ordering for backwards compatibility tests
652        let use_legacy_order = is_env_var_set("FM_BACKWARDS_COMPATIBILITY_TEST");
653        let module_iter: Vec<_> = if use_legacy_order {
654            registry.iter_legacy_order()
655        } else {
656            registry.iter().collect()
657        };
658
659        let mut module_cfgs = BTreeMap::new();
660
661        for (module_id, (kind, module_init)) in module_iter
662            .into_iter()
663            .filter(|(kind, _)| params.enabled_modules.contains(kind))
664            .enumerate()
665        {
666            info!(
667                target: LOG_NET_PEER_DKG,
668                "Running config generation for module of kind {kind}..."
669            );
670
671            let cfg = module_init.distributed_gen(&handle, &args).await?;
672
673            module_cfgs.insert(module_id as ModuleInstanceId, cfg);
674        }
675
676        let cfg = ServerConfig::from(
677            params.clone(),
678            params.identity,
679            broadcast_public_keys,
680            broadcast_sk,
681            module_cfgs,
682            code_version_str,
683        );
684
685        let checksum = cfg.consensus.consensus_hash_sha256();
686
687        info!(
688            target: LOG_NET_PEER_DKG,
689            "Comparing consensus config checksum {checksum}..."
690        );
691
692        connections.send(Recipient::Everyone, P2PMessage::Checksum(checksum));
693
694        for peer in params
695            .peer_ids()
696            .into_iter()
697            .filter(|p| *p != params.identity)
698        {
699            let peer_message =
700                receive_from_peer_with_progress(&connections, peer, "consensus config checksum")
701                    .await?;
702
703            if peer_message != P2PMessage::Checksum(checksum) {
704                warn!(
705                    target: LOG_NET_PEER_DKG,
706                    expected = ?P2PMessage::Checksum(checksum),
707                    received = ?peer_message,
708                    config = ?cfg.consensus,
709                    "Peer {peer} has sent invalid consensus config checksum message"
710                );
711
712                bail!("Peer {peer} has sent invalid consensus config checksum message");
713            }
714
715            info!(
716                target: LOG_NET_PEER_DKG,
717                "Peer {peer} has sent valid consensus config checksum message"
718            );
719        }
720
721        info!(
722            target: LOG_NET_PEER_DKG,
723            "Config generation has completed successfully!"
724        );
725
726        Ok(cfg)
727    }
728}
729
730async fn receive_from_peer_with_progress(
731    connections: &DynP2PConnections<P2PMessage>,
732    peer: PeerId,
733    message_description: &'static str,
734) -> anyhow::Result<P2PMessage> {
735    let start = Instant::now();
736
737    loop {
738        select! {
739            // Cancel-safe: `receive_from_peer` is backed by
740            // `async_channel::Receiver::recv`, so dropping this future on the
741            // periodic progress-log tick does not lose messages.
742            peer_message = connections.receive_from_peer(peer) => {
743                return peer_message.context("Unexpected shutdown of p2p connections");
744            }
745            () = sleep(Duration::from_secs(10)) => {
746                info!(
747                    target: LOG_NET_PEER_DKG,
748                    %peer,
749                    message = message_description,
750                    elapsed_secs = start.elapsed().as_secs(),
751                    "Still waiting for peer message"
752                );
753            }
754        }
755    }
756}
757
758impl ServerConfig {
759    pub fn tls_config(&self) -> TlsConfig {
760        TlsConfig {
761            private_key: Arc::new(
762                rustls::pki_types::PrivateKeyDer::try_from(
763                    Vec::from_hex(self.private.tls_key.clone().unwrap()).unwrap(),
764                )
765                .expect("Failed to parse private key"),
766            ),
767            certificates: self
768                .consensus
769                .tls_certs
770                .iter()
771                .map(|(peer, cert)| {
772                    (
773                        *peer,
774                        rustls::pki_types::CertificateDer::from(Vec::from_hex(cert).unwrap()),
775                    )
776                })
777                .collect(),
778            peer_names: self
779                .local
780                .p2p_endpoints
781                .iter()
782                .map(|(id, endpoint)| (*id, endpoint.name.clone()))
783                .collect(),
784        }
785    }
786}
787
788impl ConfigGenParams {
789    pub fn peer_ids(&self) -> Vec<PeerId> {
790        self.peers.keys().copied().collect()
791    }
792
793    pub fn tls_config(&self) -> TlsConfig {
794        TlsConfig {
795            private_key: self.tls_key.clone().unwrap(),
796            certificates: self
797                .tls_certs()
798                .iter()
799                .map(|(peer, cert)| {
800                    (
801                        *peer,
802                        rustls::pki_types::CertificateDer::from(Vec::from_hex(cert).unwrap()),
803                    )
804                })
805                .collect(),
806            peer_names: self
807                .p2p_urls()
808                .into_iter()
809                .map(|(id, peer)| (id, peer.name))
810                .collect(),
811        }
812    }
813
814    pub fn tls_certs(&self) -> BTreeMap<PeerId, String> {
815        self.peers
816            .iter()
817            .filter_map(|(id, peer)| {
818                match peer.endpoints.clone() {
819                    PeerEndpoints::Tcp { cert, .. } => Some(cert.encode_hex()),
820                    PeerEndpoints::Iroh { .. } => None,
821                }
822                .map(|peer| (*id, peer))
823            })
824            .collect()
825    }
826
827    pub fn p2p_urls(&self) -> BTreeMap<PeerId, PeerUrl> {
828        self.peers
829            .iter()
830            .filter_map(|(id, peer)| {
831                match peer.endpoints.clone() {
832                    PeerEndpoints::Tcp { p2p_url, .. } => Some(PeerUrl {
833                        name: peer.name.clone(),
834                        url: p2p_url.clone(),
835                    }),
836                    PeerEndpoints::Iroh { .. } => None,
837                }
838                .map(|peer| (*id, peer))
839            })
840            .collect()
841    }
842
843    pub fn api_urls(&self) -> BTreeMap<PeerId, PeerUrl> {
844        self.peers
845            .iter()
846            .filter_map(|(id, peer)| {
847                match peer.endpoints.clone() {
848                    PeerEndpoints::Tcp { api_url, .. } => Some(PeerUrl {
849                        name: peer.name.clone(),
850                        url: api_url.clone(),
851                    }),
852                    PeerEndpoints::Iroh { .. } => None,
853                }
854                .map(|peer| (*id, peer))
855            })
856            .collect()
857    }
858
859    pub fn iroh_endpoints(&self) -> BTreeMap<PeerId, PeerIrohEndpoints> {
860        self.peers
861            .iter()
862            .filter_map(|(id, peer)| {
863                match peer.endpoints.clone() {
864                    PeerEndpoints::Tcp { .. } => None,
865                    PeerEndpoints::Iroh { api_pk, p2p_pk } => Some(PeerIrohEndpoints {
866                        name: peer.name.clone(),
867                        api_pk,
868                        p2p_pk,
869                    }),
870                }
871                .map(|peer| (*id, peer))
872            })
873            .collect()
874    }
875}