Skip to main content

fedimint_server/consensus/
api.rs

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