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