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 fedimint_api_client::api::{DynGlobalApi, StatusResponse};
9use fedimint_bitcoind::create_bitcoind;
10use fedimint_core::admin_client::{
11    ConfigGenConnectionsRequest, ConfigGenParamsConsensus, ConfigGenParamsRequest,
12    ConfigGenParamsResponse, PeerServerParams, ServerStatus,
13};
14use fedimint_core::config::{ConfigGenModuleParams, ServerModuleConfigGenParamsRegistry};
15use fedimint_core::core::ModuleInstanceId;
16use fedimint_core::db::Database;
17use fedimint_core::encoding::Encodable;
18use fedimint_core::endpoint_constants::{
19    ADD_CONFIG_GEN_PEER_ENDPOINT, AUTH_ENDPOINT, CHECK_BITCOIN_STATUS_ENDPOINT,
20    CONFIG_GEN_PEERS_ENDPOINT, CONSENSUS_CONFIG_GEN_PARAMS_ENDPOINT,
21    DEFAULT_CONFIG_GEN_PARAMS_ENDPOINT, RESTART_FEDERATION_SETUP_ENDPOINT, RUN_DKG_ENDPOINT,
22    SET_CONFIG_GEN_CONNECTIONS_ENDPOINT, SET_CONFIG_GEN_PARAMS_ENDPOINT, SET_PASSWORD_ENDPOINT,
23    START_CONSENSUS_ENDPOINT, STATUS_ENDPOINT, VERIFIED_CONFIGS_ENDPOINT,
24    VERIFY_CONFIG_HASH_ENDPOINT,
25};
26use fedimint_core::envs::BitcoinRpcConfig;
27use fedimint_core::module::{
28    api_endpoint, ApiAuth, ApiEndpoint, ApiEndpointContext, ApiError, ApiRequestErased, ApiVersion,
29};
30use fedimint_core::task::{sleep, TaskGroup};
31use fedimint_core::util::SafeUrl;
32use fedimint_core::PeerId;
33use fedimint_server_core::ServerModuleInitRegistry;
34use itertools::Itertools;
35use serde::{Deserialize, Serialize};
36use tokio::sync::mpsc::Sender;
37use tokio::sync::{Mutex, MutexGuard, RwLock};
38use tokio::time::Instant;
39use tokio_rustls::rustls;
40use tracing::{error, info};
41
42use crate::config::{gen_cert_and_key, ConfigGenParams, ServerConfig};
43use crate::envs::FM_PEER_ID_SORT_BY_URL_ENV;
44use crate::net::api::{check_auth, ApiResult, HasApiContext};
45
46/// Serves the config gen API endpoints
47#[derive(Clone)]
48pub struct ConfigGenApi {
49    /// In-memory state machine
50    state: Arc<Mutex<ConfigGenState>>,
51    /// DB not really used
52    db: Database,
53    /// Tracks when the config is generated
54    config_generated_tx: Sender<ServerConfig>,
55    /// Task group for running DKG
56    task_group: TaskGroup,
57    /// Code version str that will get encoded in consensus hash
58    code_version_str: String,
59    /// Api secret to use
60    api_secret: Option<String>,
61    p2p_bind_addr: SocketAddr,
62    bitcoin_status_cache: Arc<RwLock<Option<(Instant, BitcoinRpcConnectionStatus)>>>,
63    bitcoin_status_cache_duration: Duration,
64}
65
66impl ConfigGenApi {
67    pub fn new(
68        p2p_bind_addr: SocketAddr,
69        settings: ConfigGenSettings,
70        db: Database,
71        config_generated_tx: Sender<ServerConfig>,
72        task_group: &TaskGroup,
73        code_version_str: String,
74        api_secret: Option<String>,
75    ) -> Self {
76        let config_gen_api = Self {
77            state: Arc::new(Mutex::new(ConfigGenState::new(settings))),
78            db,
79            config_generated_tx,
80            task_group: task_group.clone(),
81            code_version_str,
82            api_secret,
83            p2p_bind_addr,
84            bitcoin_status_cache: Arc::new(RwLock::new(None)),
85            bitcoin_status_cache_duration: Duration::from_secs(60),
86        };
87        info!(target: fedimint_logging::LOG_NET_PEER_DKG, "Created new config gen Api");
88        config_gen_api
89    }
90
91    // Sets the auth and decryption key derived from the password
92    pub async fn set_password(&self, auth: ApiAuth) -> ApiResult<()> {
93        let mut state = self.require_status(ServerStatus::AwaitingPassword).await?;
94        let auth_trimmed = auth.0.trim();
95        if auth_trimmed != auth.0 {
96            return Err(ApiError::bad_request(
97                "Password contains leading/trailing whitespace".to_string(),
98            ));
99        }
100        state.auth = Some(auth);
101        state.status = ServerStatus::SharingConfigGenParams;
102        info!(
103            target: fedimint_logging::LOG_NET_PEER_DKG,
104            "Set password for config gen"
105        );
106        Ok(())
107    }
108
109    async fn require_status(&self, status: ServerStatus) -> ApiResult<MutexGuard<ConfigGenState>> {
110        let state = self.state.lock().await;
111        if state.status != status {
112            return Self::bad_request(&format!("Expected to be in {status:?} state"));
113        }
114        Ok(state)
115    }
116
117    async fn require_any_status(
118        &self,
119        statuses: &[ServerStatus],
120    ) -> ApiResult<MutexGuard<ConfigGenState>> {
121        let state = self.state.lock().await;
122        if !statuses.contains(&state.status) {
123            return Self::bad_request(&format!("Expected to be in one of {statuses:?} states"));
124        }
125        Ok(state)
126    }
127
128    /// Sets our connection info, possibly sending it to a leader
129    pub async fn set_config_gen_connections(
130        &self,
131        request: ConfigGenConnectionsRequest,
132    ) -> ApiResult<()> {
133        {
134            let mut state = self
135                .require_status(ServerStatus::SharingConfigGenParams)
136                .await?;
137            state.set_request(request)?;
138        }
139        self.update_leader().await?;
140        Ok(())
141    }
142
143    /// Sends our updated peer info to the leader (if we have one)
144    async fn update_leader(&self) -> ApiResult<()> {
145        let state = self.state.lock().await.clone();
146        let local = state.local.clone();
147
148        if let Some(url) = local.and_then(|local| local.leader_api_url) {
149            DynGlobalApi::from_pre_peer_id_admin_endpoint(url, &self.api_secret)
150                .add_config_gen_peer(state.our_peer_info()?)
151                .await
152                .map_err(|_| ApiError::not_found("Unable to connect to the leader".to_string()))?;
153        }
154        Ok(())
155    }
156
157    /// Called from `set_config_gen_connections` to add a peer's connection info
158    /// to the leader
159    pub async fn add_config_gen_peer(&self, peer: PeerServerParams) -> ApiResult<()> {
160        let mut state = self.state.lock().await;
161        state.peers.insert(peer.api_url.clone(), peer);
162        info!(target: fedimint_logging::LOG_NET_PEER_DKG, "New peer added to config gen");
163        Ok(())
164    }
165
166    /// Returns the peers that have called `add_config_gen_peer` on the leader
167    pub async fn config_gen_peers(&self) -> ApiResult<Vec<PeerServerParams>> {
168        let state = self.state.lock().await;
169        Ok(state.get_peer_info().into_values().collect())
170    }
171
172    /// Returns default config gen params that can be modified by the leader
173    pub async fn default_config_gen_params(&self) -> ApiResult<ConfigGenParamsRequest> {
174        let state = self.state.lock().await;
175        Ok(state.settings.default_params.clone())
176    }
177
178    /// Sets and validates the config gen params
179    ///
180    /// The leader passes consensus params, everyone passes local params
181    pub async fn set_config_gen_params(&self, request: ConfigGenParamsRequest) -> ApiResult<()> {
182        self.consensus_config_gen_params(&request).await?;
183        let mut state = self
184            .require_status(ServerStatus::SharingConfigGenParams)
185            .await?;
186        state.requested_params = Some(request);
187        info!(
188            target: fedimint_logging::LOG_NET_PEER_DKG,
189            "Set params for config gen"
190        );
191        Ok(())
192    }
193
194    async fn get_requested_params(&self) -> ApiResult<ConfigGenParamsRequest> {
195        let state = self.state.lock().await.clone();
196        state.requested_params.ok_or(ApiError::bad_request(
197            "Config params were not set on this guardian".to_string(),
198        ))
199    }
200
201    /// Gets the consensus config gen params
202    pub async fn consensus_config_gen_params(
203        &self,
204        request: &ConfigGenParamsRequest,
205    ) -> ApiResult<ConfigGenParamsResponse> {
206        let state = self.state.lock().await.clone();
207        let local = state.local.clone();
208
209        let consensus = match local.and_then(|local| local.leader_api_url) {
210            Some(leader_url) => {
211                let client = DynGlobalApi::from_pre_peer_id_admin_endpoint(
212                    leader_url.clone(),
213                    &self.api_secret,
214                );
215                let response = client.consensus_config_gen_params().await;
216                response
217                    .map_err(|_| ApiError::not_found("Cannot get leader params".to_string()))?
218                    .consensus
219            }
220            None => ConfigGenParamsConsensus {
221                peers: state.get_peer_info(),
222                meta: request.meta.clone(),
223                modules: request.modules.clone(),
224            },
225        };
226
227        let params = state.get_config_gen_params(request, consensus.clone())?;
228        Ok(ConfigGenParamsResponse {
229            consensus,
230            our_current_id: params.local.our_id,
231        })
232    }
233
234    /// Once configs are generated, updates status to ReadyForConfigGen and
235    /// spawns a task to coordinate DKG, then returns. Coordinating DKG in a
236    /// separate thread allows clients to poll the server status instead of
237    /// blocking until completion, which can be fragile due to timeouts, poor
238    /// network connections, etc.
239    ///
240    /// Calling a second time will return an error.
241    pub async fn run_dkg(&self) -> ApiResult<()> {
242        let leader = {
243            let mut state = self
244                .require_status(ServerStatus::SharingConfigGenParams)
245                .await?;
246            // Update our state
247            state.status = ServerStatus::ReadyForConfigGen;
248            info!(
249                target: fedimint_logging::LOG_NET_PEER_DKG,
250                "Update config gen status to 'Ready for config gen'"
251            );
252            // Create a WSClient for the leader
253            state.local.clone().and_then(|local| {
254                local.leader_api_url.map(|url| {
255                    DynGlobalApi::from_pre_peer_id_admin_endpoint(url, &self.api_secret.clone())
256                })
257            })
258        };
259
260        self.update_leader().await?;
261
262        let self_clone = self.clone();
263        let sub_group = self.task_group.make_subgroup();
264        let p2p_bind_addr = self.p2p_bind_addr;
265        sub_group.spawn("run dkg", move |_handle| async move {
266            // Followers wait for leader to signal readiness for DKG
267            if let Some(client) = leader {
268                loop {
269                    let status = client.status().await.map_err(|_| {
270                        ApiError::not_found("Unable to connect to the leader".to_string())
271                    })?;
272                    if status.server == ServerStatus::ReadyForConfigGen {
273                        break;
274                    }
275                    sleep(Duration::from_millis(100)).await;
276                }
277            };
278
279            // Get params and registry
280            let request = self_clone.get_requested_params().await?;
281            let response = self_clone.consensus_config_gen_params(&request).await?;
282            let (params, registry) = {
283                let state: MutexGuard<'_, ConfigGenState> = self_clone
284                    .require_status(ServerStatus::ReadyForConfigGen)
285                    .await?;
286                let params = state.get_config_gen_params(&request, response.consensus)?;
287                let registry = state.settings.registry.clone();
288                (params, registry)
289            };
290
291            // Run DKG
292            let task_group: TaskGroup = self_clone.task_group.make_subgroup();
293            let config = ServerConfig::distributed_gen(
294                p2p_bind_addr,
295                &params,
296                registry,
297                &task_group,
298                self_clone.code_version_str.clone(),
299            )
300            .await;
301            task_group
302                .shutdown_join_all(None)
303                .await
304                .expect("shuts down");
305
306            {
307                let mut state = self_clone.state.lock().await;
308                match config {
309                    Ok(config) => {
310                        state.status = ServerStatus::VerifyingConfigs;
311                        state.config = Some(config);
312                        info!(
313                            target: fedimint_logging::LOG_NET_PEER_DKG,
314                            "Set config for config gen"
315                        );
316                    }
317                    Err(e) => {
318                        error!(
319                            target: fedimint_logging::LOG_NET_PEER_DKG,
320                            "DKG failed with {:?}", e
321                        );
322                        state.status = ServerStatus::ConfigGenFailed;
323                        info!(
324                            target: fedimint_logging::LOG_NET_PEER_DKG,
325                            "Update config gen status to 'Config gen failed'"
326                        );
327                    }
328                }
329            }
330            self_clone.update_leader().await
331        });
332
333        Ok(())
334    }
335
336    /// Returns tagged hashes of consensus config to be shared with other peers.
337    /// The hashes are tagged with the peer id  such that they are unique to
338    /// each peer and their manual verification by the guardians via the UI is
339    /// more robust.
340    pub async fn verify_config_hash(&self) -> ApiResult<BTreeMap<PeerId, sha256::Hash>> {
341        let expected_status = [
342            ServerStatus::VerifyingConfigs,
343            ServerStatus::VerifiedConfigs,
344        ];
345
346        let state = self.require_any_status(&expected_status).await?;
347
348        let config = state
349            .config
350            .clone()
351            .ok_or(ApiError::bad_request("Missing config".to_string()))?;
352
353        let verification_hashes = config
354            .consensus
355            .api_endpoints
356            .keys()
357            .map(|peer| (*peer, (*peer, config.consensus.clone()).consensus_hash()))
358            .collect();
359
360        Ok(verification_hashes)
361    }
362
363    /// We have verified all our peer configs
364    pub async fn verified_configs(&self) -> ApiResult<()> {
365        {
366            let expected_status = [
367                ServerStatus::VerifyingConfigs,
368                ServerStatus::VerifiedConfigs,
369            ];
370            let mut state = self.require_any_status(&expected_status).await?;
371            if state.status == ServerStatus::VerifiedConfigs {
372                return Ok(());
373            }
374            state.status = ServerStatus::VerifiedConfigs;
375            info!(
376                target: fedimint_logging::LOG_NET_PEER_DKG,
377                "Update config gen status to 'Verified configs'"
378            );
379        }
380
381        self.update_leader().await?;
382        Ok(())
383    }
384
385    pub async fn start_consensus(&self) -> ApiResult<()> {
386        let state = self
387            .require_any_status(&[
388                ServerStatus::VerifyingConfigs,
389                ServerStatus::VerifiedConfigs,
390            ])
391            .await?;
392
393        self.config_generated_tx
394            .send(state.config.clone().expect("Config should exist"))
395            .await
396            .expect("Can send");
397
398        Ok(())
399    }
400
401    /// Returns the server status
402    pub async fn server_status(&self) -> ServerStatus {
403        self.state.lock().await.status.clone()
404    }
405
406    fn bad_request<T>(msg: &str) -> ApiResult<T> {
407        Err(ApiError::bad_request(msg.to_string()))
408    }
409
410    pub async fn restart_federation_setup(&self) -> ApiResult<()> {
411        let leader = {
412            let expected_status = [
413                ServerStatus::SharingConfigGenParams,
414                ServerStatus::ReadyForConfigGen,
415                ServerStatus::ConfigGenFailed,
416                ServerStatus::VerifyingConfigs,
417                ServerStatus::VerifiedConfigs,
418            ];
419            let mut state = self.require_any_status(&expected_status).await?;
420
421            state.status = ServerStatus::SetupRestarted;
422            info!(
423                target: fedimint_logging::LOG_NET_PEER_DKG,
424                "Update config gen status to 'Setup restarted'"
425            );
426            // Create a WSClient for the leader
427            state.local.clone().and_then(|local| {
428                local
429                    .leader_api_url
430                    .map(|url| DynGlobalApi::from_pre_peer_id_admin_endpoint(url, &self.api_secret))
431            })
432        };
433
434        self.update_leader().await?;
435
436        // Followers wait for leader to signal that all peers have restarted setup
437        // The leader will signal this by setting it's status to AwaitingPassword
438        let self_clone = self.clone();
439        let sub_group = self.task_group.make_subgroup();
440        sub_group.spawn("restart", |_handle| async move {
441            if let Some(client) = leader {
442                self_clone.await_leader_restart(&client).await?;
443            } else {
444                self_clone.await_peer_restart().await;
445            }
446            // Progress status to AwaitingPassword
447            {
448                let mut state = self_clone.state.lock().await;
449                state.reset();
450            }
451            self_clone.update_leader().await
452        });
453
454        Ok(())
455    }
456
457    // Followers wait for leader to signal that all peers have restarted setup
458    async fn await_leader_restart(&self, client: &DynGlobalApi) -> ApiResult<()> {
459        let mut retries = 0;
460        loop {
461            if let Ok(status) = client.status().await {
462                if status.server == ServerStatus::AwaitingPassword
463                    || status.server == ServerStatus::SharingConfigGenParams
464                {
465                    break Ok(());
466                }
467            } else {
468                if retries > 3 {
469                    return Err(ApiError::not_found(
470                        "Unable to connect to the leader".to_string(),
471                    ));
472                }
473                retries += 1;
474            }
475            sleep(Duration::from_millis(100)).await;
476        }
477    }
478
479    // Leader waits for all peers to restart setup,
480    async fn await_peer_restart(&self) {
481        loop {
482            {
483                let state = self.state.lock().await;
484                let peers = state.peers.clone();
485                if peers
486                    .values()
487                    .all(|peer| peer.status == Some(ServerStatus::SetupRestarted))
488                {
489                    break;
490                }
491            }
492            sleep(Duration::from_millis(100)).await;
493        }
494    }
495
496    // Check the status of the bitcoin rpc connection
497    pub async fn check_bitcoin_status(&self) -> ApiResult<BitcoinRpcConnectionStatus> {
498        // Check the cache first
499        {
500            let cached_status = self.bitcoin_status_cache.read().await;
501            if let Some((timestamp, status)) = cached_status.as_ref() {
502                if timestamp.elapsed() < self.bitcoin_status_cache_duration {
503                    return Ok(*status);
504                }
505            }
506        }
507
508        // If cache is invalid or expired, fetch new status
509        let status = Self::fetch_bitcoin_status().await?;
510
511        // Update the bitcoin status cache
512        let mut cached_status = self.bitcoin_status_cache.write().await;
513        *cached_status = Some((Instant::now(), status));
514
515        Ok(status)
516    }
517
518    async fn fetch_bitcoin_status() -> ApiResult<BitcoinRpcConnectionStatus> {
519        let bitcoin_rpc_config = BitcoinRpcConfig::get_defaults_from_env_vars().map_err(|e| {
520            ApiError::server_error(format!("Failed to get bitcoin rpc env vars: {e}"))
521        })?;
522        let client = create_bitcoind(&bitcoin_rpc_config).map_err(|e| {
523            ApiError::server_error(format!("Failed to connect to bitcoin rpc: {e}"))
524        })?;
525        let block_count = client.get_block_count().await.map_err(|e| {
526            ApiError::server_error(format!("Failed to get block count from bitcoin rpc: {e}"))
527        })?;
528        let chain_tip_block_height = block_count - 1;
529        let chain_tip_block_hash = client
530            .get_block_hash(chain_tip_block_height)
531            .await
532            .map_err(|e| {
533                ApiError::server_error(format!(
534                    "Failed to get block hash for block count {block_count} from bitcoin rpc: {e}"
535                ))
536            })?;
537        let chain_tip_block = client.get_block(&chain_tip_block_hash).await.map_err(|e| {
538            ApiError::server_error(format!(
539                "Failed to get block for block hash {chain_tip_block_hash} from bitcoin rpc: {e}"
540            ))
541        })?;
542        let chain_tip_block_time = chain_tip_block.header.time;
543        let sync_percentage = client.get_sync_percentage().await.map_err(|e| {
544            ApiError::server_error(format!(
545                "Failed to get sync percentage from bitcoin rpc: {e}"
546            ))
547        })?;
548
549        Ok(BitcoinRpcConnectionStatus {
550            chain_tip_block_height,
551            chain_tip_block_time,
552            sync_percentage,
553        })
554    }
555}
556
557#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
558pub struct BitcoinRpcConnectionStatus {
559    chain_tip_block_height: u64,
560    chain_tip_block_time: u32,
561    sync_percentage: Option<f64>,
562}
563
564/// Config gen params that are only used locally, shouldn't be shared
565#[derive(Debug, Clone)]
566pub struct ConfigGenParamsLocal {
567    /// Our peer id
568    pub our_id: PeerId,
569    /// Our TLS private key
570    pub our_private_key: rustls::PrivateKey,
571    /// Secret API auth string
572    pub api_auth: ApiAuth,
573    /// Bind address for P2P communication
574    pub p2p_bind: SocketAddr,
575    /// Bind address for API communication
576    pub api_bind: SocketAddr,
577    /// How many API connections we will accept
578    pub max_connections: u32,
579}
580
581/// All the info we configure prior to config gen starting
582#[derive(Debug, Clone)]
583pub struct ConfigGenSettings {
584    /// Limit on the number of times a config download token can be used
585    pub download_token_limit: Option<u64>,
586    /// Bind address for our P2P connection
587    pub p2p_bind: SocketAddr,
588    /// Bind address for our API connection
589    pub api_bind: SocketAddr,
590    /// URL for our P2P connection
591    pub p2p_url: SafeUrl,
592    /// URL for our API connection
593    pub api_url: SafeUrl,
594    /// The default params for the modules
595    pub default_params: ConfigGenParamsRequest,
596    /// How many API connections we will accept
597    pub max_connections: u32,
598    /// Registry for config gen
599    pub registry: ServerModuleInitRegistry,
600}
601
602/// State held by the API after receiving a `ConfigGenConnectionsRequest`
603#[derive(Debug, Clone)]
604pub struct ConfigGenState {
605    /// Our config gen settings configured locally
606    settings: ConfigGenSettings,
607    /// Our auth string
608    auth: Option<ApiAuth>,
609    /// Our local connection
610    local: Option<ConfigGenLocalConnection>,
611    /// Connection info received from other guardians, unique by api_url
612    /// (because it's non-user configurable)
613    peers: BTreeMap<SafeUrl, PeerServerParams>,
614    /// The config gen params requested by the leader
615    requested_params: Option<ConfigGenParamsRequest>,
616    /// Our status
617    status: ServerStatus,
618    /// Configs that have been generated
619    config: Option<ServerConfig>,
620}
621
622/// Our local connection info
623#[derive(Debug, Clone)]
624struct ConfigGenLocalConnection {
625    /// Our TLS private key
626    tls_private: rustls::PrivateKey,
627    /// Our TLS public cert
628    tls_cert: rustls::Certificate,
629    /// Our guardian name
630    our_name: String,
631    /// URL of "leader" guardian to send our connection info to
632    /// Will be `None` if we are the leader
633    leader_api_url: Option<SafeUrl>,
634}
635
636impl ConfigGenState {
637    fn new(settings: ConfigGenSettings) -> Self {
638        Self {
639            settings,
640            auth: None,
641            local: None,
642            peers: BTreeMap::new(),
643            requested_params: None,
644            status: ServerStatus::AwaitingPassword,
645            config: None,
646        }
647    }
648
649    fn set_request(&mut self, request: ConfigGenConnectionsRequest) -> ApiResult<()> {
650        let (tls_cert, tls_private) = gen_cert_and_key(&request.our_name)
651            .map_err(|_| ApiError::server_error("Unable to generate TLS keys".to_string()))?;
652        self.local = Some(ConfigGenLocalConnection {
653            tls_private,
654            tls_cert,
655            our_name: request.our_name,
656            leader_api_url: request.leader_api_url,
657        });
658        info!(
659            target: fedimint_logging::LOG_NET_PEER_DKG,
660            "Set local connection for config gen"
661        );
662        Ok(())
663    }
664
665    fn local_connection(&self) -> ApiResult<ConfigGenLocalConnection> {
666        self.local.clone().ok_or(ApiError::bad_request(
667            "Our connection info not set yet".to_string(),
668        ))
669    }
670
671    fn auth(&self) -> ApiResult<ApiAuth> {
672        self.auth
673            .clone()
674            .ok_or(ApiError::bad_request("Missing auth".to_string()))
675    }
676
677    fn our_peer_info(&self) -> ApiResult<PeerServerParams> {
678        let local = self.local_connection()?;
679        Ok(PeerServerParams {
680            cert: local.tls_cert.clone(),
681            p2p_url: self.settings.p2p_url.clone(),
682            api_url: self.settings.api_url.clone(),
683            name: local.our_name,
684            status: Some(self.status.clone()),
685        })
686    }
687
688    fn get_peer_info(&self) -> BTreeMap<PeerId, PeerServerParams> {
689        self.peers
690            .values()
691            .cloned()
692            .chain(self.our_peer_info().ok())
693            // Since sort order here is arbitrary, try to sort by nick-names first for more natural
694            // 'name -> id' mapping, which is helpful when operating on 'peer-ids' (debugging etc.);
695            // Ties are OK (to_lowercase), not important in practice.
696            .sorted_by_cached_key(|peer| {
697                // in certain (very obscure) cases, it might be worthwhile to sort by urls, so
698                // just expose it as an env var; probably no need to document it too much
699                if std::env::var_os(FM_PEER_ID_SORT_BY_URL_ENV).is_some_and(|var| !var.is_empty()) {
700                    peer.api_url.to_string()
701                } else {
702                    peer.name.to_lowercase()
703                }
704            })
705            .enumerate()
706            .map(|(i, peer)| (PeerId::from(i as u16), peer))
707            .collect()
708    }
709
710    /// Validates and returns the params using our `request` and `consensus`
711    /// which comes from the leader
712    fn get_config_gen_params(
713        &self,
714        request: &ConfigGenParamsRequest,
715        mut consensus: ConfigGenParamsConsensus,
716    ) -> ApiResult<ConfigGenParams> {
717        let local_connection = self.local_connection()?;
718
719        let (our_id, _) = consensus
720            .peers
721            .iter()
722            .find(|(_, param)| local_connection.tls_cert == param.cert)
723            .ok_or(ApiError::bad_request(
724                "Our TLS cert not found among peers".to_string(),
725            ))?;
726
727        let mut combined_params = vec![];
728        let default_params = self.settings.default_params.modules.clone();
729        let local_params = request.modules.clone();
730        let consensus_params = consensus.modules.clone();
731        // Use defaults in case local or consensus params are missing
732        for (id, kind, default) in default_params.iter_modules() {
733            let consensus = &consensus_params.get(id).unwrap_or(default).consensus;
734            let local = &local_params.get(id).unwrap_or(default).local;
735            let combined = ConfigGenModuleParams::new(local.clone(), consensus.clone());
736            // Check that the params are parseable
737            let module = self.settings.registry.get(kind).expect("Module exists");
738            module.validate_params(&combined).map_err(|e| {
739                ApiError::bad_request(format!(
740                    "Module {} params invalid: {}",
741                    id,
742                    itertools::join(e.chain(), ": ")
743                ))
744            })?;
745            combined_params.push((id, kind.clone(), combined));
746        }
747        consensus.modules = ServerModuleConfigGenParamsRegistry::from_iter(combined_params);
748
749        let local = ConfigGenParamsLocal {
750            our_id: *our_id,
751            our_private_key: local_connection.tls_private,
752            api_auth: self.auth()?,
753            p2p_bind: self.settings.p2p_bind,
754            api_bind: self.settings.api_bind,
755            max_connections: self.settings.max_connections,
756        };
757
758        Ok(ConfigGenParams { local, consensus })
759    }
760
761    fn reset(&mut self) {
762        self.auth = None;
763        self.local = None;
764        self.peers = BTreeMap::new();
765        self.requested_params = None;
766        self.status = ServerStatus::AwaitingPassword;
767        self.config = None;
768
769        info!(
770            target: fedimint_logging::LOG_NET_PEER_DKG,
771            "Reset config gen state"
772        );
773    }
774}
775
776#[async_trait]
777impl HasApiContext<ConfigGenApi> for ConfigGenApi {
778    async fn context(
779        &self,
780        request: &ApiRequestErased,
781        id: Option<ModuleInstanceId>,
782    ) -> (&ConfigGenApi, ApiEndpointContext<'_>) {
783        let mut db = self.db.clone();
784        let mut dbtx = self.db.begin_transaction().await;
785        if let Some(id) = id {
786            db = self.db.with_prefix_module_id(id).0;
787            dbtx = dbtx.with_prefix_module_id(id).0;
788        }
789        let state = self.state.lock().await;
790        let auth = request.auth.as_ref();
791        let has_auth = match state.auth.clone() {
792            // The first client to connect gets the set the password
793            None => true,
794            Some(configured_auth) => Some(&configured_auth) == auth,
795        };
796
797        (
798            self,
799            ApiEndpointContext::new(db, dbtx, has_auth, request.auth.clone()),
800        )
801    }
802}
803
804pub fn server_endpoints() -> Vec<ApiEndpoint<ConfigGenApi>> {
805    vec![
806        api_endpoint! {
807            SET_PASSWORD_ENDPOINT,
808            ApiVersion::new(0, 0),
809            async |config: &ConfigGenApi, context, _v: ()| -> () {
810                match context.request_auth() {
811                    None => return Err(ApiError::bad_request("Missing password".to_string())),
812                    Some(auth) => config.set_password(auth).await
813                }
814            }
815        },
816        api_endpoint! {
817            SET_CONFIG_GEN_CONNECTIONS_ENDPOINT,
818            ApiVersion::new(0, 0),
819            async |config: &ConfigGenApi, context, server: ConfigGenConnectionsRequest| -> () {
820                check_auth(context)?;
821                config.set_config_gen_connections(server).await
822            }
823        },
824        api_endpoint! {
825            ADD_CONFIG_GEN_PEER_ENDPOINT,
826            ApiVersion::new(0, 0),
827            async |config: &ConfigGenApi, _context, peer: PeerServerParams| -> () {
828                // No auth required since this is an API-to-API call and the peer connections will be manually accepted or not in the UI
829                config.add_config_gen_peer(peer).await
830            }
831        },
832        api_endpoint! {
833            CONFIG_GEN_PEERS_ENDPOINT,
834            ApiVersion::new(0, 0),
835            async |config: &ConfigGenApi, _context, _v: ()| -> Vec<PeerServerParams> {
836                config.config_gen_peers().await
837            }
838        },
839        api_endpoint! {
840            DEFAULT_CONFIG_GEN_PARAMS_ENDPOINT,
841            ApiVersion::new(0, 0),
842            async |config: &ConfigGenApi, context,  _v: ()| -> ConfigGenParamsRequest {
843                check_auth(context)?;
844                config.default_config_gen_params().await
845            }
846        },
847        api_endpoint! {
848            SET_CONFIG_GEN_PARAMS_ENDPOINT,
849            ApiVersion::new(0, 0),
850            async |config: &ConfigGenApi, context, params: ConfigGenParamsRequest| -> () {
851                check_auth(context)?;
852                config.set_config_gen_params(params).await
853            }
854        },
855        api_endpoint! {
856            CONSENSUS_CONFIG_GEN_PARAMS_ENDPOINT,
857            ApiVersion::new(0, 0),
858            async |config: &ConfigGenApi, _context, _v: ()| -> ConfigGenParamsResponse {
859                let request = config.get_requested_params().await?;
860                config.consensus_config_gen_params(&request).await
861            }
862        },
863        api_endpoint! {
864            RUN_DKG_ENDPOINT,
865            ApiVersion::new(0, 0),
866            async |config: &ConfigGenApi, context, _v: ()| -> () {
867                check_auth(context)?;
868                config.run_dkg().await
869            }
870        },
871        api_endpoint! {
872            VERIFY_CONFIG_HASH_ENDPOINT,
873            ApiVersion::new(0, 0),
874            async |config: &ConfigGenApi, context, _v: ()| -> BTreeMap<PeerId, sha256::Hash> {
875                check_auth(context)?;
876                config.verify_config_hash().await
877            }
878        },
879        api_endpoint! {
880            VERIFIED_CONFIGS_ENDPOINT,
881            ApiVersion::new(0, 0),
882            async |config: &ConfigGenApi, context, _v: ()| -> () {
883                check_auth(context)?;
884                config.verified_configs().await
885            }
886        },
887        api_endpoint! {
888            START_CONSENSUS_ENDPOINT,
889            ApiVersion::new(0, 0),
890            async |config: &ConfigGenApi, context, _v: ()| -> () {
891                check_auth(context)?;
892                config.start_consensus().await
893            }
894        },
895        api_endpoint! {
896            STATUS_ENDPOINT,
897            ApiVersion::new(0, 0),
898            async |config: &ConfigGenApi, _context, _v: ()| -> StatusResponse {
899                let server = config.server_status().await;
900                Ok(StatusResponse {
901                    server,
902                    federation: None
903                })
904            }
905        },
906        api_endpoint! {
907            AUTH_ENDPOINT,
908            ApiVersion::new(0, 0),
909            async |_config: &ConfigGenApi, context, _v: ()| -> () {
910                check_auth(context)?;
911                Ok(())
912            }
913        },
914        api_endpoint! {
915            RESTART_FEDERATION_SETUP_ENDPOINT,
916            ApiVersion::new(0, 0),
917            async |config: &ConfigGenApi, context, _v: ()| -> () {
918                check_auth(context)?;
919                config.restart_federation_setup().await
920            }
921        },
922        api_endpoint! {
923            CHECK_BITCOIN_STATUS_ENDPOINT,
924            ApiVersion::new(0, 0),
925            async |config: &ConfigGenApi, context, _v: ()| -> BitcoinRpcConnectionStatus {
926                check_auth(context)?;
927                config.check_bitcoin_status().await
928            }
929        },
930    ]
931}