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