fedimint_server/config/
api.rs

1use std::collections::BTreeMap;
2use std::net::SocketAddr;
3use std::sync::Arc;
4use std::time::Duration;
5
6use async_trait::async_trait;
7use bitcoin::hashes::sha256;
8use bitcoincore_rpc::RpcApi;
9use fedimint_api_client::api::{DynGlobalApi, StatusResponse};
10use fedimint_bitcoind::create_bitcoind;
11use fedimint_core::admin_client::{
12    ConfigGenConnectionsRequest, ConfigGenParamsConsensus, ConfigGenParamsRequest,
13    ConfigGenParamsResponse, PeerServerParams, ServerStatus,
14};
15use fedimint_core::config::{
16    ConfigGenModuleParams, ServerModuleConfigGenParamsRegistry, ServerModuleInitRegistry,
17};
18use fedimint_core::core::ModuleInstanceId;
19use fedimint_core::db::Database;
20use fedimint_core::encoding::Encodable;
21use fedimint_core::endpoint_constants::{
22    ADD_CONFIG_GEN_PEER_ENDPOINT, AUTH_ENDPOINT, CHECK_BITCOIN_STATUS_ENDPOINT,
23    CONFIG_GEN_PEERS_ENDPOINT, CONSENSUS_CONFIG_GEN_PARAMS_ENDPOINT,
24    DEFAULT_CONFIG_GEN_PARAMS_ENDPOINT, RESTART_FEDERATION_SETUP_ENDPOINT, RUN_DKG_ENDPOINT,
25    SET_CONFIG_GEN_CONNECTIONS_ENDPOINT, SET_CONFIG_GEN_PARAMS_ENDPOINT, SET_PASSWORD_ENDPOINT,
26    START_CONSENSUS_ENDPOINT, STATUS_ENDPOINT, VERIFIED_CONFIGS_ENDPOINT,
27    VERIFY_CONFIG_HASH_ENDPOINT,
28};
29use fedimint_core::envs::BitcoinRpcConfig;
30use fedimint_core::module::{
31    api_endpoint, ApiAuth, ApiEndpoint, ApiEndpointContext, ApiError, ApiRequestErased, ApiVersion,
32};
33use fedimint_core::task::{block_in_place, sleep, TaskGroup};
34use fedimint_core::util::SafeUrl;
35use fedimint_core::PeerId;
36use itertools::Itertools;
37use serde::{Deserialize, Serialize};
38use tokio::sync::mpsc::Sender;
39use tokio::sync::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
40use tokio::time::Instant;
41use tokio_rustls::rustls;
42use tracing::{error, info};
43
44use crate::config::{gen_cert_and_key, ConfigGenParams, ServerConfig};
45use crate::envs::FM_PEER_ID_SORT_BY_URL_ENV;
46use crate::net::api::{check_auth, ApiResult, HasApiContext};
47use crate::net::peers::DelayCalculator;
48
49/// Serves the config gen API endpoints
50#[derive(Clone)]
51pub struct ConfigGenApi {
52    /// In-memory state machine
53    state: Arc<Mutex<ConfigGenState>>,
54    /// DB not really used
55    db: Database,
56    /// Tracks when the config is generated
57    config_generated_tx: Sender<ServerConfig>,
58    /// Task group for running DKG
59    task_group: TaskGroup,
60    /// Code version str that will get encoded in consensus hash
61    code_version_str: String,
62    /// Api secret to use
63    api_secret: Option<String>,
64    p2p_bind_addr: SocketAddr,
65    bitcoin_status_cache: Arc<RwLock<Option<(Instant, BitcoinRpcConnectionStatus)>>>,
66    bitcoin_status_cache_duration: Duration,
67}
68
69impl ConfigGenApi {
70    pub fn new(
71        p2p_bind_addr: SocketAddr,
72        settings: ConfigGenSettings,
73        db: Database,
74        config_generated_tx: Sender<ServerConfig>,
75        task_group: &TaskGroup,
76        code_version_str: String,
77        api_secret: Option<String>,
78    ) -> Self {
79        let config_gen_api = Self {
80            state: Arc::new(Mutex::new(ConfigGenState::new(settings))),
81            db,
82            config_generated_tx,
83            task_group: task_group.clone(),
84            code_version_str,
85            api_secret,
86            p2p_bind_addr,
87            bitcoin_status_cache: Arc::new(RwLock::new(None)),
88            bitcoin_status_cache_duration: Duration::from_secs(60),
89        };
90        info!(target: fedimint_logging::LOG_NET_PEER_DKG, "Created new config gen Api");
91        config_gen_api
92    }
93
94    // Sets the auth and decryption key derived from the password
95    pub async fn set_password(&self, auth: ApiAuth) -> ApiResult<()> {
96        let mut state = self.require_status(ServerStatus::AwaitingPassword).await?;
97        let auth_trimmed = auth.0.trim();
98        if auth_trimmed != auth.0 {
99            return Err(ApiError::bad_request(
100                "Password contains leading/trailing whitespace".to_string(),
101            ));
102        }
103        state.auth = Some(auth);
104        state.status = ServerStatus::SharingConfigGenParams;
105        info!(
106            target: fedimint_logging::LOG_NET_PEER_DKG,
107            "Set password for config gen"
108        );
109        Ok(())
110    }
111
112    async fn require_status(&self, status: ServerStatus) -> ApiResult<MutexGuard<ConfigGenState>> {
113        let state = self.state.lock().await;
114        if state.status != status {
115            return Self::bad_request(&format!("Expected to be in {status:?} state"));
116        }
117        Ok(state)
118    }
119
120    async fn require_any_status(
121        &self,
122        statuses: &[ServerStatus],
123    ) -> ApiResult<MutexGuard<ConfigGenState>> {
124        let state = self.state.lock().await;
125        if !statuses.contains(&state.status) {
126            return Self::bad_request(&format!("Expected to be in one of {statuses:?} states"));
127        }
128        Ok(state)
129    }
130
131    /// Sets our connection info, possibly sending it to a leader
132    pub async fn set_config_gen_connections(
133        &self,
134        request: ConfigGenConnectionsRequest,
135    ) -> ApiResult<()> {
136        {
137            let mut state = self
138                .require_status(ServerStatus::SharingConfigGenParams)
139                .await?;
140            state.set_request(request)?;
141        }
142        self.update_leader().await?;
143        Ok(())
144    }
145
146    /// Sends our updated peer info to the leader (if we have one)
147    async fn update_leader(&self) -> ApiResult<()> {
148        let state = self.state.lock().await.clone();
149        let local = state.local.clone();
150
151        if let Some(url) = local.and_then(|local| local.leader_api_url) {
152            DynGlobalApi::from_pre_peer_id_admin_endpoint(url, &self.api_secret)
153                .add_config_gen_peer(state.our_peer_info()?)
154                .await
155                .map_err(|_| ApiError::not_found("Unable to connect to the leader".to_string()))?;
156        }
157        Ok(())
158    }
159
160    /// Called from `set_config_gen_connections` to add a peer's connection info
161    /// to the leader
162    pub async fn add_config_gen_peer(&self, peer: PeerServerParams) -> ApiResult<()> {
163        let mut state = self.state.lock().await;
164        state.peers.insert(peer.api_url.clone(), peer);
165        info!(target: fedimint_logging::LOG_NET_PEER_DKG, "New peer added to config gen");
166        Ok(())
167    }
168
169    /// Returns the peers that have called `add_config_gen_peer` on the leader
170    pub async fn config_gen_peers(&self) -> ApiResult<Vec<PeerServerParams>> {
171        let state = self.state.lock().await;
172        Ok(state.get_peer_info().into_values().collect())
173    }
174
175    /// Returns default config gen params that can be modified by the leader
176    pub async fn default_config_gen_params(&self) -> ApiResult<ConfigGenParamsRequest> {
177        let state = self.state.lock().await;
178        Ok(state.settings.default_params.clone())
179    }
180
181    /// Sets and validates the config gen params
182    ///
183    /// The leader passes consensus params, everyone passes local params
184    pub async fn set_config_gen_params(&self, request: ConfigGenParamsRequest) -> ApiResult<()> {
185        self.consensus_config_gen_params(&request).await?;
186        let mut state = self
187            .require_status(ServerStatus::SharingConfigGenParams)
188            .await?;
189        state.requested_params = Some(request);
190        info!(
191            target: fedimint_logging::LOG_NET_PEER_DKG,
192            "Set params for config gen"
193        );
194        Ok(())
195    }
196
197    async fn get_requested_params(&self) -> ApiResult<ConfigGenParamsRequest> {
198        let state = self.state.lock().await.clone();
199        state.requested_params.ok_or(ApiError::bad_request(
200            "Config params were not set on this guardian".to_string(),
201        ))
202    }
203
204    /// Gets the consensus config gen params
205    pub async fn consensus_config_gen_params(
206        &self,
207        request: &ConfigGenParamsRequest,
208    ) -> ApiResult<ConfigGenParamsResponse> {
209        let state = self.state.lock().await.clone();
210        let local = state.local.clone();
211
212        let consensus = match local.and_then(|local| local.leader_api_url) {
213            Some(leader_url) => {
214                let client = DynGlobalApi::from_pre_peer_id_admin_endpoint(
215                    leader_url.clone(),
216                    &self.api_secret,
217                );
218                let response = client.consensus_config_gen_params().await;
219                response
220                    .map_err(|_| ApiError::not_found("Cannot get leader params".to_string()))?
221                    .consensus
222            }
223            None => ConfigGenParamsConsensus {
224                peers: state.get_peer_info(),
225                meta: request.meta.clone(),
226                modules: request.modules.clone(),
227            },
228        };
229
230        let params = state.get_config_gen_params(request, consensus.clone())?;
231        Ok(ConfigGenParamsResponse {
232            consensus,
233            our_current_id: params.local.our_id,
234        })
235    }
236
237    /// Once configs are generated, updates status to ReadyForConfigGen and
238    /// spawns a task to coordinate DKG, then returns. Coordinating DKG in a
239    /// separate thread allows clients to poll the server status instead of
240    /// blocking until completion, which can be fragile due to timeouts, poor
241    /// network connections, etc.
242    ///
243    /// Calling a second time will return an error.
244    pub async fn run_dkg(&self) -> ApiResult<()> {
245        let leader = {
246            let mut state = self
247                .require_status(ServerStatus::SharingConfigGenParams)
248                .await?;
249            // Update our state
250            state.status = ServerStatus::ReadyForConfigGen;
251            info!(
252                target: fedimint_logging::LOG_NET_PEER_DKG,
253                "Update config gen status to 'Ready for config gen'"
254            );
255            // Create a WSClient for the leader
256            state.local.clone().and_then(|local| {
257                local.leader_api_url.map(|url| {
258                    DynGlobalApi::from_pre_peer_id_admin_endpoint(url, &self.api_secret.clone())
259                })
260            })
261        };
262
263        self.update_leader().await?;
264
265        let self_clone = self.clone();
266        let sub_group = self.task_group.make_subgroup();
267        let p2p_bind_addr = self.p2p_bind_addr;
268        sub_group.spawn("run dkg", move |_handle| async move {
269            // Followers wait for leader to signal readiness for DKG
270            if let Some(client) = leader {
271                loop {
272                    let status = client.status().await.map_err(|_| {
273                        ApiError::not_found("Unable to connect to the leader".to_string())
274                    })?;
275                    if status.server == ServerStatus::ReadyForConfigGen {
276                        break;
277                    }
278                    sleep(Duration::from_millis(100)).await;
279                }
280            };
281
282            // Get params and registry
283            let request = self_clone.get_requested_params().await?;
284            let response = self_clone.consensus_config_gen_params(&request).await?;
285            let (params, registry) = {
286                let state: MutexGuard<'_, ConfigGenState> = self_clone
287                    .require_status(ServerStatus::ReadyForConfigGen)
288                    .await?;
289                let params = state.get_config_gen_params(&request, response.consensus)?;
290                let registry = state.settings.registry.clone();
291                (params, registry)
292            };
293
294            // Run DKG
295            let task_group: TaskGroup = self_clone.task_group.make_subgroup();
296            let config = ServerConfig::distributed_gen(
297                p2p_bind_addr,
298                &params,
299                registry,
300                DelayCalculator::PROD_DEFAULT,
301                &task_group,
302                self_clone.code_version_str.clone(),
303            )
304            .await;
305            task_group
306                .shutdown_join_all(None)
307                .await
308                .expect("shuts down");
309
310            {
311                let mut state = self_clone.state.lock().await;
312                match config {
313                    Ok(config) => {
314                        state.status = ServerStatus::VerifyingConfigs;
315                        state.config = Some(config);
316                        info!(
317                            target: fedimint_logging::LOG_NET_PEER_DKG,
318                            "Set config for config gen"
319                        );
320                    }
321                    Err(e) => {
322                        error!(
323                            target: fedimint_logging::LOG_NET_PEER_DKG,
324                            "DKG failed with {:?}", e
325                        );
326                        state.status = ServerStatus::ConfigGenFailed;
327                        info!(
328                            target: fedimint_logging::LOG_NET_PEER_DKG,
329                            "Update config gen status to 'Config gen failed'"
330                        );
331                    }
332                }
333            }
334            self_clone.update_leader().await
335        });
336
337        Ok(())
338    }
339
340    /// Returns tagged hashes of consensus config to be shared with other peers.
341    /// The hashes are tagged with the peer id  such that they are unique to
342    /// each peer and their manual verification by the guardians via the UI is
343    /// more robust.
344    pub async fn verify_config_hash(&self) -> ApiResult<BTreeMap<PeerId, sha256::Hash>> {
345        let expected_status = [
346            ServerStatus::VerifyingConfigs,
347            ServerStatus::VerifiedConfigs,
348        ];
349
350        let state = self.require_any_status(&expected_status).await?;
351
352        let config = state
353            .config
354            .clone()
355            .ok_or(ApiError::bad_request("Missing config".to_string()))?;
356
357        let verification_hashes = config
358            .consensus
359            .api_endpoints
360            .keys()
361            .map(|peer| (*peer, (*peer, config.consensus.clone()).consensus_hash()))
362            .collect();
363
364        Ok(verification_hashes)
365    }
366
367    /// We have verified all our peer configs
368    pub async fn verified_configs(&self) -> ApiResult<()> {
369        {
370            let expected_status = [
371                ServerStatus::VerifyingConfigs,
372                ServerStatus::VerifiedConfigs,
373            ];
374            let mut state = self.require_any_status(&expected_status).await?;
375            if state.status == ServerStatus::VerifiedConfigs {
376                return Ok(());
377            }
378            state.status = ServerStatus::VerifiedConfigs;
379            info!(
380                target: fedimint_logging::LOG_NET_PEER_DKG,
381                "Update config gen status to 'Verified configs'"
382            );
383        }
384
385        self.update_leader().await?;
386        Ok(())
387    }
388
389    pub async fn start_consensus(&self) -> ApiResult<()> {
390        let state = self
391            .require_any_status(&[
392                ServerStatus::VerifyingConfigs,
393                ServerStatus::VerifiedConfigs,
394            ])
395            .await?;
396
397        self.config_generated_tx
398            .send(state.config.clone().expect("Config should exist"))
399            .await
400            .expect("Can send");
401
402        Ok(())
403    }
404
405    /// Returns the server status
406    pub async fn server_status(&self) -> ServerStatus {
407        self.state.lock().await.status.clone()
408    }
409
410    fn bad_request<T>(msg: &str) -> ApiResult<T> {
411        Err(ApiError::bad_request(msg.to_string()))
412    }
413
414    pub async fn restart_federation_setup(&self) -> ApiResult<()> {
415        let leader = {
416            let expected_status = [
417                ServerStatus::SharingConfigGenParams,
418                ServerStatus::ReadyForConfigGen,
419                ServerStatus::ConfigGenFailed,
420                ServerStatus::VerifyingConfigs,
421                ServerStatus::VerifiedConfigs,
422            ];
423            let mut state = self.require_any_status(&expected_status).await?;
424
425            state.status = ServerStatus::SetupRestarted;
426            info!(
427                target: fedimint_logging::LOG_NET_PEER_DKG,
428                "Update config gen status to 'Setup restarted'"
429            );
430            // Create a WSClient for the leader
431            state.local.clone().and_then(|local| {
432                local
433                    .leader_api_url
434                    .map(|url| DynGlobalApi::from_pre_peer_id_admin_endpoint(url, &self.api_secret))
435            })
436        };
437
438        self.update_leader().await?;
439
440        // Followers wait for leader to signal that all peers have restarted setup
441        // The leader will signal this by setting it's status to AwaitingPassword
442        let self_clone = self.clone();
443        let sub_group = self.task_group.make_subgroup();
444        sub_group.spawn("restart", |_handle| async move {
445            if let Some(client) = leader {
446                self_clone.await_leader_restart(&client).await?;
447            } else {
448                self_clone.await_peer_restart().await;
449            }
450            // Progress status to AwaitingPassword
451            {
452                let mut state = self_clone.state.lock().await;
453                state.reset();
454            }
455            self_clone.update_leader().await
456        });
457
458        Ok(())
459    }
460
461    // Followers wait for leader to signal that all peers have restarted setup
462    async fn await_leader_restart(&self, client: &DynGlobalApi) -> ApiResult<()> {
463        let mut retries = 0;
464        loop {
465            if let Ok(status) = client.status().await {
466                if status.server == ServerStatus::AwaitingPassword
467                    || status.server == ServerStatus::SharingConfigGenParams
468                {
469                    break Ok(());
470                }
471            } else {
472                if retries > 3 {
473                    return Err(ApiError::not_found(
474                        "Unable to connect to the leader".to_string(),
475                    ));
476                }
477                retries += 1;
478            }
479            sleep(Duration::from_millis(100)).await;
480        }
481    }
482
483    // Leader waits for all peers to restart setup,
484    async fn await_peer_restart(&self) {
485        loop {
486            {
487                let state = self.state.lock().await;
488                let peers = state.peers.clone();
489                if peers
490                    .values()
491                    .all(|peer| peer.status == Some(ServerStatus::SetupRestarted))
492                {
493                    break;
494                }
495            }
496            sleep(Duration::from_millis(100)).await;
497        }
498    }
499
500    // Check the status of the bitcoin rpc connection
501    pub async fn check_bitcoin_status(&self) -> ApiResult<BitcoinRpcConnectionStatus> {
502        // Check the cache first
503        {
504            let cached_status: RwLockReadGuard<'_, Option<(Instant, BitcoinRpcConnectionStatus)>> =
505                self.bitcoin_status_cache.read().await;
506            if let Some((timestamp, status)) = cached_status.as_ref() {
507                if timestamp.elapsed() < self.bitcoin_status_cache_duration {
508                    return Ok(*status);
509                }
510            }
511        }
512
513        // If cache is invalid or expired, fetch new status
514        let bitcoin_rpc_config = BitcoinRpcConfig::get_defaults_from_env_vars().map_err(|e| {
515            ApiError::server_error(format!("Failed to get bitcoin rpc env vars: {e}"))
516        })?;
517        let status = match bitcoin_rpc_config.kind.as_str() {
518            "bitcoind" => check_bitcoind_status(&bitcoin_rpc_config),
519            "esplora" => self.check_esplora_status(&bitcoin_rpc_config).await,
520            _ => Err(ApiError::bad_request(format!(
521                "Unsupported bitcoin rpc kind: {}",
522                bitcoin_rpc_config.kind
523            ))),
524        }?;
525
526        // Update the bitcoin status cache
527        let mut cached_status: RwLockWriteGuard<'_, Option<(Instant, BitcoinRpcConnectionStatus)>> =
528            self.bitcoin_status_cache.write().await;
529        *cached_status = Some((Instant::now(), status));
530
531        Ok(status)
532    }
533
534    async fn check_esplora_status(
535        &self,
536        bitcoin_rpc_config: &BitcoinRpcConfig,
537    ) -> ApiResult<BitcoinRpcConnectionStatus> {
538        let client =
539            create_bitcoind(bitcoin_rpc_config, self.task_group.make_handle()).map_err(|e| {
540                ApiError::server_error(format!("Failed to connect to bitcoin rpc: {e}"))
541            })?;
542        // TODO: Find a better way to check if esplora is synced, just returning Synced
543        // if it connects
544        let _network_info = client
545            .get_network()
546            .await
547            .map_err(|e| ApiError::server_error(format!("Failed to get esplora info: {e}")))?;
548        Ok(BitcoinRpcConnectionStatus::Synced)
549    }
550}
551
552fn check_bitcoind_status(
553    bitcoin_rpc_config: &BitcoinRpcConfig,
554) -> ApiResult<BitcoinRpcConnectionStatus> {
555    let (url, auth) = fedimint_bitcoind::bitcoincore::from_url_to_url_auth(&bitcoin_rpc_config.url)
556        .map_err(|e| ApiError::server_error(format!("Failed to parse bitcoin rpc url: {e}")))?;
557    let client = bitcoincore_rpc::Client::new(&url, auth)
558        .map_err(|e| ApiError::server_error(format!("Failed to connect to bitcoin rpc: {e}")))?;
559    let blockchain_info = block_in_place(|| client.get_blockchain_info())
560        .map_err(|e| ApiError::server_error(format!("Failed to get blockchain info: {e}")))?;
561
562    if blockchain_info.initial_block_download {
563        return Ok(BitcoinRpcConnectionStatus::Syncing(
564            blockchain_info.verification_progress,
565        ));
566    }
567
568    Ok(BitcoinRpcConnectionStatus::Synced)
569}
570
571#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
572pub enum BitcoinRpcConnectionStatus {
573    Synced,
574    Syncing(f64),
575}
576
577/// Config gen params that are only used locally, shouldn't be shared
578#[derive(Debug, Clone)]
579pub struct ConfigGenParamsLocal {
580    /// Our peer id
581    pub our_id: PeerId,
582    /// Our TLS private key
583    pub our_private_key: rustls::PrivateKey,
584    /// Secret API auth string
585    pub api_auth: ApiAuth,
586    /// Bind address for P2P communication
587    pub p2p_bind: SocketAddr,
588    /// Bind address for API communication
589    pub api_bind: SocketAddr,
590    /// How many API connections we will accept
591    pub max_connections: u32,
592}
593
594/// All the info we configure prior to config gen starting
595#[derive(Debug, Clone)]
596pub struct ConfigGenSettings {
597    /// Limit on the number of times a config download token can be used
598    pub download_token_limit: Option<u64>,
599    /// Bind address for our P2P connection
600    pub p2p_bind: SocketAddr,
601    /// Bind address for our API connection
602    pub api_bind: SocketAddr,
603    /// URL for our P2P connection
604    pub p2p_url: SafeUrl,
605    /// URL for our API connection
606    pub api_url: SafeUrl,
607    /// The default params for the modules
608    pub default_params: ConfigGenParamsRequest,
609    /// How many API connections we will accept
610    pub max_connections: u32,
611    /// Registry for config gen
612    pub registry: ServerModuleInitRegistry,
613}
614
615/// State held by the API after receiving a `ConfigGenConnectionsRequest`
616#[derive(Debug, Clone)]
617pub struct ConfigGenState {
618    /// Our config gen settings configured locally
619    settings: ConfigGenSettings,
620    /// Our auth string
621    auth: Option<ApiAuth>,
622    /// Our local connection
623    local: Option<ConfigGenLocalConnection>,
624    /// Connection info received from other guardians, unique by api_url
625    /// (because it's non-user configurable)
626    peers: BTreeMap<SafeUrl, PeerServerParams>,
627    /// The config gen params requested by the leader
628    requested_params: Option<ConfigGenParamsRequest>,
629    /// Our status
630    status: ServerStatus,
631    /// Configs that have been generated
632    config: Option<ServerConfig>,
633}
634
635/// Our local connection info
636#[derive(Debug, Clone)]
637struct ConfigGenLocalConnection {
638    /// Our TLS private key
639    tls_private: rustls::PrivateKey,
640    /// Our TLS public cert
641    tls_cert: rustls::Certificate,
642    /// Our guardian name
643    our_name: String,
644    /// URL of "leader" guardian to send our connection info to
645    /// Will be `None` if we are the leader
646    leader_api_url: Option<SafeUrl>,
647}
648
649impl ConfigGenState {
650    fn new(settings: ConfigGenSettings) -> Self {
651        Self {
652            settings,
653            auth: None,
654            local: None,
655            peers: BTreeMap::new(),
656            requested_params: None,
657            status: ServerStatus::AwaitingPassword,
658            config: None,
659        }
660    }
661
662    fn set_request(&mut self, request: ConfigGenConnectionsRequest) -> ApiResult<()> {
663        let (tls_cert, tls_private) = gen_cert_and_key(&request.our_name)
664            .map_err(|_| ApiError::server_error("Unable to generate TLS keys".to_string()))?;
665        self.local = Some(ConfigGenLocalConnection {
666            tls_private,
667            tls_cert,
668            our_name: request.our_name,
669            leader_api_url: request.leader_api_url,
670        });
671        info!(
672            target: fedimint_logging::LOG_NET_PEER_DKG,
673            "Set local connection for config gen"
674        );
675        Ok(())
676    }
677
678    fn local_connection(&self) -> ApiResult<ConfigGenLocalConnection> {
679        self.local.clone().ok_or(ApiError::bad_request(
680            "Our connection info not set yet".to_string(),
681        ))
682    }
683
684    fn auth(&self) -> ApiResult<ApiAuth> {
685        self.auth
686            .clone()
687            .ok_or(ApiError::bad_request("Missing auth".to_string()))
688    }
689
690    fn our_peer_info(&self) -> ApiResult<PeerServerParams> {
691        let local = self.local_connection()?;
692        Ok(PeerServerParams {
693            cert: local.tls_cert.clone(),
694            p2p_url: self.settings.p2p_url.clone(),
695            api_url: self.settings.api_url.clone(),
696            name: local.our_name,
697            status: Some(self.status.clone()),
698        })
699    }
700
701    fn get_peer_info(&self) -> BTreeMap<PeerId, PeerServerParams> {
702        self.peers
703            .values()
704            .cloned()
705            .chain(self.our_peer_info().ok())
706            // Since sort order here is arbitrary, try to sort by nick-names first for more natural
707            // 'name -> id' mapping, which is helpful when operating on 'peer-ids' (debugging etc.);
708            // Ties are OK (to_lowercase), not important in practice.
709            .sorted_by_cached_key(|peer| {
710                // in certain (very obscure) cases, it might be worthwhile to sort by urls, so
711                // just expose it as an env var; probably no need to document it too much
712                if std::env::var_os(FM_PEER_ID_SORT_BY_URL_ENV).is_some_and(|var| !var.is_empty()) {
713                    peer.api_url.to_string()
714                } else {
715                    peer.name.to_lowercase()
716                }
717            })
718            .enumerate()
719            .map(|(i, peer)| (PeerId::from(i as u16), peer))
720            .collect()
721    }
722
723    /// Validates and returns the params using our `request` and `consensus`
724    /// which comes from the leader
725    fn get_config_gen_params(
726        &self,
727        request: &ConfigGenParamsRequest,
728        mut consensus: ConfigGenParamsConsensus,
729    ) -> ApiResult<ConfigGenParams> {
730        let local_connection = self.local_connection()?;
731
732        let (our_id, _) = consensus
733            .peers
734            .iter()
735            .find(|(_, param)| local_connection.tls_cert == param.cert)
736            .ok_or(ApiError::bad_request(
737                "Our TLS cert not found among peers".to_string(),
738            ))?;
739
740        let mut combined_params = vec![];
741        let default_params = self.settings.default_params.modules.clone();
742        let local_params = request.modules.clone();
743        let consensus_params = consensus.modules.clone();
744        // Use defaults in case local or consensus params are missing
745        for (id, kind, default) in default_params.iter_modules() {
746            let consensus = &consensus_params.get(id).unwrap_or(default).consensus;
747            let local = &local_params.get(id).unwrap_or(default).local;
748            let combined = ConfigGenModuleParams::new(local.clone(), consensus.clone());
749            // Check that the params are parseable
750            let module = self.settings.registry.get(kind).expect("Module exists");
751            module.validate_params(&combined).map_err(|e| {
752                ApiError::bad_request(format!(
753                    "Module {} params invalid: {}",
754                    id,
755                    itertools::join(e.chain(), ": ")
756                ))
757            })?;
758            combined_params.push((id, kind.clone(), combined));
759        }
760        consensus.modules = ServerModuleConfigGenParamsRegistry::from_iter(combined_params);
761
762        let local = ConfigGenParamsLocal {
763            our_id: *our_id,
764            our_private_key: local_connection.tls_private,
765            api_auth: self.auth()?,
766            p2p_bind: self.settings.p2p_bind,
767            api_bind: self.settings.api_bind,
768            max_connections: self.settings.max_connections,
769        };
770
771        Ok(ConfigGenParams { local, consensus })
772    }
773
774    fn reset(&mut self) {
775        self.auth = None;
776        self.local = None;
777        self.peers = BTreeMap::new();
778        self.requested_params = None;
779        self.status = ServerStatus::AwaitingPassword;
780        self.config = None;
781
782        info!(
783            target: fedimint_logging::LOG_NET_PEER_DKG,
784            "Reset config gen state"
785        );
786    }
787}
788
789#[async_trait]
790impl HasApiContext<ConfigGenApi> for ConfigGenApi {
791    async fn context(
792        &self,
793        request: &ApiRequestErased,
794        id: Option<ModuleInstanceId>,
795    ) -> (&ConfigGenApi, ApiEndpointContext<'_>) {
796        let mut db = self.db.clone();
797        let mut dbtx = self.db.begin_transaction().await;
798        if let Some(id) = id {
799            db = self.db.with_prefix_module_id(id).0;
800            dbtx = dbtx.with_prefix_module_id(id).0;
801        }
802        let state = self.state.lock().await;
803        let auth = request.auth.as_ref();
804        let has_auth = match state.auth.clone() {
805            // The first client to connect gets the set the password
806            None => true,
807            Some(configured_auth) => Some(&configured_auth) == auth,
808        };
809
810        (
811            self,
812            ApiEndpointContext::new(db, dbtx, has_auth, request.auth.clone()),
813        )
814    }
815}
816
817pub fn server_endpoints() -> Vec<ApiEndpoint<ConfigGenApi>> {
818    vec![
819        api_endpoint! {
820            SET_PASSWORD_ENDPOINT,
821            ApiVersion::new(0, 0),
822            async |config: &ConfigGenApi, context, _v: ()| -> () {
823                match context.request_auth() {
824                    None => return Err(ApiError::bad_request("Missing password".to_string())),
825                    Some(auth) => config.set_password(auth).await
826                }
827            }
828        },
829        api_endpoint! {
830            SET_CONFIG_GEN_CONNECTIONS_ENDPOINT,
831            ApiVersion::new(0, 0),
832            async |config: &ConfigGenApi, context, server: ConfigGenConnectionsRequest| -> () {
833                check_auth(context)?;
834                config.set_config_gen_connections(server).await
835            }
836        },
837        api_endpoint! {
838            ADD_CONFIG_GEN_PEER_ENDPOINT,
839            ApiVersion::new(0, 0),
840            async |config: &ConfigGenApi, _context, peer: PeerServerParams| -> () {
841                // No auth required since this is an API-to-API call and the peer connections will be manually accepted or not in the UI
842                config.add_config_gen_peer(peer).await
843            }
844        },
845        api_endpoint! {
846            CONFIG_GEN_PEERS_ENDPOINT,
847            ApiVersion::new(0, 0),
848            async |config: &ConfigGenApi, _context, _v: ()| -> Vec<PeerServerParams> {
849                config.config_gen_peers().await
850            }
851        },
852        api_endpoint! {
853            DEFAULT_CONFIG_GEN_PARAMS_ENDPOINT,
854            ApiVersion::new(0, 0),
855            async |config: &ConfigGenApi, context,  _v: ()| -> ConfigGenParamsRequest {
856                check_auth(context)?;
857                config.default_config_gen_params().await
858            }
859        },
860        api_endpoint! {
861            SET_CONFIG_GEN_PARAMS_ENDPOINT,
862            ApiVersion::new(0, 0),
863            async |config: &ConfigGenApi, context, params: ConfigGenParamsRequest| -> () {
864                check_auth(context)?;
865                config.set_config_gen_params(params).await
866            }
867        },
868        api_endpoint! {
869            CONSENSUS_CONFIG_GEN_PARAMS_ENDPOINT,
870            ApiVersion::new(0, 0),
871            async |config: &ConfigGenApi, _context, _v: ()| -> ConfigGenParamsResponse {
872                let request = config.get_requested_params().await?;
873                config.consensus_config_gen_params(&request).await
874            }
875        },
876        api_endpoint! {
877            RUN_DKG_ENDPOINT,
878            ApiVersion::new(0, 0),
879            async |config: &ConfigGenApi, context, _v: ()| -> () {
880                check_auth(context)?;
881                config.run_dkg().await
882            }
883        },
884        api_endpoint! {
885            VERIFY_CONFIG_HASH_ENDPOINT,
886            ApiVersion::new(0, 0),
887            async |config: &ConfigGenApi, context, _v: ()| -> BTreeMap<PeerId, sha256::Hash> {
888                check_auth(context)?;
889                config.verify_config_hash().await
890            }
891        },
892        api_endpoint! {
893            VERIFIED_CONFIGS_ENDPOINT,
894            ApiVersion::new(0, 0),
895            async |config: &ConfigGenApi, context, _v: ()| -> () {
896                check_auth(context)?;
897                config.verified_configs().await
898            }
899        },
900        api_endpoint! {
901            START_CONSENSUS_ENDPOINT,
902            ApiVersion::new(0, 0),
903            async |config: &ConfigGenApi, context, _v: ()| -> () {
904                check_auth(context)?;
905                config.start_consensus().await
906            }
907        },
908        api_endpoint! {
909            STATUS_ENDPOINT,
910            ApiVersion::new(0, 0),
911            async |config: &ConfigGenApi, _context, _v: ()| -> StatusResponse {
912                let server = config.server_status().await;
913                Ok(StatusResponse {
914                    server,
915                    federation: None
916                })
917            }
918        },
919        api_endpoint! {
920            AUTH_ENDPOINT,
921            ApiVersion::new(0, 0),
922            async |_config: &ConfigGenApi, context, _v: ()| -> () {
923                check_auth(context)?;
924                Ok(())
925            }
926        },
927        api_endpoint! {
928            RESTART_FEDERATION_SETUP_ENDPOINT,
929            ApiVersion::new(0, 0),
930            async |config: &ConfigGenApi, context, _v: ()| -> () {
931                check_auth(context)?;
932                config.restart_federation_setup().await
933            }
934        },
935        api_endpoint! {
936            CHECK_BITCOIN_STATUS_ENDPOINT,
937            ApiVersion::new(0, 0),
938            async |config: &ConfigGenApi, context, _v: ()| -> BitcoinRpcConnectionStatus {
939                check_auth(context)?;
940                config.check_bitcoin_status().await
941            }
942        },
943    ]
944}
945
946#[cfg(test)]
947mod tests {
948
949    use std::collections::{BTreeMap, BTreeSet, HashSet};
950    use std::fs;
951    use std::path::{Path, PathBuf};
952    use std::sync::Arc;
953    use std::time::Duration;
954
955    use fedimint_api_client::api::{DynGlobalApi, FederationResult, StatusResponse};
956    use fedimint_core::admin_client::{ConfigGenParamsRequest, ServerStatus};
957    use fedimint_core::config::{ServerModuleConfigGenParamsRegistry, ServerModuleInitRegistry};
958    use fedimint_core::db::mem_impl::MemDatabase;
959    use fedimint_core::db::IRawDatabaseExt;
960    use fedimint_core::module::ApiAuth;
961    use fedimint_core::runtime::spawn;
962    use fedimint_core::task::{sleep, TaskGroup};
963    use fedimint_core::util::SafeUrl;
964    use fedimint_core::Amount;
965    use fedimint_dummy_common::config::{
966        DummyConfig, DummyGenParams, DummyGenParamsConsensus, DummyGenParamsLocal,
967    };
968    use fedimint_dummy_server::DummyInit;
969    use fedimint_logging::TracingSetup;
970    use fedimint_portalloc::port_alloc;
971    use fedimint_testing_core::test_dir;
972    use futures::future::join_all;
973    use itertools::Itertools;
974    use tracing::info;
975
976    use crate::config::api::{ConfigGenConnectionsRequest, ConfigGenSettings};
977    use crate::config::io::{read_server_config, PLAINTEXT_PASSWORD};
978    use crate::config::{DynServerModuleInit, ServerConfig, DEFAULT_MAX_CLIENT_CONNECTIONS};
979    use crate::fedimint_core::module::ServerModuleInit;
980    use crate::net::api::ApiSecrets;
981
982    /// Helper in config API tests for simulating a guardian's client and server
983    struct TestConfigApi {
984        client: DynGlobalApi,
985        auth: ApiAuth,
986        name: String,
987        settings: ConfigGenSettings,
988        amount: Amount,
989        dir: PathBuf,
990    }
991
992    impl TestConfigApi {
993        /// Creates a new test API taking up a port, with P2P endpoint on the
994        /// next port
995        fn new(port: u16, name_suffix: u16, data_dir: &Path) -> TestConfigApi {
996            let db = MemDatabase::new().into_database();
997
998            let name = format!("peer{name_suffix}");
999            let api_bind = format!("127.0.0.1:{port}").parse().expect("parses");
1000            let api_url: SafeUrl = format!("ws://127.0.0.1:{port}").parse().expect("parses");
1001            let p2p_bind = format!("127.0.0.1:{}", port + 1).parse().expect("parses");
1002            let p2p_url = format!("fedimint://127.0.0.1:{}", port + 1)
1003                .parse()
1004                .expect("parses");
1005            let module_inits = ServerModuleInitRegistry::from_iter([DummyInit.into()]);
1006            let mut modules = ServerModuleConfigGenParamsRegistry::default();
1007            modules.attach_config_gen_params_by_id(0, DummyInit::kind(), DummyGenParams::default());
1008
1009            let default_params = ConfigGenParamsRequest {
1010                meta: BTreeMap::new(),
1011                modules,
1012            };
1013            let settings = ConfigGenSettings {
1014                download_token_limit: None,
1015                p2p_bind,
1016                api_bind,
1017                p2p_url,
1018                api_url: api_url.clone(),
1019                default_params,
1020                max_connections: DEFAULT_MAX_CLIENT_CONNECTIONS,
1021                registry: ServerModuleInitRegistry::from(vec![DynServerModuleInit::from(
1022                    DummyInit,
1023                )]),
1024            };
1025
1026            let dir = data_dir.join(name_suffix.to_string());
1027            fs::create_dir_all(dir.clone()).expect("Unable to create test dir");
1028
1029            let dir_clone = dir.clone();
1030            let settings_clone = settings.clone();
1031
1032            spawn("fedimint server", async move {
1033                crate::run(
1034                    dir_clone,
1035                    ApiSecrets::none(),
1036                    settings_clone,
1037                    db,
1038                    "dummyversionhash".to_owned(),
1039                    &module_inits,
1040                    TaskGroup::new(),
1041                )
1042                .await
1043                .expect("Failed to run fedimint server");
1044            });
1045
1046            // our id doesn't really exist at this point
1047            let auth = ApiAuth(format!("password-{port}"));
1048            let client = DynGlobalApi::from_pre_peer_id_admin_endpoint(api_url, &None);
1049
1050            TestConfigApi {
1051                client,
1052                auth,
1053                name,
1054                settings,
1055                amount: Amount::from_sats(u64::from(port)),
1056                dir,
1057            }
1058        }
1059
1060        /// Helper function using generated urls
1061        async fn set_connections(&self, leader: &Option<SafeUrl>) -> FederationResult<()> {
1062            self.client
1063                .set_config_gen_connections(
1064                    ConfigGenConnectionsRequest {
1065                        our_name: self.name.clone(),
1066                        leader_api_url: leader.clone(),
1067                    },
1068                    self.auth.clone(),
1069                )
1070                .await
1071        }
1072
1073        /// Helper for getting server status
1074        async fn status(&self) -> StatusResponse {
1075            loop {
1076                match self.client.status().await {
1077                    Ok(status) => return status,
1078                    Err(_) => sleep(Duration::from_millis(1000)).await,
1079                }
1080                info!(
1081                    target: fedimint_logging::LOG_TEST,
1082                    "Test retrying server status"
1083                );
1084            }
1085        }
1086
1087        /// Helper for awaiting all servers have the status
1088        /// Use this BEFORE server config gen params have been set
1089        async fn wait_status_preconfig(&self, status: ServerStatus, peers: &Vec<TestConfigApi>) {
1090            loop {
1091                let server_status = self.status().await.server;
1092                if server_status == status {
1093                    for peer in peers {
1094                        let peer_status = peer.status().await.server;
1095                        if peer_status != server_status {
1096                            info!(
1097                                target: fedimint_logging::LOG_TEST,
1098                                "Test retrying peer server status preconfig"
1099                            );
1100                            sleep(Duration::from_millis(10)).await;
1101                            continue;
1102                        }
1103                    }
1104                    break;
1105                }
1106                info!(
1107                    target: fedimint_logging::LOG_TEST,
1108                    "Test retrying server status preconfig"
1109                );
1110            }
1111        }
1112
1113        /// Helper for awaiting all servers have the status
1114        /// Use this AFTER server config gen params have been set
1115        async fn wait_status(&self, status: ServerStatus) {
1116            loop {
1117                let response = self.client.consensus_config_gen_params().await.unwrap();
1118                let mismatched: Vec<_> = response
1119                    .consensus
1120                    .peers
1121                    .iter()
1122                    .filter(|(_, param)| param.status != Some(status.clone()))
1123                    .collect();
1124                if mismatched.is_empty() {
1125                    break;
1126                }
1127                info!(
1128                    target: fedimint_logging::LOG_TEST,
1129                    "Test retrying server status"
1130                );
1131                sleep(Duration::from_millis(10)).await;
1132            }
1133        }
1134
1135        /// Sets local param to name and unique consensus amount for testing
1136        async fn set_config_gen_params(&self) {
1137            let mut modules = ServerModuleConfigGenParamsRegistry::default();
1138            modules.attach_config_gen_params_by_id(
1139                0,
1140                DummyInit::kind(),
1141                DummyGenParams {
1142                    local: DummyGenParamsLocal,
1143                    consensus: DummyGenParamsConsensus {
1144                        tx_fee: self.amount,
1145                    },
1146                },
1147            );
1148            let request = ConfigGenParamsRequest {
1149                meta: BTreeMap::from([("\"test\"".to_string(), self.name.clone())]),
1150                modules,
1151            };
1152
1153            self.client
1154                .set_config_gen_params(request, self.auth.clone())
1155                .await
1156                .unwrap();
1157        }
1158
1159        /// reads the dummy module config from the filesystem
1160        fn read_config(&self) -> ServerConfig {
1161            let auth = fs::read_to_string(self.dir.join(PLAINTEXT_PASSWORD));
1162            read_server_config(&auth.unwrap(), &self.dir).unwrap()
1163        }
1164    }
1165
1166    #[tokio::test(flavor = "multi_thread")]
1167    async fn test_config_api() {
1168        const PEER_NUM: u16 = 4;
1169        const PORTS_PER_PEER: u16 = 2;
1170        let _ = TracingSetup::default().init();
1171        let (data_dir, _maybe_tmp_dir_guard) = test_dir("test-config-api");
1172        let base_port = port_alloc(PEER_NUM * PORTS_PER_PEER).unwrap();
1173
1174        let mut followers = vec![];
1175        let mut test_config = TestConfigApi::new(base_port, 0, &data_dir);
1176
1177        for i in 1..PEER_NUM {
1178            let port = base_port + (i * PORTS_PER_PEER);
1179            let follower = TestConfigApi::new(port, i, &data_dir);
1180            followers.push(follower);
1181        }
1182
1183        test_config = validate_leader_setup(test_config).await;
1184
1185        // Setup followers and send connection info
1186        for follower in &mut followers {
1187            assert_eq!(
1188                follower.status().await.server,
1189                ServerStatus::AwaitingPassword
1190            );
1191            follower
1192                .client
1193                .set_password(follower.auth.clone())
1194                .await
1195                .unwrap();
1196            let leader_url = Some(test_config.settings.api_url.clone());
1197            follower.set_connections(&leader_url).await.unwrap();
1198            follower.name = format!("{}_", follower.name);
1199            follower.set_connections(&leader_url).await.unwrap();
1200            follower.set_config_gen_params().await;
1201        }
1202
1203        // Validate we can do a full fedimint setup
1204        validate_full_setup(test_config, followers).await;
1205    }
1206
1207    #[tokio::test(flavor = "multi_thread")]
1208    #[ignore] // TODO: flaky https://github.com/fedimint/fedimint/issues/4308
1209    async fn test_restart_setup() {
1210        const PEER_NUM: u16 = 4;
1211        const PORTS_PER_PEER: u16 = 2;
1212        let _ = TracingSetup::default().init();
1213        let (data_dir, _maybe_tmp_dir_guard) = test_dir("test-restart-setup");
1214        let base_port = port_alloc(PEER_NUM * PORTS_PER_PEER).unwrap();
1215
1216        let mut followers = vec![];
1217        let mut test_config = TestConfigApi::new(base_port, 0, &data_dir);
1218
1219        for i in 1..PEER_NUM {
1220            let port = base_port + (i * PORTS_PER_PEER);
1221            let follower = TestConfigApi::new(port, i, &data_dir);
1222            followers.push(follower);
1223        }
1224
1225        test_config = validate_leader_setup(test_config).await;
1226
1227        // Setup followers and send connection info
1228        for follower in &mut followers {
1229            assert_eq!(
1230                follower.status().await.server,
1231                ServerStatus::AwaitingPassword
1232            );
1233            follower
1234                .client
1235                .set_password(follower.auth.clone())
1236                .await
1237                .unwrap();
1238            let leader_url = Some(test_config.settings.api_url.clone());
1239            follower.set_connections(&leader_url).await.unwrap();
1240            follower.name = format!("{}_", follower.name);
1241            follower.set_connections(&leader_url).await.unwrap();
1242            follower.set_config_gen_params().await;
1243        }
1244        test_config
1245            .wait_status(ServerStatus::SharingConfigGenParams)
1246            .await;
1247
1248        // Leader can trigger a setup restart
1249        test_config
1250            .client
1251            .restart_federation_setup(test_config.auth.clone())
1252            .await
1253            .unwrap();
1254
1255        // All peers can trigger a setup restart. This has to be done manually by each
1256        // peer, and any peer could trigger a restart before the leader does.
1257        for peer in &followers {
1258            peer.client
1259                .restart_federation_setup(peer.auth.clone())
1260                .await
1261                .ok();
1262        }
1263
1264        // Ensure all servers have restarted
1265        test_config
1266            .wait_status_preconfig(ServerStatus::SetupRestarted, &followers)
1267            .await;
1268        test_config
1269            .wait_status_preconfig(ServerStatus::AwaitingPassword, &followers)
1270            .await;
1271
1272        test_config = validate_leader_setup(test_config).await;
1273
1274        // Setup followers and send connection info
1275        for follower in &mut followers {
1276            assert_eq!(
1277                follower.status().await.server,
1278                ServerStatus::AwaitingPassword
1279            );
1280            follower
1281                .client
1282                .set_password(follower.auth.clone())
1283                .await
1284                .unwrap();
1285            let leader_url = Some(test_config.settings.api_url.clone());
1286            follower.set_connections(&leader_url).await.unwrap();
1287            follower.set_config_gen_params().await;
1288        }
1289
1290        // Validate we can do a full fedimint setup after a restart
1291        validate_full_setup(test_config, followers).await;
1292    }
1293
1294    // Validate steps when leader initiates fedimint setup
1295    async fn validate_leader_setup(mut leader: TestConfigApi) -> TestConfigApi {
1296        assert_eq!(leader.status().await.server, ServerStatus::AwaitingPassword);
1297
1298        // Cannot set the password twice
1299        leader
1300            .client
1301            .set_password(leader.auth.clone())
1302            .await
1303            .unwrap();
1304        assert!(leader
1305            .client
1306            .set_password(leader.auth.clone())
1307            .await
1308            .is_err());
1309
1310        // We can call this twice to change the leader name
1311        leader.set_connections(&None).await.unwrap();
1312        leader.name = "leader".to_string();
1313        leader.set_connections(&None).await.unwrap();
1314
1315        // Leader sets the config
1316        let _ = leader
1317            .client
1318            .get_default_config_gen_params(leader.auth.clone())
1319            .await
1320            .unwrap();
1321        leader.set_config_gen_params().await;
1322
1323        leader
1324    }
1325
1326    // Validate we can use the config api to do a full fedimint setup
1327    async fn validate_full_setup(leader: TestConfigApi, mut followers: Vec<TestConfigApi>) {
1328        // Confirm we can get peer servers if we are the leader
1329        let peers = leader.client.get_config_gen_peers().await.unwrap();
1330        let names: Vec<_> = peers.into_iter().map(|peer| peer.name).sorted().collect();
1331        assert_eq!(names, vec!["leader", "peer1_", "peer2_", "peer3_"]);
1332
1333        leader
1334            .wait_status(ServerStatus::SharingConfigGenParams)
1335            .await;
1336
1337        // Followers can fetch configs
1338        let mut configs = vec![];
1339        for peer in &followers {
1340            configs.push(peer.client.consensus_config_gen_params().await.unwrap());
1341        }
1342        // Confirm all consensus configs are the same
1343        let mut consensus: Vec<_> = configs.iter().map(|p| p.consensus.clone()).collect();
1344        consensus.dedup();
1345        assert_eq!(consensus.len(), 1);
1346        // Confirm all peer ids are unique
1347        let ids: BTreeSet<_> = configs.iter().map(|p| p.our_current_id).collect();
1348        assert_eq!(ids.len(), followers.len());
1349
1350        // all peers run DKG
1351        let leader_amount = leader.amount;
1352        let leader_name = leader.name.clone();
1353        followers.push(leader);
1354        let all_peers = Arc::new(followers);
1355        let (results, ()) = tokio::join!(
1356            join_all(
1357                all_peers
1358                    .iter()
1359                    .map(|peer| peer.client.run_dkg(peer.auth.clone()))
1360            ),
1361            all_peers[0].wait_status(ServerStatus::VerifyingConfigs)
1362        );
1363        for result in results {
1364            result.expect("DKG failed");
1365        }
1366
1367        // verify config hashes equal for all peers
1368        let mut hashes = HashSet::new();
1369        for peer in all_peers.iter() {
1370            peer.wait_status(ServerStatus::VerifyingConfigs).await;
1371            hashes.insert(
1372                peer.client
1373                    .get_verify_config_hash(peer.auth.clone())
1374                    .await
1375                    .unwrap(),
1376            );
1377        }
1378        assert_eq!(hashes.len(), 1);
1379
1380        // set verified configs
1381        for peer in all_peers.iter() {
1382            peer.client.verified_configs(peer.auth.clone()).await.ok();
1383        }
1384
1385        // start consensus
1386        for peer in all_peers.iter() {
1387            peer.client.start_consensus(peer.auth.clone()).await.ok();
1388        }
1389
1390        sleep(Duration::from_secs(5)).await;
1391
1392        for peer in all_peers.iter() {
1393            assert_eq!(peer.status().await.server, ServerStatus::ConsensusRunning);
1394
1395            // verify the local and consensus values for peers
1396            let cfg = peer.read_config(); // read persisted configs
1397            let dummy: DummyConfig = cfg.get_module_config_typed(0).unwrap();
1398            assert_eq!(dummy.consensus.tx_fee, leader_amount);
1399            assert_eq!(cfg.consensus.meta["\"test\""], leader_name);
1400        }
1401    }
1402}