fedimint_server/consensus/
api.rs

1//! Implements the client API through which users interact with the federation
2use std::cmp::Ordering;
3use std::collections::{BTreeMap, HashMap};
4use std::path::{Path, PathBuf};
5use std::time::Duration;
6
7use anyhow::{Context, Result};
8use async_trait::async_trait;
9use bitcoin::hashes::sha256;
10use fedimint_aead::{encrypt, get_encryption_key, random_salt};
11use fedimint_api_client::api::{
12    LegacyFederationStatus, LegacyP2PConnectionStatus, LegacyPeerStatus, StatusResponse,
13};
14use fedimint_core::admin_client::{GuardianConfigBackup, ServerStatusLegacy, SetupStatus};
15use fedimint_core::backup::{
16    BackupStatistics, ClientBackupKey, ClientBackupKeyPrefix, ClientBackupSnapshot,
17};
18use fedimint_core::config::{ClientConfig, JsonClientConfig, META_FEDERATION_NAME_KEY};
19use fedimint_core::core::backup::{BACKUP_REQUEST_MAX_PAYLOAD_SIZE_BYTES, SignedBackupRequest};
20use fedimint_core::core::{DynOutputOutcome, ModuleInstanceId, ModuleKind};
21use fedimint_core::db::{
22    Committable, Database, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped,
23};
24#[allow(deprecated)]
25use fedimint_core::endpoint_constants::AWAIT_OUTPUT_OUTCOME_ENDPOINT;
26use fedimint_core::endpoint_constants::{
27    API_ANNOUNCEMENTS_ENDPOINT, AUDIT_ENDPOINT, AUTH_ENDPOINT, AWAIT_OUTPUTS_OUTCOMES_ENDPOINT,
28    AWAIT_SESSION_OUTCOME_ENDPOINT, AWAIT_SIGNED_SESSION_OUTCOME_ENDPOINT,
29    AWAIT_TRANSACTION_ENDPOINT, BACKUP_ENDPOINT, BACKUP_STATISTICS_ENDPOINT,
30    CHANGE_PASSWORD_ENDPOINT, CLIENT_CONFIG_ENDPOINT, CLIENT_CONFIG_JSON_ENDPOINT,
31    CONSENSUS_ORD_LATENCY_ENDPOINT, FEDERATION_ID_ENDPOINT, FEDIMINTD_VERSION_ENDPOINT,
32    GUARDIAN_CONFIG_BACKUP_ENDPOINT, INVITE_CODE_ENDPOINT, P2P_CONNECTION_STATUS_ENDPOINT,
33    P2P_CONNECTION_TYPE_ENDPOINT, RECOVER_ENDPOINT, SERVER_CONFIG_CONSENSUS_HASH_ENDPOINT,
34    SESSION_COUNT_ENDPOINT, SESSION_STATUS_ENDPOINT, SESSION_STATUS_V2_ENDPOINT,
35    SETUP_STATUS_ENDPOINT, SHUTDOWN_ENDPOINT, SIGN_API_ANNOUNCEMENT_ENDPOINT, STATUS_ENDPOINT,
36    SUBMIT_API_ANNOUNCEMENT_ENDPOINT, SUBMIT_TRANSACTION_ENDPOINT, VERSION_ENDPOINT,
37};
38use fedimint_core::epoch::ConsensusItem;
39use fedimint_core::module::audit::{Audit, AuditSummary};
40use fedimint_core::module::{
41    ApiAuth, ApiEndpoint, ApiEndpointContext, ApiError, ApiRequestErased, ApiResult, ApiVersion,
42    SerdeModuleEncoding, SerdeModuleEncodingBase64, SupportedApiVersionsSummary, api_endpoint,
43};
44use fedimint_core::net::api_announcement::{
45    ApiAnnouncement, SignedApiAnnouncement, SignedApiAnnouncementSubmission,
46};
47use fedimint_core::net::auth::{GuardianAuthToken, check_auth};
48use fedimint_core::secp256k1::{PublicKey, SECP256K1};
49use fedimint_core::session_outcome::{
50    SessionOutcome, SessionStatus, SessionStatusV2, SignedSessionOutcome,
51};
52use fedimint_core::task::TaskGroup;
53use fedimint_core::transaction::{
54    SerdeTransaction, Transaction, TransactionError, TransactionSubmissionOutcome,
55};
56use fedimint_core::util::{FmtCompact, SafeUrl};
57use fedimint_core::{OutPoint, OutPointRange, PeerId, TransactionId, secp256k1};
58use fedimint_logging::LOG_NET_API;
59use fedimint_server_core::bitcoin_rpc::ServerBitcoinRpcMonitor;
60use fedimint_server_core::dashboard_ui::{ConnectionType, IDashboardApi, ServerBitcoinRpcStatus};
61use fedimint_server_core::{DynServerModule, ServerModuleRegistry, ServerModuleRegistryExt};
62use futures::StreamExt;
63use tokio::sync::watch::{self, Receiver, Sender};
64use tracing::{debug, info, warn};
65
66use crate::config::io::{
67    CONSENSUS_CONFIG, ENCRYPTED_EXT, JSON_EXT, LOCAL_CONFIG, PRIVATE_CONFIG, SALT_FILE,
68    reencrypt_private_config,
69};
70use crate::config::{ServerConfig, legacy_consensus_config_hash};
71use crate::consensus::db::{AcceptedItemPrefix, AcceptedTransactionKey, SignedSessionOutcomeKey};
72use crate::consensus::engine::get_finished_session_count_static;
73use crate::consensus::transaction::{TxProcessingMode, process_transaction_with_dbtx};
74use crate::metrics::{BACKUP_WRITE_SIZE_BYTES, STORED_BACKUPS_COUNT};
75use crate::net::api::HasApiContext;
76use crate::net::api::announcement::{ApiAnnouncementKey, ApiAnnouncementPrefix};
77use crate::net::p2p::{P2PConnectionTypeReceivers, P2PStatusReceivers};
78
79#[derive(Clone)]
80pub struct ConsensusApi {
81    /// Our server configuration
82    pub cfg: ServerConfig,
83    /// Directory where config files are stored
84    pub cfg_dir: PathBuf,
85    /// Database for serving the API
86    pub db: Database,
87    /// Modules registered with the federation
88    pub modules: ServerModuleRegistry,
89    /// Cached client config
90    pub client_cfg: ClientConfig,
91    pub force_api_secret: Option<String>,
92    /// For sending API events to consensus such as transactions
93    pub submission_sender: async_channel::Sender<ConsensusItem>,
94    pub shutdown_receiver: Receiver<Option<u64>>,
95    pub shutdown_sender: Sender<Option<u64>>,
96    pub ord_latency_receiver: watch::Receiver<Option<Duration>>,
97    pub p2p_status_receivers: P2PStatusReceivers,
98    pub p2p_connection_type_receivers: P2PConnectionTypeReceivers,
99    pub ci_status_receivers: BTreeMap<PeerId, Receiver<Option<u64>>>,
100    pub bitcoin_rpc_connection: ServerBitcoinRpcMonitor,
101    pub supported_api_versions: SupportedApiVersionsSummary,
102    pub code_version_str: String,
103    pub task_group: TaskGroup,
104}
105
106impl ConsensusApi {
107    pub fn api_versions_summary(&self) -> &SupportedApiVersionsSummary {
108        &self.supported_api_versions
109    }
110
111    pub fn get_active_api_secret(&self) -> Option<String> {
112        // TODO: In the future, we might want to fetch it from the DB, so it's possible
113        // to customize from the UX
114        self.force_api_secret.clone()
115    }
116
117    // we want to return an error if and only if the submitted transaction is
118    // invalid and will be rejected if we were to submit it to consensus
119    pub async fn submit_transaction(
120        &self,
121        transaction: Transaction,
122    ) -> Result<TransactionId, TransactionError> {
123        let txid = transaction.tx_hash();
124
125        debug!(target: LOG_NET_API, %txid, "Received a submitted transaction");
126
127        // Create read-only DB tx so that the read state is consistent
128        let mut dbtx = self.db.begin_transaction_nc().await;
129        // we already processed the transaction before
130        if dbtx
131            .get_value(&AcceptedTransactionKey(txid))
132            .await
133            .is_some()
134        {
135            debug!(target: LOG_NET_API, %txid, "Transaction already accepted");
136            return Ok(txid);
137        }
138
139        // We ignore any writes, as we only verify if the transaction is valid here
140        dbtx.ignore_uncommitted();
141
142        process_transaction_with_dbtx(
143            self.modules.clone(),
144            &mut dbtx,
145            &transaction,
146            self.cfg.consensus.version,
147            TxProcessingMode::Submission,
148        )
149        .await
150        .inspect_err(|err| {
151            debug!(target: LOG_NET_API, %txid, err = %err.fmt_compact(), "Transaction rejected");
152        })?;
153
154        let _ = self
155            .submission_sender
156            .send(ConsensusItem::Transaction(transaction.clone()))
157            .await
158            .inspect_err(|err| {
159                warn!(target: LOG_NET_API, %txid, err = %err.fmt_compact(), "Unable to submit the tx into consensus");
160            });
161
162        Ok(txid)
163    }
164
165    pub async fn await_transaction(
166        &self,
167        txid: TransactionId,
168    ) -> (Vec<ModuleInstanceId>, DatabaseTransaction<'_, Committable>) {
169        self.db
170            .wait_key_check(&AcceptedTransactionKey(txid), std::convert::identity)
171            .await
172    }
173
174    pub async fn await_output_outcome(
175        &self,
176        outpoint: OutPoint,
177    ) -> Result<SerdeModuleEncoding<DynOutputOutcome>> {
178        let (module_ids, mut dbtx) = self.await_transaction(outpoint.txid).await;
179
180        let module_id = module_ids
181            .into_iter()
182            .nth(outpoint.out_idx as usize)
183            .with_context(|| format!("Outpoint index out of bounds {outpoint:?}"))?;
184
185        #[allow(deprecated)]
186        let outcome = self
187            .modules
188            .get_expect(module_id)
189            .output_status(
190                &mut dbtx.to_ref_with_prefix_module_id(module_id).0.into_nc(),
191                outpoint,
192                module_id,
193            )
194            .await
195            .context("No output outcome for outpoint")?;
196
197        Ok((&outcome).into())
198    }
199
200    pub async fn await_outputs_outcomes(
201        &self,
202        outpoint_range: OutPointRange,
203    ) -> Result<Vec<Option<SerdeModuleEncoding<DynOutputOutcome>>>> {
204        // Wait for the transaction to be accepted first
205        let (module_ids, mut dbtx) = self.await_transaction(outpoint_range.txid()).await;
206
207        let mut outcomes = Vec::with_capacity(outpoint_range.count());
208
209        for outpoint in outpoint_range {
210            let module_id = module_ids
211                .get(outpoint.out_idx as usize)
212                .with_context(|| format!("Outpoint index out of bounds {outpoint:?}"))?;
213
214            #[allow(deprecated)]
215            let outcome = self
216                .modules
217                .get_expect(*module_id)
218                .output_status(
219                    &mut dbtx.to_ref_with_prefix_module_id(*module_id).0.into_nc(),
220                    outpoint,
221                    *module_id,
222                )
223                .await
224                .map(|outcome| (&outcome).into());
225
226            outcomes.push(outcome);
227        }
228
229        Ok(outcomes)
230    }
231
232    pub async fn session_count(&self) -> u64 {
233        get_finished_session_count_static(&mut self.db.begin_transaction_nc().await).await
234    }
235
236    pub async fn await_signed_session_outcome(&self, index: u64) -> SignedSessionOutcome {
237        self.db
238            .wait_key_check(&SignedSessionOutcomeKey(index), std::convert::identity)
239            .await
240            .0
241    }
242
243    pub async fn session_status(&self, session_index: u64) -> SessionStatusV2 {
244        let mut dbtx = self.db.begin_transaction_nc().await;
245
246        match session_index.cmp(&get_finished_session_count_static(&mut dbtx).await) {
247            Ordering::Greater => SessionStatusV2::Initial,
248            Ordering::Equal => SessionStatusV2::Pending(
249                dbtx.find_by_prefix(&AcceptedItemPrefix)
250                    .await
251                    .map(|entry| entry.1)
252                    .collect()
253                    .await,
254            ),
255            Ordering::Less => SessionStatusV2::Complete(
256                dbtx.get_value(&SignedSessionOutcomeKey(session_index))
257                    .await
258                    .expect("There are no gaps in session outcomes"),
259            ),
260        }
261    }
262
263    pub async fn get_federation_status(&self) -> ApiResult<LegacyFederationStatus> {
264        let session_count = self.session_count().await;
265        let scheduled_shutdown = self.shutdown_receiver.borrow().to_owned();
266
267        let status_by_peer = self
268            .p2p_status_receivers
269            .iter()
270            .map(|(peer, p2p_receiver)| {
271                let ci_receiver = self.ci_status_receivers.get(peer).unwrap();
272
273                let consensus_status = LegacyPeerStatus {
274                    connection_status: match *p2p_receiver.borrow() {
275                        Some(..) => LegacyP2PConnectionStatus::Connected,
276                        None => LegacyP2PConnectionStatus::Disconnected,
277                    },
278                    last_contribution: *ci_receiver.borrow(),
279                    flagged: ci_receiver.borrow().unwrap_or(0) + 1 < session_count,
280                };
281
282                (*peer, consensus_status)
283            })
284            .collect::<HashMap<PeerId, LegacyPeerStatus>>();
285
286        let peers_flagged = status_by_peer
287            .values()
288            .filter(|status| status.flagged)
289            .count() as u64;
290
291        let peers_online = status_by_peer
292            .values()
293            .filter(|status| status.connection_status == LegacyP2PConnectionStatus::Connected)
294            .count() as u64;
295
296        let peers_offline = status_by_peer
297            .values()
298            .filter(|status| status.connection_status == LegacyP2PConnectionStatus::Disconnected)
299            .count() as u64;
300
301        Ok(LegacyFederationStatus {
302            session_count,
303            status_by_peer,
304            peers_online,
305            peers_offline,
306            peers_flagged,
307            scheduled_shutdown,
308        })
309    }
310
311    fn shutdown(&self, index: Option<u64>) {
312        self.shutdown_sender.send_replace(index);
313    }
314
315    async fn get_federation_audit(&self) -> ApiResult<AuditSummary> {
316        let mut dbtx = self.db.begin_transaction_nc().await;
317        // Writes are related to compacting audit keys, which we can safely ignore
318        // within an API request since the compaction will happen when constructing an
319        // audit in the consensus server
320        dbtx.ignore_uncommitted();
321
322        let mut audit = Audit::default();
323        let mut module_instance_id_to_kind: HashMap<ModuleInstanceId, String> = HashMap::new();
324        for (module_instance_id, kind, module) in self.modules.iter_modules() {
325            module_instance_id_to_kind.insert(module_instance_id, kind.as_str().to_string());
326            module
327                .audit(
328                    &mut dbtx.to_ref_with_prefix_module_id(module_instance_id).0,
329                    &mut audit,
330                    module_instance_id,
331                )
332                .await;
333        }
334        Ok(AuditSummary::from_audit(
335            &audit,
336            &module_instance_id_to_kind,
337        ))
338    }
339
340    /// Uses the in-memory config to write a config backup tar archive that
341    /// guardians can download. Private keys are encrypted with the guardian
342    /// password, so it should be safe to store anywhere, this also means the
343    /// backup is useless without the password.
344    fn get_guardian_config_backup(
345        &self,
346        password: &str,
347        _auth: &GuardianAuthToken,
348    ) -> GuardianConfigBackup {
349        let mut tar_archive_builder = tar::Builder::new(Vec::new());
350
351        let mut append = |name: &Path, data: &[u8]| {
352            let mut header = tar::Header::new_gnu();
353            header.set_path(name).expect("Error setting path");
354            header.set_size(data.len() as u64);
355            header.set_mode(0o644);
356            header.set_cksum();
357            tar_archive_builder
358                .append(&header, data)
359                .expect("Error adding data to tar archive");
360        };
361
362        append(
363            &PathBuf::from(LOCAL_CONFIG).with_extension(JSON_EXT),
364            &serde_json::to_vec(&self.cfg.local).expect("Error encoding local config"),
365        );
366
367        append(
368            &PathBuf::from(CONSENSUS_CONFIG).with_extension(JSON_EXT),
369            &serde_json::to_vec(&self.cfg.consensus).expect("Error encoding consensus config"),
370        );
371
372        // Note that the encrypted config returned here uses a different salt than the
373        // on-disk version. While this may be confusing it shouldn't be a problem since
374        // the content and encryption key are the same. It's unpractical to read the
375        // on-disk version here since the server/api aren't aware of the config dir and
376        // ideally we can keep it that way.
377        let encryption_salt = random_salt();
378        append(&PathBuf::from(SALT_FILE), encryption_salt.as_bytes());
379
380        let private_config_bytes =
381            serde_json::to_vec(&self.cfg.private).expect("Error encoding private config");
382        let encryption_key = get_encryption_key(password, &encryption_salt)
383            .expect("Generating key from password failed");
384        let private_config_encrypted =
385            hex::encode(encrypt(private_config_bytes, &encryption_key).expect("Encryption failed"));
386        append(
387            &PathBuf::from(PRIVATE_CONFIG).with_extension(ENCRYPTED_EXT),
388            private_config_encrypted.as_bytes(),
389        );
390
391        let tar_archive_bytes = tar_archive_builder
392            .into_inner()
393            .expect("Error building tar archive");
394
395        GuardianConfigBackup { tar_archive_bytes }
396    }
397
398    async fn handle_backup_request(
399        &self,
400        dbtx: &mut DatabaseTransaction<'_>,
401        request: SignedBackupRequest,
402    ) -> Result<(), ApiError> {
403        let request = request
404            .verify_valid(SECP256K1)
405            .map_err(|_| ApiError::bad_request("invalid request".into()))?;
406
407        if request.payload.len() > BACKUP_REQUEST_MAX_PAYLOAD_SIZE_BYTES {
408            return Err(ApiError::bad_request("snapshot too large".into()));
409        }
410        debug!(target: LOG_NET_API, id = %request.id, len = request.payload.len(), "Received client backup request");
411        if let Some(prev) = dbtx.get_value(&ClientBackupKey(request.id)).await
412            && request.timestamp <= prev.timestamp
413        {
414            debug!(target: LOG_NET_API, id = %request.id, len = request.payload.len(), "Received client backup request with old timestamp - ignoring");
415            return Err(ApiError::bad_request("timestamp too small".into()));
416        }
417
418        info!(target: LOG_NET_API, id = %request.id, len = request.payload.len(), "Storing new client backup");
419        let overwritten = dbtx
420            .insert_entry(
421                &ClientBackupKey(request.id),
422                &ClientBackupSnapshot {
423                    timestamp: request.timestamp,
424                    data: request.payload.clone(),
425                },
426            )
427            .await
428            .is_some();
429        BACKUP_WRITE_SIZE_BYTES.observe(request.payload.len() as f64);
430        if !overwritten {
431            dbtx.on_commit(|| STORED_BACKUPS_COUNT.inc());
432        }
433
434        Ok(())
435    }
436
437    async fn handle_recover_request(
438        &self,
439        dbtx: &mut DatabaseTransaction<'_>,
440        id: PublicKey,
441    ) -> Option<ClientBackupSnapshot> {
442        dbtx.get_value(&ClientBackupKey(id)).await
443    }
444
445    /// List API URL announcements from all peers we have received them from (at
446    /// least ourselves)
447    async fn api_announcements(&self) -> BTreeMap<PeerId, SignedApiAnnouncement> {
448        self.db
449            .begin_transaction_nc()
450            .await
451            .find_by_prefix(&ApiAnnouncementPrefix)
452            .await
453            .map(|(announcement_key, announcement)| (announcement_key.0, announcement))
454            .collect()
455            .await
456    }
457
458    /// Returns the tagged fedimintd version currently running
459    fn fedimintd_version(&self) -> String {
460        self.code_version_str.clone()
461    }
462
463    /// Add an API URL announcement from a peer to our database to be returned
464    /// by [`ConsensusApi::api_announcements`].
465    async fn submit_api_announcement(
466        &self,
467        peer_id: PeerId,
468        announcement: SignedApiAnnouncement,
469    ) -> Result<(), ApiError> {
470        let Some(peer_key) = self.cfg.consensus.broadcast_public_keys.get(&peer_id) else {
471            return Err(ApiError::bad_request("Peer not in federation".into()));
472        };
473
474        if !announcement.verify(SECP256K1, peer_key) {
475            return Err(ApiError::bad_request("Invalid signature".into()));
476        }
477
478        let mut dbtx = self.db.begin_transaction().await;
479
480        if let Some(existing_announcement) = dbtx.get_value(&ApiAnnouncementKey(peer_id)).await {
481            // If the current announcement is semantically identical to the new one (except
482            // for potentially having a different, valid signature) we return ok to allow
483            // the caller to stop submitting the value if they are in a retry loop.
484            if existing_announcement.api_announcement == announcement.api_announcement {
485                return Ok(());
486            }
487
488            // We only accept announcements with a nonce higher than the current one to
489            // avoid replay attacks.
490            if existing_announcement.api_announcement.nonce >= announcement.api_announcement.nonce {
491                return Err(ApiError::bad_request(
492                    "Outdated or redundant announcement".into(),
493                ));
494            }
495        }
496
497        dbtx.insert_entry(&ApiAnnouncementKey(peer_id), &announcement)
498            .await;
499        dbtx.commit_tx().await;
500        Ok(())
501    }
502
503    async fn sign_api_announcement(&self, new_url: SafeUrl) -> SignedApiAnnouncement {
504        self.db
505            .autocommit(
506                |dbtx, _| {
507                    let new_url_inner = new_url.clone();
508                    Box::pin(async move {
509                        let new_nonce = dbtx
510                            .get_value(&ApiAnnouncementKey(self.cfg.local.identity))
511                            .await
512                            .map_or(0, |a| a.api_announcement.nonce + 1);
513                        let announcement = ApiAnnouncement {
514                            api_url: new_url_inner,
515                            nonce: new_nonce,
516                        };
517                        let ctx = secp256k1::Secp256k1::new();
518                        let signed_announcement = announcement
519                            .sign(&ctx, &self.cfg.private.broadcast_secret_key.keypair(&ctx));
520
521                        dbtx.insert_entry(
522                            &ApiAnnouncementKey(self.cfg.local.identity),
523                            &signed_announcement,
524                        )
525                        .await;
526
527                        Result::<_, ()>::Ok(signed_announcement)
528                    })
529                },
530                None,
531            )
532            .await
533            .expect("Will not terminate on error")
534    }
535
536    /// Changes the guardian password by re-encrypting the private config and
537    /// changing the on-disk password file if present. `fedimintd` is shut down
538    /// afterward, the user's service manager (e.g. `systemd` is expected to
539    /// restart it).
540    fn change_guardian_password(
541        &self,
542        new_password: &str,
543        _auth: &GuardianAuthToken,
544    ) -> Result<(), ApiError> {
545        reencrypt_private_config(&self.cfg_dir, &self.cfg.private, new_password)
546            .map_err(|e| ApiError::server_error(format!("Failed to change password: {e}")))?;
547
548        info!(target: LOG_NET_API, "Successfully changed guardian password");
549
550        Ok(())
551    }
552}
553
554#[async_trait]
555impl HasApiContext<ConsensusApi> for ConsensusApi {
556    async fn context(
557        &self,
558        request: &ApiRequestErased,
559        id: Option<ModuleInstanceId>,
560    ) -> (&ConsensusApi, ApiEndpointContext<'_>) {
561        let mut db = self.db.clone();
562        let mut dbtx = self.db.begin_transaction().await;
563        if let Some(id) = id {
564            db = self.db.with_prefix_module_id(id).0;
565            dbtx = dbtx.with_prefix_module_id(id).0;
566        }
567        (
568            self,
569            ApiEndpointContext::new(
570                db,
571                dbtx,
572                request.auth == Some(self.cfg.private.api_auth.clone()),
573                request.auth.clone(),
574            ),
575        )
576    }
577}
578
579#[async_trait]
580impl HasApiContext<DynServerModule> for ConsensusApi {
581    async fn context(
582        &self,
583        request: &ApiRequestErased,
584        id: Option<ModuleInstanceId>,
585    ) -> (&DynServerModule, ApiEndpointContext<'_>) {
586        let (_, context): (&ConsensusApi, _) = self.context(request, id).await;
587        (
588            self.modules.get_expect(id.expect("required module id")),
589            context,
590        )
591    }
592}
593
594#[async_trait]
595impl IDashboardApi for ConsensusApi {
596    async fn auth(&self) -> ApiAuth {
597        self.cfg.private.api_auth.clone()
598    }
599
600    async fn guardian_id(&self) -> PeerId {
601        self.cfg.local.identity
602    }
603
604    async fn guardian_names(&self) -> BTreeMap<PeerId, String> {
605        self.cfg
606            .consensus
607            .api_endpoints()
608            .iter()
609            .map(|(peer_id, endpoint)| (*peer_id, endpoint.name.clone()))
610            .collect()
611    }
612
613    async fn federation_name(&self) -> String {
614        self.cfg
615            .consensus
616            .meta
617            .get(META_FEDERATION_NAME_KEY)
618            .cloned()
619            .expect("Federation name must be set")
620    }
621
622    async fn session_count(&self) -> u64 {
623        self.session_count().await
624    }
625
626    async fn get_session_status(&self, session_idx: u64) -> SessionStatusV2 {
627        self.session_status(session_idx).await
628    }
629
630    async fn consensus_ord_latency(&self) -> Option<Duration> {
631        *self.ord_latency_receiver.borrow()
632    }
633
634    async fn p2p_connection_status(&self) -> BTreeMap<PeerId, Option<Duration>> {
635        self.p2p_status_receivers
636            .iter()
637            .map(|(peer, receiver)| (*peer, *receiver.borrow()))
638            .collect()
639    }
640
641    async fn p2p_connection_type_status(&self) -> BTreeMap<PeerId, ConnectionType> {
642        self.p2p_connection_type_receivers
643            .iter()
644            .map(|(peer, receiver)| (*peer, *receiver.borrow()))
645            .collect()
646    }
647
648    async fn federation_invite_code(&self) -> String {
649        self.cfg
650            .get_invite_code(self.get_active_api_secret())
651            .to_string()
652    }
653
654    async fn federation_audit(&self) -> AuditSummary {
655        self.get_federation_audit()
656            .await
657            .expect("Failed to get federation audit")
658    }
659
660    async fn bitcoin_rpc_url(&self) -> SafeUrl {
661        self.bitcoin_rpc_connection.url()
662    }
663
664    async fn bitcoin_rpc_status(&self) -> Option<ServerBitcoinRpcStatus> {
665        self.bitcoin_rpc_connection.status()
666    }
667
668    async fn download_guardian_config_backup(
669        &self,
670        password: &str,
671        guardian_auth: &GuardianAuthToken,
672    ) -> GuardianConfigBackup {
673        self.get_guardian_config_backup(password, guardian_auth)
674    }
675
676    fn get_module_by_kind(&self, kind: ModuleKind) -> Option<&DynServerModule> {
677        self.modules
678            .iter_modules()
679            .find_map(|(_, module_kind, module)| {
680                if *module_kind == kind {
681                    Some(module)
682                } else {
683                    None
684                }
685            })
686    }
687
688    async fn fedimintd_version(&self) -> String {
689        self.code_version_str.clone()
690    }
691}
692
693pub fn server_endpoints() -> Vec<ApiEndpoint<ConsensusApi>> {
694    vec![
695        api_endpoint! {
696            VERSION_ENDPOINT,
697            ApiVersion::new(0, 0),
698            async |fedimint: &ConsensusApi, _context, _v: ()| -> SupportedApiVersionsSummary {
699                Ok(fedimint.api_versions_summary().to_owned())
700            }
701        },
702        api_endpoint! {
703            SUBMIT_TRANSACTION_ENDPOINT,
704            ApiVersion::new(0, 0),
705            async |fedimint: &ConsensusApi, _context, transaction: SerdeTransaction| -> SerdeModuleEncoding<TransactionSubmissionOutcome> {
706                let transaction = transaction
707                    .try_into_inner(&fedimint.modules.decoder_registry())
708                    .map_err(|e| ApiError::bad_request(e.to_string()))?;
709
710                // we return an inner error if and only if the submitted transaction is
711                // invalid and will be rejected if we were to submit it to consensus
712                Ok((&TransactionSubmissionOutcome(fedimint.submit_transaction(transaction).await)).into())
713            }
714        },
715        api_endpoint! {
716            AWAIT_TRANSACTION_ENDPOINT,
717            ApiVersion::new(0, 0),
718            async |fedimint: &ConsensusApi, _context, tx_hash: TransactionId| -> TransactionId {
719                fedimint.await_transaction(tx_hash).await;
720
721                Ok(tx_hash)
722            }
723        },
724        api_endpoint! {
725            AWAIT_OUTPUT_OUTCOME_ENDPOINT,
726            ApiVersion::new(0, 0),
727            async |fedimint: &ConsensusApi, _context, outpoint: OutPoint| -> SerdeModuleEncoding<DynOutputOutcome> {
728                let outcome = fedimint
729                    .await_output_outcome(outpoint)
730                    .await
731                    .map_err(|e| ApiError::bad_request(e.to_string()))?;
732
733                Ok(outcome)
734            }
735        },
736        api_endpoint! {
737            AWAIT_OUTPUTS_OUTCOMES_ENDPOINT,
738            ApiVersion::new(0, 8),
739            async |fedimint: &ConsensusApi, _context, outpoint_range: OutPointRange| -> Vec<Option<SerdeModuleEncoding<DynOutputOutcome>>> {
740                let outcomes = fedimint
741                    .await_outputs_outcomes(outpoint_range)
742                    .await
743                    .map_err(|e| ApiError::bad_request(e.to_string()))?;
744
745                Ok(outcomes)
746            }
747        },
748        api_endpoint! {
749            INVITE_CODE_ENDPOINT,
750            ApiVersion::new(0, 0),
751            async |fedimint: &ConsensusApi, _context,  _v: ()| -> String {
752                Ok(fedimint.cfg.get_invite_code(fedimint.get_active_api_secret()).to_string())
753            }
754        },
755        api_endpoint! {
756            FEDERATION_ID_ENDPOINT,
757            ApiVersion::new(0, 2),
758            async |fedimint: &ConsensusApi, _context,  _v: ()| -> String {
759                Ok(fedimint.cfg.calculate_federation_id().to_string())
760            }
761        },
762        api_endpoint! {
763            CLIENT_CONFIG_ENDPOINT,
764            ApiVersion::new(0, 0),
765            async |fedimint: &ConsensusApi, _context, _v: ()| -> ClientConfig {
766                Ok(fedimint.client_cfg.clone())
767            }
768        },
769        // Helper endpoint for Admin UI that can't parse consensus encoding
770        api_endpoint! {
771            CLIENT_CONFIG_JSON_ENDPOINT,
772            ApiVersion::new(0, 0),
773            async |fedimint: &ConsensusApi, _context, _v: ()| -> JsonClientConfig {
774                Ok(fedimint.client_cfg.to_json())
775            }
776        },
777        api_endpoint! {
778            SERVER_CONFIG_CONSENSUS_HASH_ENDPOINT,
779            ApiVersion::new(0, 0),
780            async |fedimint: &ConsensusApi, _context, _v: ()| -> sha256::Hash {
781                Ok(legacy_consensus_config_hash(&fedimint.cfg.consensus))
782            }
783        },
784        api_endpoint! {
785            STATUS_ENDPOINT,
786            ApiVersion::new(0, 0),
787            async |fedimint: &ConsensusApi, _context, _v: ()| -> StatusResponse {
788                Ok(StatusResponse {
789                    server: ServerStatusLegacy::ConsensusRunning,
790                    federation: Some(fedimint.get_federation_status().await?)
791                })}
792        },
793        api_endpoint! {
794            SETUP_STATUS_ENDPOINT,
795            ApiVersion::new(0, 0),
796            async |_f: &ConsensusApi, _c, _v: ()| -> SetupStatus {
797                Ok(SetupStatus::ConsensusIsRunning)
798            }
799        },
800        api_endpoint! {
801            CONSENSUS_ORD_LATENCY_ENDPOINT,
802            ApiVersion::new(0, 0),
803            async |fedimint: &ConsensusApi, _c, _v: ()| -> Option<Duration> {
804                Ok(*fedimint.ord_latency_receiver.borrow())
805            }
806        },
807        api_endpoint! {
808            P2P_CONNECTION_STATUS_ENDPOINT,
809            ApiVersion::new(0, 0),
810            async |fedimint: &ConsensusApi, _c, _v: ()| -> BTreeMap<PeerId, Option<Duration>> {
811                Ok(fedimint.p2p_status_receivers
812                    .iter()
813                    .map(|(peer, receiver)| (*peer, *receiver.borrow()))
814                    .collect())
815            }
816        },
817        api_endpoint! {
818            P2P_CONNECTION_TYPE_ENDPOINT,
819            ApiVersion::new(0, 7),
820            async |fedimint: &ConsensusApi, _c, _v: ()| -> BTreeMap<PeerId, ConnectionType> {
821                Ok(fedimint.p2p_connection_type_receivers
822                    .iter()
823                    .map(|(peer, receiver)| (*peer, *receiver.borrow()))
824                    .collect())
825            }
826        },
827        api_endpoint! {
828            SESSION_COUNT_ENDPOINT,
829            ApiVersion::new(0, 0),
830            async |fedimint: &ConsensusApi, _context, _v: ()| -> u64 {
831                Ok(fedimint.session_count().await)
832            }
833        },
834        api_endpoint! {
835            AWAIT_SESSION_OUTCOME_ENDPOINT,
836            ApiVersion::new(0, 0),
837            async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SessionOutcome> {
838                Ok((&fedimint.await_signed_session_outcome(index).await.session_outcome).into())
839            }
840        },
841        api_endpoint! {
842            AWAIT_SIGNED_SESSION_OUTCOME_ENDPOINT,
843            ApiVersion::new(0, 0),
844            async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SignedSessionOutcome> {
845                Ok((&fedimint.await_signed_session_outcome(index).await).into())
846            }
847        },
848        api_endpoint! {
849            SESSION_STATUS_ENDPOINT,
850            ApiVersion::new(0, 1),
851            async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SessionStatus> {
852                Ok((&SessionStatus::from(fedimint.session_status(index).await)).into())
853            }
854        },
855        api_endpoint! {
856            SESSION_STATUS_V2_ENDPOINT,
857            ApiVersion::new(0, 5),
858            async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncodingBase64<SessionStatusV2> {
859                Ok((&fedimint.session_status(index).await).into())
860            }
861        },
862        api_endpoint! {
863            SHUTDOWN_ENDPOINT,
864            ApiVersion::new(0, 3),
865            async |fedimint: &ConsensusApi, context, index: Option<u64>| -> () {
866                check_auth(context)?;
867                fedimint.shutdown(index);
868                Ok(())
869            }
870        },
871        api_endpoint! {
872            AUDIT_ENDPOINT,
873            ApiVersion::new(0, 0),
874            async |fedimint: &ConsensusApi, context, _v: ()| -> AuditSummary {
875                check_auth(context)?;
876                Ok(fedimint.get_federation_audit().await?)
877            }
878        },
879        api_endpoint! {
880            GUARDIAN_CONFIG_BACKUP_ENDPOINT,
881            ApiVersion::new(0, 2),
882            async |fedimint: &ConsensusApi, context, _v: ()| -> GuardianConfigBackup {
883                let auth = check_auth(context)?;
884                let password = context.request_auth().expect("Auth was checked before").0;
885                Ok(fedimint.get_guardian_config_backup(&password, &auth))
886            }
887        },
888        api_endpoint! {
889            BACKUP_ENDPOINT,
890            ApiVersion::new(0, 0),
891            async |fedimint: &ConsensusApi, context, request: SignedBackupRequest| -> () {
892                fedimint
893                    .handle_backup_request(&mut context.dbtx().into_nc(), request).await?;
894                Ok(())
895
896            }
897        },
898        api_endpoint! {
899            RECOVER_ENDPOINT,
900            ApiVersion::new(0, 0),
901            async |fedimint: &ConsensusApi, context, id: PublicKey| -> Option<ClientBackupSnapshot> {
902                Ok(fedimint
903                    .handle_recover_request(&mut context.dbtx().into_nc(), id).await)
904            }
905        },
906        api_endpoint! {
907            AUTH_ENDPOINT,
908            ApiVersion::new(0, 0),
909            async |_fedimint: &ConsensusApi, context, _v: ()| -> () {
910                check_auth(context)?;
911                Ok(())
912            }
913        },
914        api_endpoint! {
915            API_ANNOUNCEMENTS_ENDPOINT,
916            ApiVersion::new(0, 3),
917            async |fedimint: &ConsensusApi, _context, _v: ()| -> BTreeMap<PeerId, SignedApiAnnouncement> {
918                Ok(fedimint.api_announcements().await)
919            }
920        },
921        api_endpoint! {
922            SUBMIT_API_ANNOUNCEMENT_ENDPOINT,
923            ApiVersion::new(0, 3),
924            async |fedimint: &ConsensusApi, _context, submission: SignedApiAnnouncementSubmission| -> () {
925                fedimint.submit_api_announcement(submission.peer_id, submission.signed_api_announcement).await
926            }
927        },
928        api_endpoint! {
929            SIGN_API_ANNOUNCEMENT_ENDPOINT,
930            ApiVersion::new(0, 3),
931            async |fedimint: &ConsensusApi, context, new_url: SafeUrl| -> SignedApiAnnouncement {
932                check_auth(context)?;
933                Ok(fedimint.sign_api_announcement(new_url).await)
934            }
935        },
936        api_endpoint! {
937            FEDIMINTD_VERSION_ENDPOINT,
938            ApiVersion::new(0, 4),
939            async |fedimint: &ConsensusApi, _context, _v: ()| -> String {
940                Ok(fedimint.fedimintd_version())
941            }
942        },
943        api_endpoint! {
944            BACKUP_STATISTICS_ENDPOINT,
945            ApiVersion::new(0, 5),
946            async |_fedimint: &ConsensusApi, context, _v: ()| -> BackupStatistics {
947                check_auth(context)?;
948                Ok(backup_statistics_static(&mut context.dbtx().into_nc()).await)
949            }
950        },
951        api_endpoint! {
952            CHANGE_PASSWORD_ENDPOINT,
953            ApiVersion::new(0, 6),
954            async |fedimint: &ConsensusApi, context, new_password: String| -> () {
955                let auth = check_auth(context)?;
956                fedimint.change_guardian_password(&new_password, &auth)?;
957                let task_group = fedimint.task_group.clone();
958                fedimint_core::runtime::spawn("shutdown after password change",  async move {
959                    info!(target: LOG_NET_API, "Will shutdown after password change");
960                    fedimint_core:: runtime::sleep(Duration::from_secs(1)).await;
961                    task_group.shutdown();
962                });
963                Ok(())
964            }
965        },
966    ]
967}
968
969pub(crate) async fn backup_statistics_static(
970    dbtx: &mut DatabaseTransaction<'_>,
971) -> BackupStatistics {
972    const DAY_SECS: u64 = 24 * 60 * 60;
973    const WEEK_SECS: u64 = 7 * DAY_SECS;
974    const MONTH_SECS: u64 = 30 * DAY_SECS;
975    const QUARTER_SECS: u64 = 3 * MONTH_SECS;
976
977    let mut backup_stats = BackupStatistics::default();
978
979    let mut all_backups_stream = dbtx.find_by_prefix(&ClientBackupKeyPrefix).await;
980    while let Some((_, backup)) = all_backups_stream.next().await {
981        backup_stats.num_backups += 1;
982        backup_stats.total_size += backup.data.len();
983
984        let age_secs = backup.timestamp.elapsed().unwrap_or_default().as_secs();
985        if age_secs < DAY_SECS {
986            backup_stats.refreshed_1d += 1;
987        }
988        if age_secs < WEEK_SECS {
989            backup_stats.refreshed_1w += 1;
990        }
991        if age_secs < MONTH_SECS {
992            backup_stats.refreshed_1m += 1;
993        }
994        if age_secs < QUARTER_SECS {
995            backup_stats.refreshed_3m += 1;
996        }
997    }
998
999    backup_stats
1000}