Skip to main content

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