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 { major: 0, minor: 9 }])
320                .expect("not version conflicts"),
321        }
322    }
323    /// Creates a new config from the results of a trusted or distributed key
324    /// setup
325    pub fn from(
326        params: ConfigGenParams,
327        identity: PeerId,
328        broadcast_public_keys: BTreeMap<PeerId, PublicKey>,
329        broadcast_secret_key: SecretKey,
330        modules: BTreeMap<ModuleInstanceId, ServerModuleConfig>,
331        code_version: String,
332    ) -> Self {
333        let consensus = ServerConfigConsensus {
334            // Setup validates untrusted code versions before config generation.
335            // Keep this lower-level API usable by test helpers that use opaque
336            // version strings, as it was before DKG compatibility was added.
337            code_version: fedimint_core::version::DkgVersion::parse(&code_version).map_or_else(
338                |_| fedimint_core::version::release_version(&code_version).to_owned(),
339                |version| version.compatibility_version().to_string(),
340            ),
341            version: CORE_CONSENSUS_VERSION,
342            broadcast_public_keys,
343            broadcast_rounds_per_session: if is_running_in_test_env() {
344                DEFAULT_TEST_BROADCAST_ROUNDS_PER_SESSION
345            } else {
346                DEFAULT_BROADCAST_ROUNDS_PER_SESSION
347            },
348            api_endpoints: params.api_urls(),
349            iroh_endpoints: params.iroh_endpoints(),
350            tls_certs: params.tls_certs(),
351            modules: modules
352                .iter()
353                .map(|(peer, cfg)| (*peer, cfg.consensus.clone()))
354                .collect(),
355            meta: params.meta.clone(),
356        };
357
358        let local = ServerConfigLocal {
359            p2p_endpoints: params.p2p_urls(),
360            identity,
361            max_connections: DEFAULT_MAX_CLIENT_CONNECTIONS,
362            broadcast_round_delay_ms: if is_running_in_test_env() {
363                DEFAULT_TEST_BROADCAST_ROUND_DELAY_MS
364            } else {
365                DEFAULT_BROADCAST_ROUND_DELAY_MS
366            },
367        };
368
369        let private = ServerConfigPrivate {
370            tls_key: params
371                .tls_key
372                .map(|key| key.secret_der().to_vec().encode_hex()),
373            iroh_api_sk: params.iroh_api_sk,
374            iroh_p2p_sk: params.iroh_p2p_sk,
375            broadcast_secret_key,
376            modules: modules
377                .iter()
378                .map(|(peer, cfg)| (*peer, cfg.private.clone()))
379                .collect(),
380        };
381
382        Self {
383            consensus,
384            local,
385            private,
386        }
387    }
388
389    pub fn calculate_federation_id(&self) -> FederationId {
390        FederationId(self.consensus.api_endpoints().consensus_hash())
391    }
392
393    /// Constructs a module config by name
394    pub fn get_module_config_typed<T: TypedServerModuleConfig>(
395        &self,
396        id: ModuleInstanceId,
397    ) -> anyhow::Result<T> {
398        let private = Self::get_module_cfg_by_instance_id(&self.private.modules, id)?;
399        let consensus = self
400            .consensus
401            .modules
402            .get(&id)
403            .ok_or_else(|| format_err!("Typed module {id} not found"))?
404            .clone();
405        let module = ServerModuleConfig::from(private, consensus);
406
407        Ok(module.to_typed()?)
408    }
409    pub fn get_module_id_by_kind(
410        &self,
411        kind: impl Into<ModuleKind>,
412    ) -> anyhow::Result<ModuleInstanceId> {
413        let kind = kind.into();
414        Ok(*self
415            .consensus
416            .modules
417            .iter()
418            .find(|(_, v)| v.kind == kind)
419            .ok_or_else(|| format_err!("Module {kind} not found"))?
420            .0)
421    }
422
423    /// Constructs a module config by id
424    pub fn get_module_config(&self, id: ModuleInstanceId) -> anyhow::Result<ServerModuleConfig> {
425        let private = Self::get_module_cfg_by_instance_id(&self.private.modules, id)?;
426        let consensus = self
427            .consensus
428            .modules
429            .get(&id)
430            .ok_or_else(|| format_err!("Module config {id} not found"))?
431            .clone();
432        Ok(ServerModuleConfig::from(private, consensus))
433    }
434
435    fn get_module_cfg_by_instance_id(
436        json: &BTreeMap<ModuleInstanceId, JsonWithKind>,
437        id: ModuleInstanceId,
438    ) -> anyhow::Result<JsonWithKind> {
439        Ok(json
440            .get(&id)
441            .ok_or_else(|| format_err!("Module cfg {id} not found"))
442            .cloned()?
443            .with_fixed_empty_value())
444    }
445
446    pub fn validate_config(
447        &self,
448        identity: &PeerId,
449        module_config_gens: &ServerModuleInitRegistry,
450    ) -> anyhow::Result<()> {
451        let endpoints = self.consensus.api_endpoints().clone();
452        let consensus = self.consensus.clone();
453        let private = self.private.clone();
454
455        let my_public_key = private.broadcast_secret_key.public_key(&Secp256k1::new());
456
457        if Some(&my_public_key) != consensus.broadcast_public_keys.get(identity) {
458            bail!("Broadcast secret key doesn't match corresponding public key");
459        }
460        if endpoints.keys().max().copied().map(PeerId::to_usize) != Some(endpoints.len() - 1) {
461            bail!("Peer ids are not indexed from 0");
462        }
463        if endpoints.keys().min().copied() != Some(PeerId::from(0)) {
464            bail!("Peer ids are not indexed from 0");
465        }
466
467        for (module_id, module_kind) in &self
468            .consensus
469            .modules
470            .iter()
471            .map(|(id, config)| Ok((*id, config.kind.clone())))
472            .collect::<anyhow::Result<BTreeSet<_>>>()?
473        {
474            module_config_gens
475                .get(module_kind)
476                .ok_or_else(|| format_err!("module config gen not found {module_kind}"))?
477                .validate_config(identity, self.get_module_config(*module_id)?)?;
478        }
479
480        Ok(())
481    }
482
483    pub fn trusted_dealer_gen(
484        params: &BTreeMap<PeerId, ConfigGenParams>,
485        registry: &ServerModuleInitRegistry,
486        code_version_str: &str,
487    ) -> BTreeMap<PeerId, Self> {
488        let peer0 = &params[&PeerId::from(0)];
489
490        let mut broadcast_pks = BTreeMap::new();
491        let mut broadcast_sks = BTreeMap::new();
492        for peer_id in peer0.peer_ids() {
493            let (broadcast_sk, broadcast_pk) = secp256k1::generate_keypair(&mut OsRng);
494            broadcast_pks.insert(peer_id, broadcast_pk);
495            broadcast_sks.insert(peer_id, broadcast_sk);
496        }
497
498        let args = ConfigGenModuleArgs {
499            network: peer0.network,
500            disable_base_fees: peer0.disable_base_fees,
501        };
502
503        // Use legacy module ordering for backwards compatibility tests
504        let use_legacy_order = is_env_var_set("FM_BACKWARDS_COMPATIBILITY_TEST");
505        let module_iter: Vec<_> = if use_legacy_order {
506            registry.iter_legacy_order()
507        } else {
508            registry.iter().collect()
509        };
510
511        let module_configs: BTreeMap<_, _> = module_iter
512            .into_iter()
513            .filter(|(kind, _)| peer0.enabled_modules.contains(kind))
514            .enumerate()
515            .map(|(module_id, (_kind, module_init))| {
516                (
517                    module_id as ModuleInstanceId,
518                    module_init.trusted_dealer_gen(&peer0.peer_ids(), &args),
519                )
520            })
521            .collect();
522
523        let server_config: BTreeMap<_, _> = peer0
524            .peer_ids()
525            .iter()
526            .map(|&id| {
527                let config = ServerConfig::from(
528                    params[&id].clone(),
529                    id,
530                    broadcast_pks.clone(),
531                    *broadcast_sks.get(&id).expect("We created this entry"),
532                    module_configs
533                        .iter()
534                        .map(|(module_id, cfgs)| (*module_id, cfgs[&id].clone()))
535                        .collect(),
536                    code_version_str.to_string(),
537                );
538                (id, config)
539            })
540            .collect();
541
542        server_config
543    }
544
545    /// Runs the distributed key gen algorithm
546    pub async fn distributed_gen(
547        params: &ConfigGenParams,
548        registry: ServerModuleInitRegistry,
549        code_version_str: String,
550        connections: DynP2PConnections<P2PMessage>,
551        mut p2p_status_receivers: P2PStatusReceivers,
552    ) -> anyhow::Result<Self> {
553        let _timing /* logs on drop */ = timing::TimeReporter::new("distributed-gen").info();
554
555        // in case we are running by ourselves, avoid DKG
556        if params.peer_ids().len() == 1 {
557            let server = Self::trusted_dealer_gen(
558                &BTreeMap::from([(params.identity, params.clone())]),
559                &registry,
560                &code_version_str,
561            );
562
563            return Ok(server[&params.identity].clone());
564        }
565
566        info!(
567            target: LOG_NET_PEER_DKG,
568            "Waiting for all p2p connections to open..."
569        );
570
571        loop {
572            let mut pending_connection_receivers: Vec<_> = p2p_status_receivers
573                .iter_mut()
574                .filter_map(|(p, r)| {
575                    r.mark_unchanged();
576                    r.borrow().connected.is_none().then_some((*p, r.clone()))
577                })
578                .collect();
579
580            if pending_connection_receivers.is_empty() {
581                break;
582            }
583
584            let disconnected_peers = pending_connection_receivers
585                .iter()
586                .map(|(peer, receiver)| {
587                    let last_error = receiver.borrow().last_error.clone();
588
589                    (*peer, last_error)
590                })
591                .collect::<Vec<_>>();
592
593            info!(
594                target: LOG_NET_PEER_DKG,
595                pending = ?disconnected_peers,
596                "Waiting for all p2p connections to open..."
597            );
598
599            select! {
600                _ = select_all(pending_connection_receivers.iter_mut().map(|r| Box::pin(r.1.changed()))) => {}
601                () = sleep(Duration::from_secs(10)) => {}
602            }
603        }
604
605        let checksum = params.peers.consensus_hash_sha256();
606
607        info!(
608            target: LOG_NET_PEER_DKG,
609            "Comparing connection codes checksum {checksum}..."
610        );
611
612        connections.send(Recipient::Everyone, P2PMessage::Checksum(checksum));
613
614        for peer in params
615            .peer_ids()
616            .into_iter()
617            .filter(|p| *p != params.identity)
618        {
619            let peer_message = receive_from_peer_with_progress(
620                &connections,
621                peer,
622                "connection code checksum message",
623            )
624            .await?;
625
626            if peer_message != P2PMessage::Checksum(checksum) {
627                error!(
628                    target: LOG_NET_PEER_DKG,
629                    expected = ?P2PMessage::Checksum(checksum),
630                    received = ?peer_message,
631                    "Peer {peer} has sent invalid connection code checksum message"
632                );
633
634                bail!("Peer {peer} has sent invalid connection code checksum message");
635            }
636
637            info!(
638                target: LOG_NET_PEER_DKG,
639                "Peer {peer} has sent valid connection code checksum message"
640            );
641        }
642
643        info!(
644            target: LOG_NET_PEER_DKG,
645            "Running config generation..."
646        );
647
648        let handle = PeerHandle::new(
649            params.peer_ids().to_num_peers(),
650            params.identity,
651            &connections,
652        );
653
654        let (broadcast_sk, broadcast_pk) = secp256k1::generate_keypair(&mut OsRng);
655
656        let broadcast_public_keys = handle.exchange_encodable(broadcast_pk).await?;
657
658        let args = ConfigGenModuleArgs {
659            network: params.network,
660            disable_base_fees: params.disable_base_fees,
661        };
662
663        // Use legacy module ordering for backwards compatibility tests
664        let use_legacy_order = is_env_var_set("FM_BACKWARDS_COMPATIBILITY_TEST");
665        let module_iter: Vec<_> = if use_legacy_order {
666            registry.iter_legacy_order()
667        } else {
668            registry.iter().collect()
669        };
670
671        let mut module_cfgs = BTreeMap::new();
672
673        for (module_id, (kind, module_init)) in module_iter
674            .into_iter()
675            .filter(|(kind, _)| params.enabled_modules.contains(kind))
676            .enumerate()
677        {
678            info!(
679                target: LOG_NET_PEER_DKG,
680                "Running config generation for module of kind {kind}..."
681            );
682
683            let cfg = module_init.distributed_gen(&handle, &args).await?;
684
685            module_cfgs.insert(module_id as ModuleInstanceId, cfg);
686        }
687
688        let cfg = ServerConfig::from(
689            params.clone(),
690            params.identity,
691            broadcast_public_keys,
692            broadcast_sk,
693            module_cfgs,
694            code_version_str,
695        );
696
697        let checksum = cfg.consensus.consensus_hash_sha256();
698
699        info!(
700            target: LOG_NET_PEER_DKG,
701            "Comparing consensus config checksum {checksum}..."
702        );
703
704        connections.send(Recipient::Everyone, P2PMessage::Checksum(checksum));
705
706        for peer in params
707            .peer_ids()
708            .into_iter()
709            .filter(|p| *p != params.identity)
710        {
711            let peer_message =
712                receive_from_peer_with_progress(&connections, peer, "consensus config checksum")
713                    .await?;
714
715            if peer_message != P2PMessage::Checksum(checksum) {
716                warn!(
717                    target: LOG_NET_PEER_DKG,
718                    expected = ?P2PMessage::Checksum(checksum),
719                    received = ?peer_message,
720                    config = ?cfg.consensus,
721                    "Peer {peer} has sent invalid consensus config checksum message"
722                );
723
724                bail!("Peer {peer} has sent invalid consensus config checksum message");
725            }
726
727            info!(
728                target: LOG_NET_PEER_DKG,
729                "Peer {peer} has sent valid consensus config checksum message"
730            );
731        }
732
733        info!(
734            target: LOG_NET_PEER_DKG,
735            "Config generation has completed successfully!"
736        );
737
738        Ok(cfg)
739    }
740}
741
742async fn receive_from_peer_with_progress(
743    connections: &DynP2PConnections<P2PMessage>,
744    peer: PeerId,
745    message_description: &'static str,
746) -> anyhow::Result<P2PMessage> {
747    let start = Instant::now();
748
749    loop {
750        select! {
751            // Cancel-safe: `receive_from_peer` is backed by
752            // `async_channel::Receiver::recv`, so dropping this future on the
753            // periodic progress-log tick does not lose messages.
754            peer_message = connections.receive_from_peer(peer) => {
755                return peer_message.context("Unexpected shutdown of p2p connections");
756            }
757            () = sleep(Duration::from_secs(10)) => {
758                info!(
759                    target: LOG_NET_PEER_DKG,
760                    %peer,
761                    message = message_description,
762                    elapsed_secs = start.elapsed().as_secs(),
763                    "Still waiting for peer message"
764                );
765            }
766        }
767    }
768}
769
770impl ServerConfig {
771    pub fn tls_config(&self) -> TlsConfig {
772        TlsConfig {
773            private_key: Arc::new(
774                rustls::pki_types::PrivateKeyDer::try_from(
775                    Vec::from_hex(self.private.tls_key.clone().unwrap()).unwrap(),
776                )
777                .expect("Failed to parse private key"),
778            ),
779            certificates: self
780                .consensus
781                .tls_certs
782                .iter()
783                .map(|(peer, cert)| {
784                    (
785                        *peer,
786                        rustls::pki_types::CertificateDer::from(Vec::from_hex(cert).unwrap()),
787                    )
788                })
789                .collect(),
790            peer_names: self
791                .local
792                .p2p_endpoints
793                .iter()
794                .map(|(id, endpoint)| (*id, endpoint.name.clone()))
795                .collect(),
796        }
797    }
798}
799
800impl ConfigGenParams {
801    pub fn peer_ids(&self) -> Vec<PeerId> {
802        self.peers.keys().copied().collect()
803    }
804
805    pub fn tls_config(&self) -> TlsConfig {
806        TlsConfig {
807            private_key: self.tls_key.clone().unwrap(),
808            certificates: self
809                .tls_certs()
810                .iter()
811                .map(|(peer, cert)| {
812                    (
813                        *peer,
814                        rustls::pki_types::CertificateDer::from(Vec::from_hex(cert).unwrap()),
815                    )
816                })
817                .collect(),
818            peer_names: self
819                .p2p_urls()
820                .into_iter()
821                .map(|(id, peer)| (id, peer.name))
822                .collect(),
823        }
824    }
825
826    pub fn tls_certs(&self) -> BTreeMap<PeerId, String> {
827        self.peers
828            .iter()
829            .filter_map(|(id, peer)| {
830                match peer.endpoints.clone() {
831                    PeerEndpoints::Tcp { cert, .. } => Some(cert.encode_hex()),
832                    PeerEndpoints::Iroh { .. } => None,
833                }
834                .map(|peer| (*id, peer))
835            })
836            .collect()
837    }
838
839    pub fn p2p_urls(&self) -> BTreeMap<PeerId, PeerUrl> {
840        self.peers
841            .iter()
842            .filter_map(|(id, peer)| {
843                match peer.endpoints.clone() {
844                    PeerEndpoints::Tcp { p2p_url, .. } => Some(PeerUrl {
845                        name: peer.name.clone(),
846                        url: p2p_url.clone(),
847                    }),
848                    PeerEndpoints::Iroh { .. } => None,
849                }
850                .map(|peer| (*id, peer))
851            })
852            .collect()
853    }
854
855    pub fn api_urls(&self) -> BTreeMap<PeerId, PeerUrl> {
856        self.peers
857            .iter()
858            .filter_map(|(id, peer)| {
859                match peer.endpoints.clone() {
860                    PeerEndpoints::Tcp { api_url, .. } => Some(PeerUrl {
861                        name: peer.name.clone(),
862                        url: api_url.clone(),
863                    }),
864                    PeerEndpoints::Iroh { .. } => None,
865                }
866                .map(|peer| (*id, peer))
867            })
868            .collect()
869    }
870
871    pub fn iroh_endpoints(&self) -> BTreeMap<PeerId, PeerIrohEndpoints> {
872        self.peers
873            .iter()
874            .filter_map(|(id, peer)| {
875                match peer.endpoints.clone() {
876                    PeerEndpoints::Tcp { .. } => None,
877                    PeerEndpoints::Iroh { api_pk, p2p_pk } => Some(PeerIrohEndpoints {
878                        name: peer.name.clone(),
879                        api_pk,
880                        p2p_pk,
881                    }),
882                }
883                .map(|peer| (*id, peer))
884            })
885            .collect()
886    }
887}
888
889#[cfg(test)]
890mod tests;