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