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.auth == Some(self.cfg.private.api_auth.clone()),
685                request.auth.clone(),
686            ),
687        )
688    }
689}
690
691#[async_trait]
692impl HasApiContext<DynServerModule> for ConsensusApi {
693    async fn context(
694        &self,
695        request: &ApiRequestErased,
696        id: Option<ModuleInstanceId>,
697    ) -> (&DynServerModule, ApiEndpointContext) {
698        let (_, context): (&ConsensusApi, _) = self.context(request, id).await;
699        (
700            self.modules.get_expect(id.expect("required module id")),
701            context,
702        )
703    }
704}
705
706#[async_trait]
707impl IDashboardApi for ConsensusApi {
708    async fn auth(&self) -> ApiAuth {
709        self.cfg.private.api_auth.clone()
710    }
711
712    async fn guardian_id(&self) -> PeerId {
713        self.cfg.local.identity
714    }
715
716    async fn guardian_names(&self) -> BTreeMap<PeerId, String> {
717        self.cfg
718            .consensus
719            .api_endpoints()
720            .iter()
721            .map(|(peer_id, endpoint)| (*peer_id, endpoint.name.clone()))
722            .collect()
723    }
724
725    async fn federation_name(&self) -> String {
726        self.cfg
727            .consensus
728            .meta
729            .get(META_FEDERATION_NAME_KEY)
730            .cloned()
731            .expect("Federation name must be set")
732    }
733
734    async fn session_count(&self) -> u64 {
735        self.session_count().await
736    }
737
738    async fn get_session_status(&self, session_idx: u64) -> SessionStatusV2 {
739        self.session_status(session_idx).await
740    }
741
742    async fn consensus_ord_latency(&self) -> Option<Duration> {
743        *self.ord_latency_receiver.borrow()
744    }
745
746    async fn p2p_connection_status(&self) -> BTreeMap<PeerId, Option<P2PConnectionStatus>> {
747        self.p2p_status_receivers
748            .iter()
749            .map(|(peer, receiver)| (*peer, receiver.borrow().clone()))
750            .collect()
751    }
752
753    async fn federation_invite_code(&self) -> String {
754        self.cfg
755            .get_invite_code(self.get_active_api_secret())
756            .to_string()
757    }
758
759    async fn federation_audit(&self) -> AuditSummary {
760        self.get_federation_audit()
761            .await
762            .expect("Failed to get federation audit")
763    }
764
765    async fn bitcoin_rpc_url(&self) -> SafeUrl {
766        self.bitcoin_rpc_connection.url()
767    }
768
769    async fn bitcoin_rpc_status(&self) -> Option<ServerBitcoinRpcStatus> {
770        self.bitcoin_rpc_connection.status()
771    }
772
773    async fn download_guardian_config_backup(
774        &self,
775        password: &str,
776        guardian_auth: &GuardianAuthToken,
777    ) -> GuardianConfigBackup {
778        self.get_guardian_config_backup(password, guardian_auth)
779    }
780
781    fn get_module_by_kind(&self, kind: ModuleKind) -> Option<&DynServerModule> {
782        self.modules
783            .iter_modules()
784            .find_map(|(_, module_kind, module)| {
785                if *module_kind == kind {
786                    Some(module)
787                } else {
788                    None
789                }
790            })
791    }
792
793    async fn fedimintd_version(&self) -> String {
794        self.code_version_str.clone()
795    }
796
797    async fn change_password(
798        &self,
799        new_password: &str,
800        current_password: &str,
801        guardian_auth: &GuardianAuthToken,
802    ) -> Result<(), String> {
803        let auth = &self.auth().await.0;
804        if auth != current_password {
805            return Err("Current password is incorrect".into());
806        }
807        self.change_guardian_password(new_password, guardian_auth)
808            .map_err(|e| e.to_string())
809    }
810}
811
812pub fn server_endpoints() -> Vec<ApiEndpoint<ConsensusApi>> {
813    vec![
814        api_endpoint! {
815            VERSION_ENDPOINT,
816            ApiVersion::new(0, 0),
817            async |fedimint: &ConsensusApi, _context, _v: ()| -> SupportedApiVersionsSummary {
818                Ok(fedimint.api_versions_summary().to_owned())
819            }
820        },
821        api_endpoint! {
822            SUBMIT_TRANSACTION_ENDPOINT,
823            ApiVersion::new(0, 0),
824            async |fedimint: &ConsensusApi, _context, transaction: SerdeTransaction| -> SerdeModuleEncoding<TransactionSubmissionOutcome> {
825                let transaction = transaction
826                    .try_into_inner(&fedimint.modules.decoder_registry())
827                    .map_err(|e| ApiError::bad_request(e.to_string()))?;
828
829                // we return an inner error if and only if the submitted transaction is
830                // invalid and will be rejected if we were to submit it to consensus
831                Ok((&TransactionSubmissionOutcome(fedimint.submit_transaction(transaction).await)).into())
832            }
833        },
834        api_endpoint! {
835            AWAIT_TRANSACTION_ENDPOINT,
836            ApiVersion::new(0, 0),
837            async |fedimint: &ConsensusApi, _context, tx_hash: TransactionId| -> TransactionId {
838                fedimint.await_transaction(tx_hash).await;
839
840                Ok(tx_hash)
841            }
842        },
843        api_endpoint! {
844            AWAIT_OUTPUT_OUTCOME_ENDPOINT,
845            ApiVersion::new(0, 0),
846            async |fedimint: &ConsensusApi, _context, outpoint: OutPoint| -> SerdeModuleEncoding<DynOutputOutcome> {
847                let outcome = fedimint
848                    .await_output_outcome(outpoint)
849                    .await
850                    .map_err(|e| ApiError::bad_request(e.to_string()))?;
851
852                Ok(outcome)
853            }
854        },
855        api_endpoint! {
856            AWAIT_OUTPUTS_OUTCOMES_ENDPOINT,
857            ApiVersion::new(0, 8),
858            async |fedimint: &ConsensusApi, _context, outpoint_range: OutPointRange| -> Vec<Option<SerdeModuleEncoding<DynOutputOutcome>>> {
859                let outcomes = fedimint
860                    .await_outputs_outcomes(outpoint_range)
861                    .await
862                    .map_err(|e| ApiError::bad_request(e.to_string()))?;
863
864                Ok(outcomes)
865            }
866        },
867        api_endpoint! {
868            INVITE_CODE_ENDPOINT,
869            ApiVersion::new(0, 0),
870            async |fedimint: &ConsensusApi, _context,  _v: ()| -> String {
871                Ok(fedimint.cfg.get_invite_code(fedimint.get_active_api_secret()).to_string())
872            }
873        },
874        api_endpoint! {
875            FEDERATION_ID_ENDPOINT,
876            ApiVersion::new(0, 2),
877            async |fedimint: &ConsensusApi, _context,  _v: ()| -> String {
878                Ok(fedimint.cfg.calculate_federation_id().to_string())
879            }
880        },
881        api_endpoint! {
882            CLIENT_CONFIG_ENDPOINT,
883            ApiVersion::new(0, 0),
884            async |fedimint: &ConsensusApi, _context, _v: ()| -> ClientConfig {
885                Ok(fedimint.client_cfg.clone())
886            }
887        },
888        // Helper endpoint for Admin UI that can't parse consensus encoding
889        api_endpoint! {
890            CLIENT_CONFIG_JSON_ENDPOINT,
891            ApiVersion::new(0, 0),
892            async |fedimint: &ConsensusApi, _context, _v: ()| -> JsonClientConfig {
893                Ok(fedimint.client_cfg.to_json())
894            }
895        },
896        api_endpoint! {
897            SERVER_CONFIG_CONSENSUS_HASH_ENDPOINT,
898            ApiVersion::new(0, 0),
899            async |fedimint: &ConsensusApi, _context, _v: ()| -> sha256::Hash {
900                Ok(legacy_consensus_config_hash(&fedimint.cfg.consensus))
901            }
902        },
903        api_endpoint! {
904            STATUS_ENDPOINT,
905            ApiVersion::new(0, 0),
906            async |fedimint: &ConsensusApi, _context, _v: ()| -> StatusResponse {
907                Ok(StatusResponse {
908                    server: ServerStatusLegacy::ConsensusRunning,
909                    federation: Some(fedimint.get_federation_status().await?)
910                })}
911        },
912        api_endpoint! {
913            SETUP_STATUS_ENDPOINT,
914            ApiVersion::new(0, 0),
915            async |_f: &ConsensusApi, _c, _v: ()| -> SetupStatus {
916                Ok(SetupStatus::ConsensusIsRunning)
917            }
918        },
919        api_endpoint! {
920            CONSENSUS_ORD_LATENCY_ENDPOINT,
921            ApiVersion::new(0, 0),
922            async |fedimint: &ConsensusApi, _c, _v: ()| -> Option<Duration> {
923                Ok(*fedimint.ord_latency_receiver.borrow())
924            }
925        },
926        api_endpoint! {
927            P2P_CONNECTION_STATUS_ENDPOINT,
928            ApiVersion::new(0, 0),
929            async |fedimint: &ConsensusApi, _c, _v: ()| -> BTreeMap<PeerId, Option<P2PConnectionStatus>> {
930                Ok(fedimint.p2p_status_receivers
931                    .iter()
932                    .map(|(peer, receiver)| (*peer, receiver.borrow().clone()))
933                    .collect())
934            }
935        },
936        api_endpoint! {
937            SESSION_COUNT_ENDPOINT,
938            ApiVersion::new(0, 0),
939            async |fedimint: &ConsensusApi, _context, _v: ()| -> u64 {
940                Ok(fedimint.session_count().await)
941            }
942        },
943        api_endpoint! {
944            AWAIT_SESSION_OUTCOME_ENDPOINT,
945            ApiVersion::new(0, 0),
946            async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SessionOutcome> {
947                Ok((&fedimint.await_signed_session_outcome(index).await.session_outcome).into())
948            }
949        },
950        api_endpoint! {
951            AWAIT_SIGNED_SESSION_OUTCOME_ENDPOINT,
952            ApiVersion::new(0, 0),
953            async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SignedSessionOutcome> {
954                Ok((&fedimint.await_signed_session_outcome(index).await).into())
955            }
956        },
957        api_endpoint! {
958            SESSION_STATUS_ENDPOINT,
959            ApiVersion::new(0, 1),
960            async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SessionStatus> {
961                Ok((&SessionStatus::from(fedimint.session_status(index).await)).into())
962            }
963        },
964        api_endpoint! {
965            SESSION_STATUS_V2_ENDPOINT,
966            ApiVersion::new(0, 5),
967            async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncodingBase64<SessionStatusV2> {
968                Ok((&fedimint.session_status(index).await).into())
969            }
970        },
971        api_endpoint! {
972            SHUTDOWN_ENDPOINT,
973            ApiVersion::new(0, 3),
974            async |fedimint: &ConsensusApi, context, index: Option<u64>| -> () {
975                check_auth(context)?;
976                fedimint.shutdown(index);
977                Ok(())
978            }
979        },
980        api_endpoint! {
981            AUDIT_ENDPOINT,
982            ApiVersion::new(0, 0),
983            async |fedimint: &ConsensusApi, context, _v: ()| -> AuditSummary {
984                check_auth(context)?;
985                Ok(fedimint.get_federation_audit().await?)
986            }
987        },
988        api_endpoint! {
989            GUARDIAN_CONFIG_BACKUP_ENDPOINT,
990            ApiVersion::new(0, 2),
991            async |fedimint: &ConsensusApi, context, _v: ()| -> GuardianConfigBackup {
992                let auth = check_auth(context)?;
993                let password = context.request_auth().expect("Auth was checked before").0;
994                Ok(fedimint.get_guardian_config_backup(&password, &auth))
995            }
996        },
997        api_endpoint! {
998            BACKUP_ENDPOINT,
999            ApiVersion::new(0, 0),
1000            async |fedimint: &ConsensusApi, context, request: SignedBackupRequest| -> () {
1001                let db = context.db();
1002                let mut dbtx = db.begin_transaction().await;
1003                fedimint
1004                    .handle_backup_request(&mut dbtx.to_ref_nc(), request).await?;
1005                dbtx.commit_tx_result().await?;
1006                Ok(())
1007
1008            }
1009        },
1010        api_endpoint! {
1011            RECOVER_ENDPOINT,
1012            ApiVersion::new(0, 0),
1013            async |fedimint: &ConsensusApi, context, id: PublicKey| -> Option<ClientBackupSnapshot> {
1014                let db = context.db();
1015                let mut dbtx = db.begin_transaction_nc().await;
1016                Ok(fedimint
1017                    .handle_recover_request(&mut dbtx, id).await)
1018            }
1019        },
1020        api_endpoint! {
1021            AUTH_ENDPOINT,
1022            ApiVersion::new(0, 0),
1023            async |_fedimint: &ConsensusApi, context, _v: ()| -> () {
1024                check_auth(context)?;
1025                Ok(())
1026            }
1027        },
1028        api_endpoint! {
1029            API_ANNOUNCEMENTS_ENDPOINT,
1030            ApiVersion::new(0, 3),
1031            async |fedimint: &ConsensusApi, _context, _v: ()| -> BTreeMap<PeerId, SignedApiAnnouncement> {
1032                Ok(fedimint.api_announcements().await)
1033            }
1034        },
1035        api_endpoint! {
1036            SUBMIT_API_ANNOUNCEMENT_ENDPOINT,
1037            ApiVersion::new(0, 3),
1038            async |fedimint: &ConsensusApi, _context, submission: SignedApiAnnouncementSubmission| -> () {
1039                fedimint.submit_api_announcement(submission.peer_id, submission.signed_api_announcement).await
1040            }
1041        },
1042        api_endpoint! {
1043            SIGN_API_ANNOUNCEMENT_ENDPOINT,
1044            ApiVersion::new(0, 3),
1045            async |fedimint: &ConsensusApi, context, new_url: SafeUrl| -> SignedApiAnnouncement {
1046                check_auth(context)?;
1047                Ok(fedimint.sign_api_announcement(new_url).await)
1048            }
1049        },
1050        api_endpoint! {
1051            GUARDIAN_METADATA_ENDPOINT,
1052            ApiVersion::new(0, 9),
1053            async |fedimint: &ConsensusApi, _context, _v: ()| -> BTreeMap<PeerId, fedimint_core::net::guardian_metadata::SignedGuardianMetadata> {
1054                Ok(fedimint.guardian_metadata_list().await)
1055            }
1056        },
1057        api_endpoint! {
1058            SUBMIT_GUARDIAN_METADATA_ENDPOINT,
1059            ApiVersion::new(0, 9),
1060            async |fedimint: &ConsensusApi, _context, submission: fedimint_core::net::guardian_metadata::SignedGuardianMetadataSubmission| -> () {
1061                fedimint.submit_guardian_metadata(submission.peer_id, submission.signed_guardian_metadata).await
1062            }
1063        },
1064        api_endpoint! {
1065            SIGN_GUARDIAN_METADATA_ENDPOINT,
1066            ApiVersion::new(0, 9),
1067            async |fedimint: &ConsensusApi, context, metadata: fedimint_core::net::guardian_metadata::GuardianMetadata| -> fedimint_core::net::guardian_metadata::SignedGuardianMetadata {
1068                check_auth(context)?;
1069                Ok(fedimint.sign_guardian_metadata(metadata).await)
1070            }
1071        },
1072        api_endpoint! {
1073            FEDIMINTD_VERSION_ENDPOINT,
1074            ApiVersion::new(0, 4),
1075            async |fedimint: &ConsensusApi, _context, _v: ()| -> String {
1076                Ok(fedimint.fedimintd_version())
1077            }
1078        },
1079        api_endpoint! {
1080            BACKUP_STATISTICS_ENDPOINT,
1081            ApiVersion::new(0, 5),
1082            async |_fedimint: &ConsensusApi, context, _v: ()| -> BackupStatistics {
1083                check_auth(context)?;
1084                let db = context.db();
1085                let mut dbtx = db.begin_transaction_nc().await;
1086                Ok(backup_statistics_static(&mut dbtx).await)
1087            }
1088        },
1089        api_endpoint! {
1090            CHANGE_PASSWORD_ENDPOINT,
1091            ApiVersion::new(0, 6),
1092            async |fedimint: &ConsensusApi, context, new_password: String| -> () {
1093                let auth = check_auth(context)?;
1094                fedimint.change_guardian_password(&new_password, &auth)?;
1095                let task_group = fedimint.task_group.clone();
1096                fedimint_core::runtime::spawn("shutdown after password change",  async move {
1097                    info!(target: LOG_NET_API, "Will shutdown after password change");
1098                    fedimint_core:: runtime::sleep(Duration::from_secs(1)).await;
1099                    task_group.shutdown();
1100                });
1101                Ok(())
1102            }
1103        },
1104        api_endpoint! {
1105            CHAIN_ID_ENDPOINT,
1106            ApiVersion::new(0, 9),
1107            async |fedimint: &ConsensusApi, _context, _v: ()| -> ChainId {
1108                fedimint
1109                    .bitcoin_rpc_connection
1110                    .get_chain_id()
1111                    .await
1112                    .map_err(|e| ApiError::server_error(e.to_string()))
1113            }
1114        },
1115    ]
1116}
1117
1118pub(crate) async fn backup_statistics_static(
1119    dbtx: &mut DatabaseTransaction<'_>,
1120) -> BackupStatistics {
1121    const DAY_SECS: u64 = 24 * 60 * 60;
1122    const WEEK_SECS: u64 = 7 * DAY_SECS;
1123    const MONTH_SECS: u64 = 30 * DAY_SECS;
1124    const QUARTER_SECS: u64 = 3 * MONTH_SECS;
1125
1126    let mut backup_stats = BackupStatistics::default();
1127
1128    let mut all_backups_stream = dbtx.find_by_prefix(&ClientBackupKeyPrefix).await;
1129    while let Some((_, backup)) = all_backups_stream.next().await {
1130        backup_stats.num_backups += 1;
1131        backup_stats.total_size += backup.data.len();
1132
1133        let age_secs = backup.timestamp.elapsed().unwrap_or_default().as_secs();
1134        if age_secs < DAY_SECS {
1135            backup_stats.refreshed_1d += 1;
1136        }
1137        if age_secs < WEEK_SECS {
1138            backup_stats.refreshed_1w += 1;
1139        }
1140        if age_secs < MONTH_SECS {
1141            backup_stats.refreshed_1m += 1;
1142        }
1143        if age_secs < QUARTER_SECS {
1144            backup_stats.refreshed_3m += 1;
1145        }
1146    }
1147
1148    backup_stats
1149}