Skip to main content

fedimint_server/consensus/
api.rs

1//! Implements the client API through which users interact with the federation
2use std::cmp::Ordering;
3use std::collections::BTreeMap;
4use std::future::pending;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7use std::time::{Duration, SystemTime};
8
9use anyhow::{Context, Result, ensure};
10use async_trait::async_trait;
11use bitcoin::hashes::sha256;
12use fedimint_api_client::api::{
13    LegacyFederationStatus, LegacyP2PConnectionStatus, LegacyPeerStatus, StatusResponse,
14};
15use fedimint_core::admin_client::{GuardianConfigBackup, ServerStatusLegacy, SetupStatus};
16use fedimint_core::backup::{
17    BackupStatistics, ClientBackupKey, ClientBackupKeyPrefix, ClientBackupSnapshot,
18};
19use fedimint_core::config::{ClientConfig, JsonClientConfig, META_FEDERATION_NAME_KEY};
20use fedimint_core::core::backup::{BACKUP_REQUEST_MAX_PAYLOAD_SIZE_BYTES, SignedBackupRequest};
21use fedimint_core::core::{DynOutputOutcome, ModuleInstanceId, ModuleKind};
22use fedimint_core::db::{
23    Committable, Database, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped,
24};
25#[allow(deprecated)]
26use fedimint_core::endpoint_constants::AWAIT_OUTPUT_OUTCOME_ENDPOINT;
27use fedimint_core::endpoint_constants::{
28    API_ANNOUNCEMENTS_ENDPOINT, AUDIT_ENDPOINT, AUTH_ENDPOINT, AWAIT_OUTPUTS_OUTCOMES_ENDPOINT,
29    AWAIT_SESSION_OUTCOME_ENDPOINT, AWAIT_SIGNED_SESSION_OUTCOME_ENDPOINT,
30    AWAIT_TRANSACTION_ENDPOINT, BACKUP_ENDPOINT, BACKUP_STATISTICS_ENDPOINT, CHAIN_ID_ENDPOINT,
31    CLIENT_CONFIG_ENDPOINT, CLIENT_CONFIG_JSON_ENDPOINT, CONSENSUS_ORD_LATENCY_ENDPOINT,
32    FEDERATION_ID_ENDPOINT, FEDIMINTD_VERSION_ENDPOINT, GUARDIAN_CONFIG_BACKUP_ENDPOINT,
33    GUARDIAN_METADATA_ENDPOINT, INVITE_CODE_ENDPOINT, P2P_CONNECTION_STATUS_ENDPOINT,
34    PEER_AVAILABILITY_ENDPOINT, RECOVER_ENDPOINT, SERVER_CONFIG_CONSENSUS_HASH_ENDPOINT,
35    SESSION_COUNT_ENDPOINT, SESSION_STATUS_ENDPOINT, SESSION_STATUS_V2_ENDPOINT,
36    SETUP_STATUS_ENDPOINT, SHUTDOWN_AFTER_CURRENT_SESSION_ENDPOINT, SHUTDOWN_ENDPOINT,
37    SIGN_API_ANNOUNCEMENT_ENDPOINT, SIGN_GUARDIAN_METADATA_ENDPOINT, STATUS_ENDPOINT,
38    SUBMIT_API_ANNOUNCEMENT_ENDPOINT, SUBMIT_GUARDIAN_METADATA_ENDPOINT,
39    SUBMIT_TRANSACTION_ENDPOINT, VERSION_ENDPOINT,
40};
41use fedimint_core::invite_code::InviteCode;
42use fedimint_core::module::audit::{Audit, AuditSummary};
43use fedimint_core::module::{
44    ApiAuth, ApiEndpoint, ApiEndpointContext, ApiError, ApiRequestErased, ApiResult, ApiVersion,
45    SerdeModuleEncoding, SerdeModuleEncodingBase64, SupportedApiVersionsSummary,
46    admin_api_endpoint, public_api_endpoint,
47};
48use fedimint_core::net::api_announcement::{
49    ApiAnnouncement, SignedApiAnnouncement, SignedApiAnnouncementSubmission,
50};
51use fedimint_core::net::auth::GuardianAuthToken;
52use fedimint_core::peer_availability::PeerAvailability;
53use fedimint_core::secp256k1::{PublicKey, SECP256K1};
54use fedimint_core::session_outcome::{ConsensusItem, SessionStatus, SessionStatusV2};
55use fedimint_core::task::TaskGroup;
56use fedimint_core::transaction::{
57    SerdeTransaction, Transaction, TransactionError, TransactionSubmissionOutcome,
58};
59use fedimint_core::util::{FmtCompact, SafeUrl};
60use fedimint_core::version::non_zero_version_hash;
61use fedimint_core::{ChainId, OutPoint, OutPointRange, PeerId, TransactionId, secp256k1};
62use fedimint_logging::LOG_NET_API;
63use fedimint_server_core::bitcoin_rpc::ServerBitcoinRpcMonitor;
64use fedimint_server_core::dashboard_ui::{
65    IDashboardApi, P2PConnectionStatus, ServerBitcoinRpcStatus,
66};
67use fedimint_server_core::{DynServerModule, ServerModuleRegistry, ServerModuleRegistryExt};
68use futures::StreamExt;
69use tokio::sync::watch::{self, Receiver, Sender};
70use tracing::{debug, info, warn};
71
72use crate::config::io::{CONSENSUS_CONFIG, JSON_EXT, LOCAL_CONFIG, PRIVATE_CONFIG};
73use crate::config::{ServerConfig, legacy_consensus_config_hash};
74use crate::consensus::db::{
75    AcceptedItemPrefix, AcceptedTransactionKey, PeerAvailabilityKey, SignedSessionOutcomeKey,
76};
77use crate::consensus::engine::{
78    ScheduledShutdown, ShutdownRequest, get_finished_session_count_static,
79};
80use crate::consensus::session_cache::OutcomeFormat;
81use crate::consensus::transaction::{TxProcessingMode, process_transaction_with_dbtx};
82use crate::metrics::{BACKUP_WRITE_SIZE_BYTES, STORED_BACKUPS_COUNT};
83use crate::net::api::HasApiContext;
84use crate::net::api::announcement::{ApiAnnouncementKey, ApiAnnouncementPrefix, get_api_urls};
85use crate::net::p2p::{P2PConnectionState, P2PStatusReceivers};
86
87/// Maximum number of output outcomes a single `await_outputs_outcomes` request
88/// may ask for. The endpoint is public and unauthenticated and the requested
89/// range is used verbatim to size a `Vec`, so without a cap one request can
90/// trigger a terabyte-scale allocation and abort the guardian process.
91///
92/// Real transactions have a handful of outputs; a client that somehow needs
93/// more can still ask for them one outpoint at a time.
94const MAX_OUTPUTS_OUTCOMES_BATCH: usize = 1024;
95
96/// Maximum clock lead accepted for a signed client backup request.
97///
98/// This matches the one-hour limit used for signed guardian metadata
99/// timestamps.
100const MAX_BACKUP_FUTURE_TIMESTAMP_SECS: u64 = 60 * 60;
101
102fn backup_timestamp_is_too_far_in_future(timestamp: SystemTime, now: SystemTime) -> bool {
103    timestamp
104        .duration_since(now)
105        .is_ok_and(|lead| lead > Duration::from_secs(MAX_BACKUP_FUTURE_TIMESTAMP_SECS))
106}
107
108/// Number of output outcomes `outpoint_range` asks for, if it is a range we are
109/// willing to serve.
110///
111/// The range is deserialized verbatim from an unauthenticated request, so it
112/// has to be bounded before anything is sized from it.
113fn checked_outputs_outcomes_count(outpoint_range: OutPointRange) -> Result<usize> {
114    let count = outpoint_range
115        .checked_count()
116        .context("Outpoint range is descending or too large")?;
117
118    ensure!(
119        count <= MAX_OUTPUTS_OUTCOMES_BATCH,
120        "Outpoint range must cover at most {MAX_OUTPUTS_OUTCOMES_BATCH} outputs, got {count}"
121    );
122
123    Ok(count)
124}
125
126/// Status of a single peer, read from the p2p and consensus-item watch
127/// channels it is tracked by.
128///
129/// Each channel is read exactly once, into a local. `watch::Receiver::borrow`
130/// returns a guard over a `parking_lot::RwLock`, and Rust drops temporaries at
131/// the end of the enclosing *statement* rather than the sub-expression, so
132/// borrowing inline in a struct literal would keep several guards alive at
133/// once. That deadlocks the whole process: `parking_lot`'s `RwLock` is
134/// write-preferring, so a second `borrow()` of a channel this thread already
135/// holds a guard on blocks behind any writer that queued in between, and that
136/// writer can never drain because the first guard is still held. These are
137/// blocking waits, so every stuck request burns a tokio worker until the
138/// runtime has none left to poll the reactor with.
139fn legacy_peer_status(
140    p2p_receiver: &watch::Receiver<P2PConnectionState>,
141    ci_receiver: &watch::Receiver<Option<u64>>,
142    session_count: u64,
143) -> LegacyPeerStatus {
144    let p2p_connected = p2p_receiver.borrow().connected.is_some();
145    let last_contribution = *ci_receiver.borrow();
146
147    LegacyPeerStatus {
148        connection_status: if p2p_connected {
149            LegacyP2PConnectionStatus::Connected
150        } else {
151            LegacyP2PConnectionStatus::Disconnected
152        },
153        last_contribution,
154        flagged: last_contribution.unwrap_or(0) + 1 < session_count,
155    }
156}
157
158#[derive(Clone)]
159pub struct ConsensusApi {
160    /// Our server configuration
161    pub cfg: ServerConfig,
162    /// Database for serving the API
163    pub db: Database,
164    /// Modules registered with the federation
165    pub modules: ServerModuleRegistry,
166    /// Cached client config
167    pub client_cfg: ClientConfig,
168    pub force_api_secret: Option<String>,
169    /// For sending API events to consensus such as transactions
170    pub submission_sender: async_channel::Sender<ConsensusItem>,
171    pub shutdown_receiver: Receiver<Option<ScheduledShutdown>>,
172    pub shutdown_sender: Sender<Option<ScheduledShutdown>>,
173    pub ord_latency_receiver: watch::Receiver<Option<Duration>>,
174    /// Notified whenever consensus accepts an item.
175    pub accepted_item_receiver: watch::Receiver<()>,
176    /// Notified whenever consensus completes a session.
177    pub completed_session_receiver: watch::Receiver<super::session_cache::CompletedSession>,
178    pub p2p_status_receivers: P2PStatusReceivers,
179    pub ci_status_receivers: BTreeMap<PeerId, Receiver<Option<u64>>>,
180    pub bitcoin_rpc_connection: ServerBitcoinRpcMonitor,
181    pub supported_api_versions: SupportedApiVersionsSummary,
182    pub auth_ui: Option<ApiAuth>,
183    pub auth_api: Option<ApiAuth>,
184    pub code_version_str: String,
185    pub code_version_hash: String,
186    pub task_group: TaskGroup,
187}
188
189impl ConsensusApi {
190    pub fn api_versions_summary(&self) -> &SupportedApiVersionsSummary {
191        &self.supported_api_versions
192    }
193
194    pub fn get_active_api_secret(&self) -> Option<String> {
195        // TODO: In the future, we might want to fetch it from the DB, so it's possible
196        // to customize from the UX
197        self.force_api_secret.clone()
198    }
199
200    // Submit a transaction and long-poll until it is either accepted by
201    // consensus or becomes invalid; we return an error if and only if the
202    // transaction is invalid against the current consensus state
203    pub async fn submit_transaction(
204        &self,
205        transaction: Transaction,
206    ) -> Result<TransactionId, TransactionError> {
207        let txid = transaction.tx_hash();
208
209        debug!(target: LOG_NET_API, %txid, "Received a submitted transaction");
210
211        // Marked as seen before the transaction is checked, so that every item
212        // accepted and every session completed from here on wakes the loop below.
213        let mut accepted_item = self.accepted_item_receiver.clone();
214        let mut completed_session = self.completed_session_receiver.clone();
215
216        accepted_item.mark_unchanged();
217        completed_session.mark_unchanged();
218
219        // Create read-only DB tx so that the read state is consistent
220        let mut dbtx = self.db.begin_transaction_nc().await;
221        // we already processed the transaction before
222        if dbtx
223            .get_value(&AcceptedTransactionKey(txid))
224            .await
225            .is_some()
226        {
227            debug!(target: LOG_NET_API, %txid, "Transaction already accepted");
228            return Ok(txid);
229        }
230
231        // We ignore any writes, as we only verify if the transaction is valid here
232        dbtx.ignore_uncommitted();
233
234        process_transaction_with_dbtx(
235            self.modules.clone(),
236            &mut dbtx,
237            &transaction,
238            self.cfg.consensus.version,
239            TxProcessingMode::Submission,
240        )
241        .await
242        .inspect_err(|err| {
243            debug!(target: LOG_NET_API, %txid, err = %err.fmt_compact(), "Transaction rejected");
244        })?;
245
246        drop(dbtx);
247
248        let _ = self
249            .submission_sender
250            .send(ConsensusItem::Transaction(transaction.clone()))
251            .await
252            .inspect_err(|err| {
253                warn!(target: LOG_NET_API, %txid, err = %err.fmt_compact(), "Unable to submit the tx into consensus");
254            });
255
256        loop {
257            let (changed, session_completed) = tokio::select! {
258                changed = accepted_item.changed() => (changed, false),
259                changed = completed_session.changed() => (changed, true),
260            };
261
262            // The engine owns the senders for the life of the server, so this only
263            // fails at shutdown, which cancels the request anyway.
264            if changed.is_err() {
265                pending::<()>().await;
266            }
267
268            let mut dbtx = self.db.begin_transaction_nc().await;
269
270            // Our transaction may be the item that was just accepted, in which
271            // case revalidation would fail against its own state changes
272            if dbtx
273                .get_value(&AcceptedTransactionKey(txid))
274                .await
275                .is_some()
276            {
277                return Ok(txid);
278            }
279
280            dbtx.ignore_uncommitted();
281
282            process_transaction_with_dbtx(
283                self.modules.clone(),
284                &mut dbtx,
285                &transaction,
286                self.cfg.consensus.version,
287                TxProcessingMode::Submission,
288            )
289            .await
290            .inspect_err(|err| {
291                debug!(target: LOG_NET_API, %txid, err = %err.fmt_compact(), "Transaction rejected");
292            })?;
293
294            drop(dbtx);
295
296            if session_completed {
297                // A pending transaction may have been dropped at the session
298                // boundary since the data provider is recreated every session,
299                // hence we resubmit it for the next session
300                let _ = self
301                    .submission_sender
302                    .send(ConsensusItem::Transaction(transaction.clone()))
303                    .await
304                    .inspect_err(|err| {
305                        warn!(target: LOG_NET_API, %txid, err = %err.fmt_compact(), "Unable to submit the tx into consensus");
306                    });
307            }
308        }
309    }
310
311    pub async fn await_transaction(
312        &self,
313        txid: TransactionId,
314    ) -> (Vec<ModuleInstanceId>, DatabaseTransaction<'_, Committable>) {
315        debug!(target: LOG_NET_API, %txid, "Awaiting transaction acceptance");
316        self.db
317            .wait_key_check(&AcceptedTransactionKey(txid), std::convert::identity)
318            .await
319    }
320
321    pub async fn await_output_outcome(
322        &self,
323        outpoint: OutPoint,
324    ) -> Result<SerdeModuleEncoding<DynOutputOutcome>> {
325        debug!(target: LOG_NET_API, %outpoint, "Awaiting output outcome");
326        let (module_ids, mut dbtx) = self.await_transaction(outpoint.txid).await;
327
328        let module_id = module_ids
329            .into_iter()
330            .nth(outpoint.out_idx as usize)
331            .with_context(|| format!("Outpoint index out of bounds {outpoint:?}"))?;
332
333        #[allow(deprecated)]
334        let outcome = self
335            .modules
336            .get_expect(module_id)
337            .output_status(
338                &mut dbtx.to_ref_with_prefix_module_id(module_id).0.into_nc(),
339                outpoint,
340                module_id,
341            )
342            .await
343            .context("No output outcome for outpoint")?;
344
345        Ok((&outcome).into())
346    }
347
348    pub async fn await_outputs_outcomes(
349        &self,
350        outpoint_range: OutPointRange,
351    ) -> Result<Vec<Option<SerdeModuleEncoding<DynOutputOutcome>>>> {
352        // Has to happen before `await_transaction`, which blocks until the
353        // transaction shows up.
354        let count = checked_outputs_outcomes_count(outpoint_range)?;
355
356        // Wait for the transaction to be accepted first
357        let (module_ids, mut dbtx) = self.await_transaction(outpoint_range.txid()).await;
358
359        let mut outcomes = Vec::with_capacity(count);
360
361        for outpoint in outpoint_range {
362            let module_id = module_ids
363                .get(outpoint.out_idx as usize)
364                .with_context(|| format!("Outpoint index out of bounds {outpoint:?}"))?;
365
366            #[allow(deprecated)]
367            let outcome = self
368                .modules
369                .get_expect(*module_id)
370                .output_status(
371                    &mut dbtx.to_ref_with_prefix_module_id(*module_id).0.into_nc(),
372                    outpoint,
373                    *module_id,
374                )
375                .await
376                .map(|outcome| (&outcome).into());
377
378            outcomes.push(outcome);
379        }
380
381        Ok(outcomes)
382    }
383
384    pub fn session_count(&self) -> u64 {
385        self.completed_session_receiver
386            .borrow()
387            .as_ref()
388            .map_or(0, |latest| latest.index + 1)
389    }
390
391    async fn await_session_outcome(
392        &self,
393        index: u64,
394        format: OutcomeFormat,
395    ) -> Arc<serde_json::Value> {
396        super::session_cache::await_outcome(
397            &self.db,
398            &self.completed_session_receiver,
399            index,
400            format,
401        )
402        .await
403    }
404
405    pub async fn session_status(&self, session_index: u64) -> SessionStatusV2 {
406        let mut dbtx = self.db.begin_transaction_nc().await;
407
408        match session_index.cmp(&get_finished_session_count_static(&mut dbtx).await) {
409            Ordering::Greater => SessionStatusV2::Initial,
410            Ordering::Equal => SessionStatusV2::Pending(
411                dbtx.find_by_prefix(&AcceptedItemPrefix)
412                    .await
413                    .map(|entry| entry.1)
414                    .collect()
415                    .await,
416            ),
417            Ordering::Less => SessionStatusV2::Complete(
418                dbtx.get_value(&SignedSessionOutcomeKey(session_index))
419                    .await
420                    .expect("There are no gaps in session outcomes"),
421            ),
422        }
423    }
424
425    pub fn get_federation_status(&self) -> ApiResult<LegacyFederationStatus> {
426        let session_count = self.session_count();
427        let scheduled_shutdown = self
428            .shutdown_receiver
429            .borrow()
430            .map(|scheduled| scheduled.session);
431
432        let status_by_peer = self
433            .p2p_status_receivers
434            .iter()
435            .map(|(peer, p2p_receiver)| {
436                let ci_receiver = self.ci_status_receivers.get(peer).unwrap();
437
438                (
439                    *peer,
440                    legacy_peer_status(p2p_receiver, ci_receiver, session_count),
441                )
442            })
443            .collect::<BTreeMap<_, _>>();
444
445        let peers_flagged = status_by_peer
446            .values()
447            .filter(|status| status.flagged)
448            .count() as u64;
449
450        let peers_online = status_by_peer
451            .values()
452            .filter(|status| status.connection_status == LegacyP2PConnectionStatus::Connected)
453            .count() as u64;
454
455        let peers_offline = status_by_peer
456            .values()
457            .filter(|status| status.connection_status == LegacyP2PConnectionStatus::Disconnected)
458            .count() as u64;
459
460        Ok(LegacyFederationStatus {
461            session_count,
462            status_by_peer,
463            peers_online,
464            peers_offline,
465            peers_flagged,
466            scheduled_shutdown,
467        })
468    }
469
470    fn shutdown(&self, index: Option<u64>) {
471        self.shutdown_sender
472            .send_replace(index.map(|session| ScheduledShutdown {
473                session,
474                request: ShutdownRequest::AtSession,
475            }));
476    }
477
478    /// Schedules a shutdown for the session that is running now. The
479    /// consensus loop shuts down at the first session boundary at or after
480    /// the scheduled session, so a session that completes while this request
481    /// is handled defers the shutdown to the end of the next one instead of
482    /// cancelling it. Repeating the request never moves a shutdown that is
483    /// already due by then to a later session.
484    ///
485    /// Unlike `shutdown`, the resulting stop is reported as
486    /// [`ShutdownRequest::AfterCurrentSession`], so the process running the
487    /// server can tell it apart from a shutdown scheduled for a given session.
488    fn shutdown_after_current_session(&self) {
489        let current_session = self.session_count();
490        self.shutdown_sender
491            .send_if_modified(|scheduled| schedule_shutdown_after(scheduled, current_session));
492    }
493
494    async fn get_federation_audit(&self) -> ApiResult<AuditSummary> {
495        let mut dbtx = self.db.begin_transaction_nc().await;
496        // Writes are related to compacting audit keys, which we can safely ignore
497        // within an API request since the compaction will happen when constructing an
498        // audit in the consensus server
499        dbtx.ignore_uncommitted();
500
501        let mut audit = Audit::default();
502        let mut module_instance_id_to_kind = BTreeMap::new();
503        for (module_instance_id, kind, module) in self.modules.iter_modules() {
504            module_instance_id_to_kind.insert(module_instance_id, kind.as_str().to_string());
505            module
506                .audit(
507                    &mut dbtx.to_ref_with_prefix_module_id(module_instance_id).0,
508                    &mut audit,
509                    module_instance_id,
510                )
511                .await;
512        }
513        Ok(AuditSummary::from_audit(
514            &audit,
515            &module_instance_id_to_kind,
516        ))
517    }
518
519    /// Uses the in-memory config to write a config backup tar archive that
520    /// guardians can download. Private keys are stored as plaintext JSON.
521    /// Operators should rely on disk encryption for at-rest protection.
522    fn get_guardian_config_backup(&self, _auth: &GuardianAuthToken) -> GuardianConfigBackup {
523        let mut tar_archive_builder = tar::Builder::new(Vec::new());
524
525        let mut append = |name: &Path, data: &[u8]| {
526            let mut header = tar::Header::new_gnu();
527            header.set_path(name).expect("Error setting path");
528            header.set_size(data.len() as u64);
529            header.set_mode(0o644);
530            header.set_cksum();
531            tar_archive_builder
532                .append(&header, data)
533                .expect("Error adding data to tar archive");
534        };
535
536        append(
537            &PathBuf::from(LOCAL_CONFIG).with_extension(JSON_EXT),
538            &serde_json::to_vec(&self.cfg.local).expect("Error encoding local config"),
539        );
540
541        append(
542            &PathBuf::from(CONSENSUS_CONFIG).with_extension(JSON_EXT),
543            &serde_json::to_vec(&self.cfg.consensus).expect("Error encoding consensus config"),
544        );
545
546        append(
547            &PathBuf::from(PRIVATE_CONFIG).with_extension(JSON_EXT),
548            &serde_json::to_vec(&self.cfg.private).expect("Error encoding private config"),
549        );
550
551        let tar_archive_bytes = tar_archive_builder
552            .into_inner()
553            .expect("Error building tar archive");
554
555        GuardianConfigBackup { tar_archive_bytes }
556    }
557
558    async fn handle_backup_request(
559        &self,
560        dbtx: &mut DatabaseTransaction<'_>,
561        request: SignedBackupRequest,
562    ) -> Result<(), ApiError> {
563        let request = request
564            .verify_valid(SECP256K1)
565            .map_err(|_| ApiError::bad_request("invalid request".into()))?;
566
567        if backup_timestamp_is_too_far_in_future(request.timestamp, fedimint_core::time::now()) {
568            return Err(ApiError::bad_request(
569                "timestamp too far in the future".into(),
570            ));
571        }
572
573        if request.payload.len() > BACKUP_REQUEST_MAX_PAYLOAD_SIZE_BYTES {
574            return Err(ApiError::bad_request("snapshot too large".into()));
575        }
576        debug!(target: LOG_NET_API, id = %request.id, len = request.payload.len(), "Received client backup request");
577        if let Some(prev) = dbtx.get_value(&ClientBackupKey(request.id)).await
578            && request.timestamp <= prev.timestamp
579        {
580            debug!(target: LOG_NET_API, id = %request.id, len = request.payload.len(), "Received client backup request with old timestamp - ignoring");
581            return Err(ApiError::bad_request("timestamp too small".into()));
582        }
583
584        info!(target: LOG_NET_API, id = %request.id, len = request.payload.len(), "Storing new client backup");
585        let overwritten = dbtx
586            .insert_entry(
587                &ClientBackupKey(request.id),
588                &ClientBackupSnapshot {
589                    timestamp: request.timestamp,
590                    data: request.payload.clone(),
591                },
592            )
593            .await
594            .is_some();
595        BACKUP_WRITE_SIZE_BYTES.observe(request.payload.len() as f64);
596        if !overwritten {
597            dbtx.on_commit(|| STORED_BACKUPS_COUNT.inc());
598        }
599
600        Ok(())
601    }
602
603    async fn handle_recover_request(
604        &self,
605        dbtx: &mut DatabaseTransaction<'_>,
606        id: PublicKey,
607    ) -> Option<ClientBackupSnapshot> {
608        dbtx.get_value(&ClientBackupKey(id)).await
609    }
610
611    /// List API URL announcements from all peers we have received them from (at
612    /// least ourselves)
613    async fn api_announcements(&self) -> BTreeMap<PeerId, SignedApiAnnouncement> {
614        self.db
615            .begin_transaction_nc()
616            .await
617            .find_by_prefix(&ApiAnnouncementPrefix)
618            .await
619            .map(|(announcement_key, announcement)| (announcement_key.0, announcement))
620            .collect()
621            .await
622    }
623
624    /// Returns the tagged fedimintd version currently running
625    fn fedimintd_version(&self) -> String {
626        self.code_version_str.clone()
627    }
628
629    /// Add an API URL announcement from a peer to our database to be returned
630    /// by [`ConsensusApi::api_announcements`].
631    async fn submit_api_announcement(
632        &self,
633        peer_id: PeerId,
634        announcement: SignedApiAnnouncement,
635    ) -> Result<(), ApiError> {
636        let Some(peer_key) = self.cfg.consensus.broadcast_public_keys.get(&peer_id) else {
637            return Err(ApiError::bad_request("Peer not in federation".into()));
638        };
639
640        if !announcement.verify(SECP256K1, peer_key) {
641            return Err(ApiError::bad_request("Invalid signature".into()));
642        }
643
644        // Use autocommit to handle potential transaction conflicts with retries
645        self.db
646            .autocommit(
647                |dbtx, _| {
648                    let announcement = announcement.clone();
649                    Box::pin(async move {
650                        if let Some(existing_announcement) =
651                            dbtx.get_value(&ApiAnnouncementKey(peer_id)).await
652                        {
653                            // If the current announcement is semantically identical to the new one
654                            // (except for potentially having a
655                            // different, valid signature) we return ok to allow
656                            // the caller to stop submitting the value if they are in a retry loop.
657                            if existing_announcement.api_announcement
658                                == announcement.api_announcement
659                            {
660                                return Ok(());
661                            }
662
663                            // We only accept announcements with a nonce higher than the current one
664                            // to avoid replay attacks.
665                            if existing_announcement.api_announcement.nonce
666                                >= announcement.api_announcement.nonce
667                            {
668                                return Err(ApiError::bad_request(
669                                    "Outdated or redundant announcement".into(),
670                                ));
671                            }
672                        }
673
674                        dbtx.insert_entry(&ApiAnnouncementKey(peer_id), &announcement)
675                            .await;
676                        Ok(())
677                    })
678                },
679                None,
680            )
681            .await
682            .map_err(|e| match e {
683                fedimint_core::db::AutocommitError::ClosureError { error, .. } => error,
684                fedimint_core::db::AutocommitError::CommitFailed { last_error, .. } => {
685                    ApiError::server_error(format!("Database commit failed: {last_error}"))
686                }
687            })
688    }
689
690    async fn sign_api_announcement(&self, new_url: SafeUrl) -> SignedApiAnnouncement {
691        self.db
692            .autocommit(
693                |dbtx, _| {
694                    let new_url_inner = new_url.clone();
695                    Box::pin(async move {
696                        let new_nonce = dbtx
697                            .get_value(&ApiAnnouncementKey(self.cfg.local.identity))
698                            .await
699                            .map_or(0, |a| a.api_announcement.nonce + 1);
700                        let announcement = ApiAnnouncement {
701                            api_url: new_url_inner,
702                            nonce: new_nonce,
703                        };
704                        let ctx = secp256k1::Secp256k1::new();
705                        let signed_announcement = announcement
706                            .sign(&ctx, &self.cfg.private.broadcast_secret_key.keypair(&ctx));
707
708                        dbtx.insert_entry(
709                            &ApiAnnouncementKey(self.cfg.local.identity),
710                            &signed_announcement,
711                        )
712                        .await;
713
714                        Result::<_, ()>::Ok(signed_announcement)
715                    })
716                },
717                None,
718            )
719            .await
720            .expect("Will not terminate on error")
721    }
722
723    async fn guardian_metadata_list(
724        &self,
725    ) -> BTreeMap<PeerId, fedimint_core::net::guardian_metadata::SignedGuardianMetadata> {
726        use crate::net::api::guardian_metadata::{GuardianMetadataKey, GuardianMetadataPrefix};
727
728        self.db
729            .begin_transaction_nc()
730            .await
731            .find_by_prefix(&GuardianMetadataPrefix)
732            .await
733            .map(|(key, metadata): (GuardianMetadataKey, _)| (key.0, metadata))
734            .collect()
735            .await
736    }
737
738    async fn submit_guardian_metadata(
739        &self,
740        peer_id: PeerId,
741        metadata: fedimint_core::net::guardian_metadata::SignedGuardianMetadata,
742    ) -> Result<(), ApiError> {
743        use crate::net::api::guardian_metadata::GuardianMetadataKey;
744
745        let Some(peer_key) = self.cfg.consensus.broadcast_public_keys.get(&peer_id) else {
746            return Err(ApiError::bad_request("Peer not in federation".into()));
747        };
748
749        let now = fedimint_core::time::duration_since_epoch();
750        if let Err(e) = metadata.verify(SECP256K1, peer_key, now) {
751            return Err(ApiError::bad_request(format!(
752                "Invalid signature or timestamp: {e}"
753            )));
754        }
755
756        let mut dbtx = self.db.begin_transaction().await;
757
758        if let Some(existing_metadata) = dbtx.get_value(&GuardianMetadataKey(peer_id)).await {
759            // If the current metadata is semantically identical to the new one (except
760            // for potentially having a different, valid signature) we return ok to allow
761            // the caller to stop submitting the value if they are in a retry loop.
762            if existing_metadata.bytes == metadata.bytes {
763                return Ok(());
764            }
765
766            // Only update if the new metadata has a newer timestamp
767            if metadata.guardian_metadata().timestamp_secs
768                <= existing_metadata.guardian_metadata().timestamp_secs
769            {
770                return Err(ApiError::bad_request(
771                    "New metadata timestamp is not newer than existing".into(),
772                ));
773            }
774        }
775
776        dbtx.insert_entry(&GuardianMetadataKey(peer_id), &metadata)
777            .await;
778        dbtx.commit_tx().await;
779
780        Ok(())
781    }
782
783    async fn sign_guardian_metadata(
784        &self,
785        new_metadata: fedimint_core::net::guardian_metadata::GuardianMetadata,
786    ) -> fedimint_core::net::guardian_metadata::SignedGuardianMetadata {
787        sign_guardian_metadata_preserving_iroh_endpoint(
788            &self.db,
789            self.cfg.local.identity,
790            &self.cfg.private.broadcast_secret_key,
791            new_metadata,
792        )
793        .await
794    }
795
796    async fn get_invite_code(&self, api_secret: Option<String>) -> InviteCode {
797        let identity = self.cfg.local.identity;
798        let mut api_urls = get_api_urls(&self.db, &self.cfg.consensus).await;
799
800        InviteCode::new(
801            api_urls
802                .remove(&identity)
803                .expect("API URL for our identity must be present"),
804            identity,
805            self.cfg.calculate_federation_id(),
806            api_secret,
807        )
808    }
809}
810
811async fn sign_guardian_metadata_preserving_iroh_endpoint(
812    db: &Database,
813    identity: PeerId,
814    broadcast_secret_key: &secp256k1::SecretKey,
815    new_metadata: fedimint_core::net::guardian_metadata::GuardianMetadata,
816) -> fedimint_core::net::guardian_metadata::SignedGuardianMetadata {
817    use crate::net::api::guardian_metadata::GuardianMetadataKey;
818
819    db.autocommit(
820        |dbtx, _| {
821            let mut new_metadata = new_metadata.clone();
822            Box::pin(async move {
823                new_metadata.iroh_next_endpoint = dbtx
824                    .get_value(&GuardianMetadataKey(identity))
825                    .await
826                    .and_then(|metadata| metadata.guardian_metadata().iroh_next_endpoint.clone());
827
828                let ctx = secp256k1::Secp256k1::new();
829                let signed_metadata = new_metadata.sign(&ctx, &broadcast_secret_key.keypair(&ctx));
830
831                dbtx.insert_entry(&GuardianMetadataKey(identity), &signed_metadata)
832                    .await;
833
834                Result::<_, ()>::Ok(signed_metadata)
835            })
836        },
837        None,
838    )
839    .await
840    .expect("Will not terminate on error")
841}
842
843#[async_trait]
844impl HasApiContext<ConsensusApi> for ConsensusApi {
845    async fn context(
846        &self,
847        request: &ApiRequestErased,
848        id: Option<ModuleInstanceId>,
849    ) -> (&ConsensusApi, ApiEndpointContext) {
850        let mut db = self.db.clone();
851        if let Some(id) = id {
852            db = self.db.with_prefix_module_id(id).0;
853        }
854        let has_auth = match (&self.auth_api, &request.auth) {
855            (Some(server_auth), Some(req_auth)) => server_auth.verify(req_auth.as_str()),
856            _ => false,
857        };
858
859        (self, ApiEndpointContext::new(db, has_auth))
860    }
861}
862
863#[async_trait]
864impl HasApiContext<DynServerModule> for ConsensusApi {
865    async fn context(
866        &self,
867        request: &ApiRequestErased,
868        id: Option<ModuleInstanceId>,
869    ) -> (&DynServerModule, ApiEndpointContext) {
870        let (_, context): (&ConsensusApi, _) = self.context(request, id).await;
871        (
872            self.modules.get_expect(id.expect("required module id")),
873            context,
874        )
875    }
876}
877
878#[async_trait]
879impl IDashboardApi for ConsensusApi {
880    fn auth_ui(&self) -> Option<ApiAuth> {
881        self.auth_ui.clone()
882    }
883
884    async fn guardian_id(&self) -> PeerId {
885        self.cfg.local.identity
886    }
887
888    async fn guardian_names(&self) -> BTreeMap<PeerId, String> {
889        self.cfg
890            .consensus
891            .api_endpoints()
892            .iter()
893            .map(|(peer_id, endpoint)| (*peer_id, endpoint.name.clone()))
894            .collect()
895    }
896
897    async fn federation_name(&self) -> String {
898        self.cfg
899            .consensus
900            .meta
901            .get(META_FEDERATION_NAME_KEY)
902            .cloned()
903            .expect("Federation name must be set")
904    }
905
906    async fn session_count(&self) -> u64 {
907        self.session_count()
908    }
909
910    async fn get_session_status(&self, session_idx: u64) -> SessionStatusV2 {
911        self.session_status(session_idx).await
912    }
913
914    async fn consensus_ord_latency(&self) -> Option<Duration> {
915        *self.ord_latency_receiver.borrow()
916    }
917
918    async fn p2p_connection_status(&self) -> BTreeMap<PeerId, Option<P2PConnectionStatus>> {
919        self.p2p_status_receivers
920            .iter()
921            .map(|(peer, receiver)| (*peer, receiver.borrow().connected.clone()))
922            .collect()
923    }
924
925    async fn federation_invite_code(&self) -> String {
926        self.get_invite_code(self.get_active_api_secret())
927            .await
928            .to_string()
929    }
930
931    async fn federation_audit(&self) -> AuditSummary {
932        self.get_federation_audit()
933            .await
934            .expect("Failed to get federation audit")
935    }
936
937    async fn bitcoin_rpc_url(&self) -> SafeUrl {
938        self.bitcoin_rpc_connection.url()
939    }
940
941    async fn bitcoin_rpc_status(&self) -> Option<ServerBitcoinRpcStatus> {
942        self.bitcoin_rpc_connection.status()
943    }
944
945    async fn download_guardian_config_backup(
946        &self,
947        guardian_auth: &GuardianAuthToken,
948    ) -> GuardianConfigBackup {
949        self.get_guardian_config_backup(guardian_auth)
950    }
951
952    fn get_module_by_kind(&self, kind: ModuleKind) -> Option<&DynServerModule> {
953        self.modules
954            .iter_modules()
955            .find_map(|(_, module_kind, module)| {
956                if *module_kind == kind {
957                    Some(module)
958                } else {
959                    None
960                }
961            })
962    }
963
964    async fn fedimintd_version(&self) -> String {
965        self.code_version_str.clone()
966    }
967
968    async fn fedimintd_version_hash(&self) -> Option<String> {
969        non_zero_version_hash(&self.code_version_hash).map(str::to_owned)
970    }
971}
972
973pub fn server_endpoints() -> Vec<ApiEndpoint<ConsensusApi>> {
974    vec![
975        public_api_endpoint! {
976            VERSION_ENDPOINT,
977            ApiVersion::new(0, 0),
978            async |fedimint: &ConsensusApi, _context, _v: ()| -> SupportedApiVersionsSummary {
979                Ok(fedimint.api_versions_summary().to_owned())
980            }
981        },
982        public_api_endpoint! {
983            SUBMIT_TRANSACTION_ENDPOINT,
984            ApiVersion::new(0, 0),
985            async |fedimint: &ConsensusApi, _context, transaction: SerdeTransaction| -> SerdeModuleEncoding<TransactionSubmissionOutcome> {
986                let transaction = transaction
987                    .try_into_inner(&fedimint.modules.decoder_registry())
988                    .map_err(|e| ApiError::bad_request(e.to_string()))?;
989
990                // we return an inner error if and only if the submitted transaction is
991                // invalid and will be rejected if we were to submit it to consensus
992                Ok((&TransactionSubmissionOutcome(fedimint.submit_transaction(transaction).await)).into())
993            }
994        },
995        public_api_endpoint! {
996            AWAIT_TRANSACTION_ENDPOINT,
997            ApiVersion::new(0, 0),
998            async |fedimint: &ConsensusApi, _context, tx_hash: TransactionId| -> TransactionId {
999                fedimint.await_transaction(tx_hash).await;
1000
1001                Ok(tx_hash)
1002            }
1003        },
1004        public_api_endpoint! {
1005            AWAIT_OUTPUT_OUTCOME_ENDPOINT,
1006            ApiVersion::new(0, 0),
1007            async |fedimint: &ConsensusApi, _context, outpoint: OutPoint| -> SerdeModuleEncoding<DynOutputOutcome> {
1008                let outcome = fedimint
1009                    .await_output_outcome(outpoint)
1010                    .await
1011                    .map_err(|e| ApiError::bad_request(e.to_string()))?;
1012
1013                Ok(outcome)
1014            }
1015        },
1016        public_api_endpoint! {
1017            AWAIT_OUTPUTS_OUTCOMES_ENDPOINT,
1018            ApiVersion::new(0, 8),
1019            async |fedimint: &ConsensusApi, _context, outpoint_range: OutPointRange| -> Vec<Option<SerdeModuleEncoding<DynOutputOutcome>>> {
1020                let outcomes = fedimint
1021                    .await_outputs_outcomes(outpoint_range)
1022                    .await
1023                    .map_err(|e| ApiError::bad_request(e.to_string()))?;
1024
1025                Ok(outcomes)
1026            }
1027        },
1028        public_api_endpoint! {
1029            INVITE_CODE_ENDPOINT,
1030            ApiVersion::new(0, 0),
1031            async |fedimint: &ConsensusApi, _context,  _v: ()| -> String {
1032                Ok(fedimint.get_invite_code(fedimint.get_active_api_secret()).await.to_string())
1033            }
1034        },
1035        public_api_endpoint! {
1036            FEDERATION_ID_ENDPOINT,
1037            ApiVersion::new(0, 2),
1038            async |fedimint: &ConsensusApi, _context,  _v: ()| -> String {
1039                Ok(fedimint.cfg.calculate_federation_id().to_string())
1040            }
1041        },
1042        public_api_endpoint! {
1043            CLIENT_CONFIG_ENDPOINT,
1044            ApiVersion::new(0, 0),
1045            async |fedimint: &ConsensusApi, _context, _v: ()| -> ClientConfig {
1046                Ok(fedimint.client_cfg.clone())
1047            }
1048        },
1049        // Helper endpoint for Admin UI that can't parse consensus encoding
1050        public_api_endpoint! {
1051            CLIENT_CONFIG_JSON_ENDPOINT,
1052            ApiVersion::new(0, 0),
1053            async |fedimint: &ConsensusApi, _context, _v: ()| -> JsonClientConfig {
1054                Ok(fedimint.client_cfg.to_json())
1055            }
1056        },
1057        public_api_endpoint! {
1058            SERVER_CONFIG_CONSENSUS_HASH_ENDPOINT,
1059            ApiVersion::new(0, 0),
1060            async |fedimint: &ConsensusApi, _context, _v: ()| -> sha256::Hash {
1061                Ok(legacy_consensus_config_hash(&fedimint.cfg.consensus))
1062            }
1063        },
1064        public_api_endpoint! {
1065            STATUS_ENDPOINT,
1066            ApiVersion::new(0, 0),
1067            async |fedimint: &ConsensusApi, _context, _v: ()| -> StatusResponse {
1068                Ok(StatusResponse {
1069                    server: ServerStatusLegacy::ConsensusRunning,
1070                    federation: Some(fedimint.get_federation_status()?)
1071                })}
1072        },
1073        public_api_endpoint! {
1074            SETUP_STATUS_ENDPOINT,
1075            ApiVersion::new(0, 0),
1076            async |_f: &ConsensusApi, _c, _v: ()| -> SetupStatus {
1077                Ok(SetupStatus::ConsensusIsRunning)
1078            }
1079        },
1080        public_api_endpoint! {
1081            CONSENSUS_ORD_LATENCY_ENDPOINT,
1082            ApiVersion::new(0, 0),
1083            async |fedimint: &ConsensusApi, _c, _v: ()| -> Option<Duration> {
1084                Ok(*fedimint.ord_latency_receiver.borrow())
1085            }
1086        },
1087        public_api_endpoint! {
1088            P2P_CONNECTION_STATUS_ENDPOINT,
1089            ApiVersion::new(0, 0),
1090            async |fedimint: &ConsensusApi, _c, _v: ()| -> BTreeMap<PeerId, Option<P2PConnectionStatus>> {
1091                Ok(fedimint.p2p_status_receivers
1092                    .iter()
1093                    .map(|(peer, receiver)| (*peer, receiver.borrow().connected.clone()))
1094                    .collect())
1095            }
1096        },
1097        public_api_endpoint! {
1098            PEER_AVAILABILITY_ENDPOINT,
1099            ApiVersion::new(0, 0),
1100            async |fedimint: &ConsensusApi, _c, _v: ()| -> PeerAvailability {
1101                Ok(fedimint
1102                    .db
1103                    .begin_transaction_nc()
1104                    .await
1105                    .get_value(&PeerAvailabilityKey)
1106                    .await
1107                    .unwrap_or_default()
1108                    .report(fedimint_core::time::duration_since_epoch().as_secs()))
1109            }
1110        },
1111        public_api_endpoint! {
1112            SESSION_COUNT_ENDPOINT,
1113            ApiVersion::new(0, 0),
1114            async |fedimint: &ConsensusApi, _context, _v: ()| -> u64 {
1115                Ok(fedimint.session_count())
1116            }
1117        },
1118        public_api_endpoint! {
1119            AWAIT_SESSION_OUTCOME_ENDPOINT,
1120            ApiVersion::new(0, 0),
1121            async |fedimint: &ConsensusApi, _context, index: u64| -> Arc<serde_json::Value> {
1122                Ok(fedimint.await_session_outcome(index, OutcomeFormat::Unsigned).await)
1123            }
1124        },
1125        public_api_endpoint! {
1126            AWAIT_SIGNED_SESSION_OUTCOME_ENDPOINT,
1127            ApiVersion::new(0, 0),
1128            async |fedimint: &ConsensusApi, _context, index: u64| -> Arc<serde_json::Value> {
1129                Ok(fedimint.await_session_outcome(index, OutcomeFormat::Signed).await)
1130            }
1131        },
1132        public_api_endpoint! {
1133            SESSION_STATUS_ENDPOINT,
1134            ApiVersion::new(0, 1),
1135            async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SessionStatus> {
1136                Ok((&SessionStatus::from(fedimint.session_status(index).await)).into())
1137            }
1138        },
1139        public_api_endpoint! {
1140            SESSION_STATUS_V2_ENDPOINT,
1141            ApiVersion::new(0, 5),
1142            async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncodingBase64<SessionStatusV2> {
1143                Ok((&fedimint.session_status(index).await).into())
1144            }
1145        },
1146        admin_api_endpoint! {
1147            SHUTDOWN_ENDPOINT,
1148            ApiVersion::new(0, 3),
1149            async |fedimint: &ConsensusApi, context, index: Option<u64>| -> () {
1150                fedimint.shutdown(index);
1151                Ok(())
1152            }
1153        },
1154        admin_api_endpoint! {
1155            SHUTDOWN_AFTER_CURRENT_SESSION_ENDPOINT,
1156            ApiVersion::new(0, 10),
1157            async |fedimint: &ConsensusApi, context, _v: ()| -> () {
1158                fedimint.shutdown_after_current_session();
1159                Ok(())
1160            }
1161        },
1162        admin_api_endpoint! {
1163            AUDIT_ENDPOINT,
1164            ApiVersion::new(0, 0),
1165            async |fedimint: &ConsensusApi, context, _v: ()| -> AuditSummary {
1166                Ok(fedimint.get_federation_audit().await?)
1167            }
1168        },
1169        admin_api_endpoint! {
1170            GUARDIAN_CONFIG_BACKUP_ENDPOINT,
1171            ApiVersion::new(0, 2),
1172            async |fedimint: &ConsensusApi, context, auth, _v: ()| -> GuardianConfigBackup {
1173                Ok(fedimint.get_guardian_config_backup(&auth))
1174            }
1175        },
1176        public_api_endpoint! {
1177            BACKUP_ENDPOINT,
1178            ApiVersion::new(0, 0),
1179            async |fedimint: &ConsensusApi, context, request: SignedBackupRequest| -> () {
1180                let db = context.db();
1181                let mut dbtx = db.begin_transaction().await;
1182                fedimint
1183                    .handle_backup_request(&mut dbtx.to_ref_nc(), request).await?;
1184                dbtx.commit_tx_result().await?;
1185                Ok(())
1186
1187            }
1188        },
1189        public_api_endpoint! {
1190            RECOVER_ENDPOINT,
1191            ApiVersion::new(0, 0),
1192            async |fedimint: &ConsensusApi, context, id: PublicKey| -> Option<ClientBackupSnapshot> {
1193                let db = context.db();
1194                let mut dbtx = db.begin_transaction_nc().await;
1195                Ok(fedimint
1196                    .handle_recover_request(&mut dbtx, id).await)
1197            }
1198        },
1199        admin_api_endpoint! {
1200            AUTH_ENDPOINT,
1201            ApiVersion::new(0, 0),
1202            async |_fedimint: &ConsensusApi, context, _v: ()| -> () {
1203                Ok(())
1204            }
1205        },
1206        public_api_endpoint! {
1207            API_ANNOUNCEMENTS_ENDPOINT,
1208            ApiVersion::new(0, 3),
1209            async |fedimint: &ConsensusApi, _context, _v: ()| -> BTreeMap<PeerId, SignedApiAnnouncement> {
1210                Ok(fedimint.api_announcements().await)
1211            }
1212        },
1213        public_api_endpoint! {
1214            SUBMIT_API_ANNOUNCEMENT_ENDPOINT,
1215            ApiVersion::new(0, 3),
1216            async |fedimint: &ConsensusApi, _context, submission: SignedApiAnnouncementSubmission| -> () {
1217                fedimint.submit_api_announcement(submission.peer_id, submission.signed_api_announcement).await
1218            }
1219        },
1220        admin_api_endpoint! {
1221            SIGN_API_ANNOUNCEMENT_ENDPOINT,
1222            ApiVersion::new(0, 3),
1223            async |fedimint: &ConsensusApi, context, new_url: SafeUrl| -> SignedApiAnnouncement {
1224                Ok(fedimint.sign_api_announcement(new_url).await)
1225            }
1226        },
1227        public_api_endpoint! {
1228            GUARDIAN_METADATA_ENDPOINT,
1229            ApiVersion::new(0, 9),
1230            async |fedimint: &ConsensusApi, _context, _v: ()| -> BTreeMap<PeerId, fedimint_core::net::guardian_metadata::SignedGuardianMetadata> {
1231                Ok(fedimint.guardian_metadata_list().await)
1232            }
1233        },
1234        public_api_endpoint! {
1235            SUBMIT_GUARDIAN_METADATA_ENDPOINT,
1236            ApiVersion::new(0, 9),
1237            async |fedimint: &ConsensusApi, _context, submission: fedimint_core::net::guardian_metadata::SignedGuardianMetadataSubmission| -> () {
1238                fedimint.submit_guardian_metadata(submission.peer_id, submission.signed_guardian_metadata).await
1239            }
1240        },
1241        admin_api_endpoint! {
1242            SIGN_GUARDIAN_METADATA_ENDPOINT,
1243            ApiVersion::new(0, 9),
1244            async |fedimint: &ConsensusApi, context, metadata: fedimint_core::net::guardian_metadata::GuardianMetadata| -> fedimint_core::net::guardian_metadata::SignedGuardianMetadata {
1245                Ok(fedimint.sign_guardian_metadata(metadata).await)
1246            }
1247        },
1248        public_api_endpoint! {
1249            FEDIMINTD_VERSION_ENDPOINT,
1250            ApiVersion::new(0, 4),
1251            async |fedimint: &ConsensusApi, _context, _v: ()| -> String {
1252                Ok(fedimint.fedimintd_version())
1253            }
1254        },
1255        admin_api_endpoint! {
1256            BACKUP_STATISTICS_ENDPOINT,
1257            ApiVersion::new(0, 5),
1258            async |_fedimint: &ConsensusApi, context, _v: ()| -> BackupStatistics {
1259                let db = context.db();
1260                let mut dbtx = db.begin_transaction_nc().await;
1261                Ok(backup_statistics_static(&mut dbtx).await)
1262            }
1263        },
1264        public_api_endpoint! {
1265            CHAIN_ID_ENDPOINT,
1266            ApiVersion::new(0, 9),
1267            async |fedimint: &ConsensusApi, _context, _v: ()| -> ChainId {
1268                fedimint
1269                    .bitcoin_rpc_connection
1270                    .get_chain_id()
1271                    .await
1272                    .map_err(|e| ApiError::server_error(e.to_string()))
1273            }
1274        },
1275    ]
1276}
1277
1278pub(crate) async fn backup_statistics_static(
1279    dbtx: &mut DatabaseTransaction<'_>,
1280) -> BackupStatistics {
1281    const DAY_SECS: u64 = 24 * 60 * 60;
1282    const WEEK_SECS: u64 = 7 * DAY_SECS;
1283    const MONTH_SECS: u64 = 30 * DAY_SECS;
1284    const QUARTER_SECS: u64 = 3 * MONTH_SECS;
1285
1286    let mut backup_stats = BackupStatistics::default();
1287
1288    let mut all_backups_stream = dbtx.find_by_prefix(&ClientBackupKeyPrefix).await;
1289    while let Some((_, backup)) = all_backups_stream.next().await {
1290        backup_stats.num_backups += 1;
1291        backup_stats.total_size += backup.data.len();
1292
1293        let age_secs = backup.timestamp.elapsed().unwrap_or_default().as_secs();
1294        if age_secs < DAY_SECS {
1295            backup_stats.refreshed_1d += 1;
1296        }
1297        if age_secs < WEEK_SECS {
1298            backup_stats.refreshed_1w += 1;
1299        }
1300        if age_secs < MONTH_SECS {
1301            backup_stats.refreshed_1m += 1;
1302        }
1303        if age_secs < QUARTER_SECS {
1304            backup_stats.refreshed_3m += 1;
1305        }
1306    }
1307
1308    backup_stats
1309}
1310
1311/// Schedules a shutdown after `current_session`, keeping the session of a
1312/// shutdown that is already due by then. Returns whether the schedule changed.
1313fn schedule_shutdown_after(
1314    scheduled: &mut Option<ScheduledShutdown>,
1315    current_session: u64,
1316) -> bool {
1317    let session = scheduled
1318        .map(|scheduled| scheduled.session)
1319        .filter(|session| *session <= current_session)
1320        .unwrap_or(current_session);
1321    let after_current_session = Some(ScheduledShutdown {
1322        session,
1323        request: ShutdownRequest::AfterCurrentSession,
1324    });
1325    if *scheduled == after_current_session {
1326        return false;
1327    }
1328    *scheduled = after_current_session;
1329    true
1330}
1331
1332#[cfg(test)]
1333mod tests;