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