1use std::cmp::Ordering;
3use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5use std::time::Duration;
6
7use anyhow::{Context, Result, ensure};
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,
44 admin_api_endpoint, public_api_endpoint,
45};
46use fedimint_core::net::api_announcement::{
47 ApiAnnouncement, SignedApiAnnouncement, SignedApiAnnouncementSubmission,
48};
49use fedimint_core::net::auth::GuardianAuthToken;
50use fedimint_core::secp256k1::{PublicKey, SECP256K1};
51use fedimint_core::session_outcome::{
52 SessionOutcome, SessionStatus, SessionStatusV2, SignedSessionOutcome,
53};
54use fedimint_core::task::TaskGroup;
55use fedimint_core::transaction::{
56 SerdeTransaction, Transaction, TransactionError, TransactionSubmissionOutcome,
57};
58use fedimint_core::util::{FmtCompact, SafeUrl};
59use fedimint_core::version::non_zero_version_hash;
60use fedimint_core::{ChainId, OutPoint, OutPointRange, PeerId, TransactionId, secp256k1};
61use fedimint_logging::LOG_NET_API;
62use fedimint_server_core::bitcoin_rpc::ServerBitcoinRpcMonitor;
63use fedimint_server_core::dashboard_ui::{
64 IDashboardApi, P2PConnectionStatus, ServerBitcoinRpcStatus,
65};
66use fedimint_server_core::{DynServerModule, ServerModuleRegistry, ServerModuleRegistryExt};
67use futures::StreamExt;
68use tokio::sync::watch::{self, Receiver, Sender};
69use tracing::{debug, info, warn};
70
71use crate::config::io::{CONSENSUS_CONFIG, JSON_EXT, LOCAL_CONFIG, PRIVATE_CONFIG};
72use crate::config::{ServerConfig, legacy_consensus_config_hash};
73use crate::consensus::db::{
74 AcceptedItemKey, AcceptedItemPrefix, AcceptedTransactionKey, SignedSessionOutcomeKey,
75};
76use crate::consensus::engine::get_finished_session_count_static;
77use crate::consensus::transaction::{TxProcessingMode, process_transaction_with_dbtx};
78use crate::metrics::{BACKUP_WRITE_SIZE_BYTES, STORED_BACKUPS_COUNT};
79use crate::net::api::HasApiContext;
80use crate::net::api::announcement::{ApiAnnouncementKey, ApiAnnouncementPrefix, get_api_urls};
81use crate::net::p2p::P2PStatusReceivers;
82
83const MAX_OUTPUTS_OUTCOMES_BATCH: usize = 1024;
91
92fn checked_outputs_outcomes_count(outpoint_range: OutPointRange) -> Result<usize> {
98 let count = outpoint_range
99 .checked_count()
100 .context("Outpoint range is descending or too large")?;
101
102 ensure!(
103 count <= MAX_OUTPUTS_OUTCOMES_BATCH,
104 "Outpoint range must cover at most {MAX_OUTPUTS_OUTCOMES_BATCH} outputs, got {count}"
105 );
106
107 Ok(count)
108}
109
110#[derive(Clone)]
111pub struct ConsensusApi {
112 pub cfg: ServerConfig,
114 pub db: Database,
116 pub modules: ServerModuleRegistry,
118 pub client_cfg: ClientConfig,
120 pub force_api_secret: Option<String>,
121 pub submission_sender: async_channel::Sender<ConsensusItem>,
123 pub shutdown_receiver: Receiver<Option<u64>>,
124 pub shutdown_sender: Sender<Option<u64>>,
125 pub ord_latency_receiver: watch::Receiver<Option<Duration>>,
126 pub p2p_status_receivers: P2PStatusReceivers,
127 pub ci_status_receivers: BTreeMap<PeerId, Receiver<Option<u64>>>,
128 pub bitcoin_rpc_connection: ServerBitcoinRpcMonitor,
129 pub supported_api_versions: SupportedApiVersionsSummary,
130 pub auth_ui: Option<ApiAuth>,
131 pub auth_api: Option<ApiAuth>,
132 pub code_version_str: String,
133 pub code_version_hash: String,
134 pub task_group: TaskGroup,
135}
136
137impl ConsensusApi {
138 pub fn api_versions_summary(&self) -> &SupportedApiVersionsSummary {
139 &self.supported_api_versions
140 }
141
142 pub fn get_active_api_secret(&self) -> Option<String> {
143 self.force_api_secret.clone()
146 }
147
148 pub async fn submit_transaction(
152 &self,
153 transaction: Transaction,
154 ) -> Result<TransactionId, TransactionError> {
155 let txid = transaction.tx_hash();
156
157 debug!(target: LOG_NET_API, %txid, "Received a submitted transaction");
158
159 let mut dbtx = self.db.begin_transaction_nc().await;
161 if dbtx
163 .get_value(&AcceptedTransactionKey(txid))
164 .await
165 .is_some()
166 {
167 debug!(target: LOG_NET_API, %txid, "Transaction already accepted");
168 return Ok(txid);
169 }
170
171 dbtx.ignore_uncommitted();
173
174 process_transaction_with_dbtx(
175 self.modules.clone(),
176 &mut dbtx,
177 &transaction,
178 self.cfg.consensus.version,
179 TxProcessingMode::Submission,
180 )
181 .await
182 .inspect_err(|err| {
183 debug!(target: LOG_NET_API, %txid, err = %err.fmt_compact(), "Transaction rejected");
184 })?;
185
186 let mut session_index = get_finished_session_count_static(&mut dbtx).await;
187
188 let mut item_index = dbtx.find_by_prefix(&AcceptedItemPrefix).await.count().await as u64;
189
190 drop(dbtx);
191
192 let _ = self
193 .submission_sender
194 .send(ConsensusItem::Transaction(transaction.clone()))
195 .await
196 .inspect_err(|err| {
197 warn!(target: LOG_NET_API, %txid, err = %err.fmt_compact(), "Unable to submit the tx into consensus");
198 });
199
200 loop {
201 let accepted_item_key = AcceptedItemKey(item_index);
202 let signed_session_outcome_key = SignedSessionOutcomeKey(session_index);
203
204 let session_completed = tokio::select! {
205 _ = self.db.wait_key_exists(&accepted_item_key) => false,
206 _ = self.db.wait_key_exists(&signed_session_outcome_key) => true,
207 };
208
209 let mut dbtx = self.db.begin_transaction_nc().await;
210
211 if dbtx
214 .get_value(&AcceptedTransactionKey(txid))
215 .await
216 .is_some()
217 {
218 return Ok(txid);
219 }
220
221 dbtx.ignore_uncommitted();
222
223 process_transaction_with_dbtx(
224 self.modules.clone(),
225 &mut dbtx,
226 &transaction,
227 self.cfg.consensus.version,
228 TxProcessingMode::Submission,
229 )
230 .await
231 .inspect_err(|err| {
232 debug!(target: LOG_NET_API, %txid, err = %err.fmt_compact(), "Transaction rejected");
233 })?;
234
235 drop(dbtx);
236
237 if session_completed {
238 let _ = self
242 .submission_sender
243 .send(ConsensusItem::Transaction(transaction.clone()))
244 .await
245 .inspect_err(|err| {
246 warn!(target: LOG_NET_API, %txid, err = %err.fmt_compact(), "Unable to submit the tx into consensus");
247 });
248
249 session_index += 1;
250 item_index = 0;
251 } else {
252 item_index += 1;
253 }
254 }
255 }
256
257 pub async fn await_transaction(
258 &self,
259 txid: TransactionId,
260 ) -> (Vec<ModuleInstanceId>, DatabaseTransaction<'_, Committable>) {
261 debug!(target: LOG_NET_API, %txid, "Awaiting transaction acceptance");
262 self.db
263 .wait_key_check(&AcceptedTransactionKey(txid), std::convert::identity)
264 .await
265 }
266
267 pub async fn await_output_outcome(
268 &self,
269 outpoint: OutPoint,
270 ) -> Result<SerdeModuleEncoding<DynOutputOutcome>> {
271 debug!(target: LOG_NET_API, %outpoint, "Awaiting output outcome");
272 let (module_ids, mut dbtx) = self.await_transaction(outpoint.txid).await;
273
274 let module_id = module_ids
275 .into_iter()
276 .nth(outpoint.out_idx as usize)
277 .with_context(|| format!("Outpoint index out of bounds {outpoint:?}"))?;
278
279 #[allow(deprecated)]
280 let outcome = self
281 .modules
282 .get_expect(module_id)
283 .output_status(
284 &mut dbtx.to_ref_with_prefix_module_id(module_id).0.into_nc(),
285 outpoint,
286 module_id,
287 )
288 .await
289 .context("No output outcome for outpoint")?;
290
291 Ok((&outcome).into())
292 }
293
294 pub async fn await_outputs_outcomes(
295 &self,
296 outpoint_range: OutPointRange,
297 ) -> Result<Vec<Option<SerdeModuleEncoding<DynOutputOutcome>>>> {
298 let count = checked_outputs_outcomes_count(outpoint_range)?;
301
302 let (module_ids, mut dbtx) = self.await_transaction(outpoint_range.txid()).await;
304
305 let mut outcomes = Vec::with_capacity(count);
306
307 for outpoint in outpoint_range {
308 let module_id = module_ids
309 .get(outpoint.out_idx as usize)
310 .with_context(|| format!("Outpoint index out of bounds {outpoint:?}"))?;
311
312 #[allow(deprecated)]
313 let outcome = self
314 .modules
315 .get_expect(*module_id)
316 .output_status(
317 &mut dbtx.to_ref_with_prefix_module_id(*module_id).0.into_nc(),
318 outpoint,
319 *module_id,
320 )
321 .await
322 .map(|outcome| (&outcome).into());
323
324 outcomes.push(outcome);
325 }
326
327 Ok(outcomes)
328 }
329
330 pub async fn session_count(&self) -> u64 {
331 get_finished_session_count_static(&mut self.db.begin_transaction_nc().await).await
332 }
333
334 pub async fn await_signed_session_outcome(&self, index: u64) -> SignedSessionOutcome {
335 self.db
336 .wait_key_check(&SignedSessionOutcomeKey(index), std::convert::identity)
337 .await
338 .0
339 }
340
341 pub async fn session_status(&self, session_index: u64) -> SessionStatusV2 {
342 let mut dbtx = self.db.begin_transaction_nc().await;
343
344 match session_index.cmp(&get_finished_session_count_static(&mut dbtx).await) {
345 Ordering::Greater => SessionStatusV2::Initial,
346 Ordering::Equal => SessionStatusV2::Pending(
347 dbtx.find_by_prefix(&AcceptedItemPrefix)
348 .await
349 .map(|entry| entry.1)
350 .collect()
351 .await,
352 ),
353 Ordering::Less => SessionStatusV2::Complete(
354 dbtx.get_value(&SignedSessionOutcomeKey(session_index))
355 .await
356 .expect("There are no gaps in session outcomes"),
357 ),
358 }
359 }
360
361 pub async fn get_federation_status(&self) -> ApiResult<LegacyFederationStatus> {
362 let session_count = self.session_count().await;
363 let scheduled_shutdown = self.shutdown_receiver.borrow().to_owned();
364
365 let status_by_peer = self
366 .p2p_status_receivers
367 .iter()
368 .map(|(peer, p2p_receiver)| {
369 let ci_receiver = self.ci_status_receivers.get(peer).unwrap();
370
371 let consensus_status = LegacyPeerStatus {
372 connection_status: match p2p_receiver.borrow().connected {
373 Some(..) => LegacyP2PConnectionStatus::Connected,
374 None => LegacyP2PConnectionStatus::Disconnected,
375 },
376 last_contribution: *ci_receiver.borrow(),
377 flagged: ci_receiver.borrow().unwrap_or(0) + 1 < session_count,
378 };
379
380 (*peer, consensus_status)
381 })
382 .collect::<BTreeMap<_, _>>();
383
384 let peers_flagged = status_by_peer
385 .values()
386 .filter(|status| status.flagged)
387 .count() as u64;
388
389 let peers_online = status_by_peer
390 .values()
391 .filter(|status| status.connection_status == LegacyP2PConnectionStatus::Connected)
392 .count() as u64;
393
394 let peers_offline = status_by_peer
395 .values()
396 .filter(|status| status.connection_status == LegacyP2PConnectionStatus::Disconnected)
397 .count() as u64;
398
399 Ok(LegacyFederationStatus {
400 session_count,
401 status_by_peer,
402 peers_online,
403 peers_offline,
404 peers_flagged,
405 scheduled_shutdown,
406 })
407 }
408
409 fn shutdown(&self, index: Option<u64>) {
410 self.shutdown_sender.send_replace(index);
411 }
412
413 async fn get_federation_audit(&self) -> ApiResult<AuditSummary> {
414 let mut dbtx = self.db.begin_transaction_nc().await;
415 dbtx.ignore_uncommitted();
419
420 let mut audit = Audit::default();
421 let mut module_instance_id_to_kind = BTreeMap::new();
422 for (module_instance_id, kind, module) in self.modules.iter_modules() {
423 module_instance_id_to_kind.insert(module_instance_id, kind.as_str().to_string());
424 module
425 .audit(
426 &mut dbtx.to_ref_with_prefix_module_id(module_instance_id).0,
427 &mut audit,
428 module_instance_id,
429 )
430 .await;
431 }
432 Ok(AuditSummary::from_audit(
433 &audit,
434 &module_instance_id_to_kind,
435 ))
436 }
437
438 fn get_guardian_config_backup(&self, _auth: &GuardianAuthToken) -> GuardianConfigBackup {
442 let mut tar_archive_builder = tar::Builder::new(Vec::new());
443
444 let mut append = |name: &Path, data: &[u8]| {
445 let mut header = tar::Header::new_gnu();
446 header.set_path(name).expect("Error setting path");
447 header.set_size(data.len() as u64);
448 header.set_mode(0o644);
449 header.set_cksum();
450 tar_archive_builder
451 .append(&header, data)
452 .expect("Error adding data to tar archive");
453 };
454
455 append(
456 &PathBuf::from(LOCAL_CONFIG).with_extension(JSON_EXT),
457 &serde_json::to_vec(&self.cfg.local).expect("Error encoding local config"),
458 );
459
460 append(
461 &PathBuf::from(CONSENSUS_CONFIG).with_extension(JSON_EXT),
462 &serde_json::to_vec(&self.cfg.consensus).expect("Error encoding consensus config"),
463 );
464
465 append(
466 &PathBuf::from(PRIVATE_CONFIG).with_extension(JSON_EXT),
467 &serde_json::to_vec(&self.cfg.private).expect("Error encoding private config"),
468 );
469
470 let tar_archive_bytes = tar_archive_builder
471 .into_inner()
472 .expect("Error building tar archive");
473
474 GuardianConfigBackup { tar_archive_bytes }
475 }
476
477 async fn handle_backup_request(
478 &self,
479 dbtx: &mut DatabaseTransaction<'_>,
480 request: SignedBackupRequest,
481 ) -> Result<(), ApiError> {
482 let request = request
483 .verify_valid(SECP256K1)
484 .map_err(|_| ApiError::bad_request("invalid request".into()))?;
485
486 if request.payload.len() > BACKUP_REQUEST_MAX_PAYLOAD_SIZE_BYTES {
487 return Err(ApiError::bad_request("snapshot too large".into()));
488 }
489 debug!(target: LOG_NET_API, id = %request.id, len = request.payload.len(), "Received client backup request");
490 if let Some(prev) = dbtx.get_value(&ClientBackupKey(request.id)).await
491 && request.timestamp <= prev.timestamp
492 {
493 debug!(target: LOG_NET_API, id = %request.id, len = request.payload.len(), "Received client backup request with old timestamp - ignoring");
494 return Err(ApiError::bad_request("timestamp too small".into()));
495 }
496
497 info!(target: LOG_NET_API, id = %request.id, len = request.payload.len(), "Storing new client backup");
498 let overwritten = dbtx
499 .insert_entry(
500 &ClientBackupKey(request.id),
501 &ClientBackupSnapshot {
502 timestamp: request.timestamp,
503 data: request.payload.clone(),
504 },
505 )
506 .await
507 .is_some();
508 BACKUP_WRITE_SIZE_BYTES.observe(request.payload.len() as f64);
509 if !overwritten {
510 dbtx.on_commit(|| STORED_BACKUPS_COUNT.inc());
511 }
512
513 Ok(())
514 }
515
516 async fn handle_recover_request(
517 &self,
518 dbtx: &mut DatabaseTransaction<'_>,
519 id: PublicKey,
520 ) -> Option<ClientBackupSnapshot> {
521 dbtx.get_value(&ClientBackupKey(id)).await
522 }
523
524 async fn api_announcements(&self) -> BTreeMap<PeerId, SignedApiAnnouncement> {
527 self.db
528 .begin_transaction_nc()
529 .await
530 .find_by_prefix(&ApiAnnouncementPrefix)
531 .await
532 .map(|(announcement_key, announcement)| (announcement_key.0, announcement))
533 .collect()
534 .await
535 }
536
537 fn fedimintd_version(&self) -> String {
539 self.code_version_str.clone()
540 }
541
542 async fn submit_api_announcement(
545 &self,
546 peer_id: PeerId,
547 announcement: SignedApiAnnouncement,
548 ) -> Result<(), ApiError> {
549 let Some(peer_key) = self.cfg.consensus.broadcast_public_keys.get(&peer_id) else {
550 return Err(ApiError::bad_request("Peer not in federation".into()));
551 };
552
553 if !announcement.verify(SECP256K1, peer_key) {
554 return Err(ApiError::bad_request("Invalid signature".into()));
555 }
556
557 self.db
559 .autocommit(
560 |dbtx, _| {
561 let announcement = announcement.clone();
562 Box::pin(async move {
563 if let Some(existing_announcement) =
564 dbtx.get_value(&ApiAnnouncementKey(peer_id)).await
565 {
566 if existing_announcement.api_announcement
571 == announcement.api_announcement
572 {
573 return Ok(());
574 }
575
576 if existing_announcement.api_announcement.nonce
579 >= announcement.api_announcement.nonce
580 {
581 return Err(ApiError::bad_request(
582 "Outdated or redundant announcement".into(),
583 ));
584 }
585 }
586
587 dbtx.insert_entry(&ApiAnnouncementKey(peer_id), &announcement)
588 .await;
589 Ok(())
590 })
591 },
592 None,
593 )
594 .await
595 .map_err(|e| match e {
596 fedimint_core::db::AutocommitError::ClosureError { error, .. } => error,
597 fedimint_core::db::AutocommitError::CommitFailed { last_error, .. } => {
598 ApiError::server_error(format!("Database commit failed: {last_error}"))
599 }
600 })
601 }
602
603 async fn sign_api_announcement(&self, new_url: SafeUrl) -> SignedApiAnnouncement {
604 self.db
605 .autocommit(
606 |dbtx, _| {
607 let new_url_inner = new_url.clone();
608 Box::pin(async move {
609 let new_nonce = dbtx
610 .get_value(&ApiAnnouncementKey(self.cfg.local.identity))
611 .await
612 .map_or(0, |a| a.api_announcement.nonce + 1);
613 let announcement = ApiAnnouncement {
614 api_url: new_url_inner,
615 nonce: new_nonce,
616 };
617 let ctx = secp256k1::Secp256k1::new();
618 let signed_announcement = announcement
619 .sign(&ctx, &self.cfg.private.broadcast_secret_key.keypair(&ctx));
620
621 dbtx.insert_entry(
622 &ApiAnnouncementKey(self.cfg.local.identity),
623 &signed_announcement,
624 )
625 .await;
626
627 Result::<_, ()>::Ok(signed_announcement)
628 })
629 },
630 None,
631 )
632 .await
633 .expect("Will not terminate on error")
634 }
635
636 async fn guardian_metadata_list(
637 &self,
638 ) -> BTreeMap<PeerId, fedimint_core::net::guardian_metadata::SignedGuardianMetadata> {
639 use crate::net::api::guardian_metadata::{GuardianMetadataKey, GuardianMetadataPrefix};
640
641 self.db
642 .begin_transaction_nc()
643 .await
644 .find_by_prefix(&GuardianMetadataPrefix)
645 .await
646 .map(|(key, metadata): (GuardianMetadataKey, _)| (key.0, metadata))
647 .collect()
648 .await
649 }
650
651 async fn submit_guardian_metadata(
652 &self,
653 peer_id: PeerId,
654 metadata: fedimint_core::net::guardian_metadata::SignedGuardianMetadata,
655 ) -> Result<(), ApiError> {
656 use crate::net::api::guardian_metadata::GuardianMetadataKey;
657
658 let Some(peer_key) = self.cfg.consensus.broadcast_public_keys.get(&peer_id) else {
659 return Err(ApiError::bad_request("Peer not in federation".into()));
660 };
661
662 let now = fedimint_core::time::duration_since_epoch();
663 if let Err(e) = metadata.verify(SECP256K1, peer_key, now) {
664 return Err(ApiError::bad_request(format!(
665 "Invalid signature or timestamp: {e}"
666 )));
667 }
668
669 let mut dbtx = self.db.begin_transaction().await;
670
671 if let Some(existing_metadata) = dbtx.get_value(&GuardianMetadataKey(peer_id)).await {
672 if existing_metadata.bytes == metadata.bytes {
676 return Ok(());
677 }
678
679 if metadata.guardian_metadata().timestamp_secs
681 <= existing_metadata.guardian_metadata().timestamp_secs
682 {
683 return Err(ApiError::bad_request(
684 "New metadata timestamp is not newer than existing".into(),
685 ));
686 }
687 }
688
689 dbtx.insert_entry(&GuardianMetadataKey(peer_id), &metadata)
690 .await;
691 dbtx.commit_tx().await;
692
693 Ok(())
694 }
695
696 async fn sign_guardian_metadata(
697 &self,
698 new_metadata: fedimint_core::net::guardian_metadata::GuardianMetadata,
699 ) -> fedimint_core::net::guardian_metadata::SignedGuardianMetadata {
700 sign_guardian_metadata_preserving_iroh_endpoint(
701 &self.db,
702 self.cfg.local.identity,
703 &self.cfg.private.broadcast_secret_key,
704 new_metadata,
705 )
706 .await
707 }
708
709 async fn get_invite_code(&self, api_secret: Option<String>) -> InviteCode {
710 let identity = self.cfg.local.identity;
711 let mut api_urls = get_api_urls(&self.db, &self.cfg.consensus).await;
712
713 InviteCode::new(
714 api_urls
715 .remove(&identity)
716 .expect("API URL for our identity must be present"),
717 identity,
718 self.cfg.calculate_federation_id(),
719 api_secret,
720 )
721 }
722}
723
724async fn sign_guardian_metadata_preserving_iroh_endpoint(
725 db: &Database,
726 identity: PeerId,
727 broadcast_secret_key: &secp256k1::SecretKey,
728 new_metadata: fedimint_core::net::guardian_metadata::GuardianMetadata,
729) -> fedimint_core::net::guardian_metadata::SignedGuardianMetadata {
730 use crate::net::api::guardian_metadata::GuardianMetadataKey;
731
732 db.autocommit(
733 |dbtx, _| {
734 let mut new_metadata = new_metadata.clone();
735 Box::pin(async move {
736 new_metadata.iroh_next_endpoint = dbtx
737 .get_value(&GuardianMetadataKey(identity))
738 .await
739 .and_then(|metadata| metadata.guardian_metadata().iroh_next_endpoint.clone());
740
741 let ctx = secp256k1::Secp256k1::new();
742 let signed_metadata = new_metadata.sign(&ctx, &broadcast_secret_key.keypair(&ctx));
743
744 dbtx.insert_entry(&GuardianMetadataKey(identity), &signed_metadata)
745 .await;
746
747 Result::<_, ()>::Ok(signed_metadata)
748 })
749 },
750 None,
751 )
752 .await
753 .expect("Will not terminate on error")
754}
755
756#[async_trait]
757impl HasApiContext<ConsensusApi> for ConsensusApi {
758 async fn context(
759 &self,
760 request: &ApiRequestErased,
761 id: Option<ModuleInstanceId>,
762 ) -> (&ConsensusApi, ApiEndpointContext) {
763 let mut db = self.db.clone();
764 if let Some(id) = id {
765 db = self.db.with_prefix_module_id(id).0;
766 }
767 let has_auth = match (&self.auth_api, &request.auth) {
768 (Some(server_auth), Some(req_auth)) => server_auth.verify(req_auth.as_str()),
769 _ => false,
770 };
771
772 (self, ApiEndpointContext::new(db, has_auth))
773 }
774}
775
776#[async_trait]
777impl HasApiContext<DynServerModule> for ConsensusApi {
778 async fn context(
779 &self,
780 request: &ApiRequestErased,
781 id: Option<ModuleInstanceId>,
782 ) -> (&DynServerModule, ApiEndpointContext) {
783 let (_, context): (&ConsensusApi, _) = self.context(request, id).await;
784 (
785 self.modules.get_expect(id.expect("required module id")),
786 context,
787 )
788 }
789}
790
791#[async_trait]
792impl IDashboardApi for ConsensusApi {
793 fn auth_ui(&self) -> Option<ApiAuth> {
794 self.auth_ui.clone()
795 }
796
797 async fn guardian_id(&self) -> PeerId {
798 self.cfg.local.identity
799 }
800
801 async fn guardian_names(&self) -> BTreeMap<PeerId, String> {
802 self.cfg
803 .consensus
804 .api_endpoints()
805 .iter()
806 .map(|(peer_id, endpoint)| (*peer_id, endpoint.name.clone()))
807 .collect()
808 }
809
810 async fn federation_name(&self) -> String {
811 self.cfg
812 .consensus
813 .meta
814 .get(META_FEDERATION_NAME_KEY)
815 .cloned()
816 .expect("Federation name must be set")
817 }
818
819 async fn session_count(&self) -> u64 {
820 self.session_count().await
821 }
822
823 async fn get_session_status(&self, session_idx: u64) -> SessionStatusV2 {
824 self.session_status(session_idx).await
825 }
826
827 async fn consensus_ord_latency(&self) -> Option<Duration> {
828 *self.ord_latency_receiver.borrow()
829 }
830
831 async fn p2p_connection_status(&self) -> BTreeMap<PeerId, Option<P2PConnectionStatus>> {
832 self.p2p_status_receivers
833 .iter()
834 .map(|(peer, receiver)| (*peer, receiver.borrow().connected.clone()))
835 .collect()
836 }
837
838 async fn federation_invite_code(&self) -> String {
839 self.get_invite_code(self.get_active_api_secret())
840 .await
841 .to_string()
842 }
843
844 async fn federation_audit(&self) -> AuditSummary {
845 self.get_federation_audit()
846 .await
847 .expect("Failed to get federation audit")
848 }
849
850 async fn bitcoin_rpc_url(&self) -> SafeUrl {
851 self.bitcoin_rpc_connection.url()
852 }
853
854 async fn bitcoin_rpc_status(&self) -> Option<ServerBitcoinRpcStatus> {
855 self.bitcoin_rpc_connection.status()
856 }
857
858 async fn download_guardian_config_backup(
859 &self,
860 guardian_auth: &GuardianAuthToken,
861 ) -> GuardianConfigBackup {
862 self.get_guardian_config_backup(guardian_auth)
863 }
864
865 fn get_module_by_kind(&self, kind: ModuleKind) -> Option<&DynServerModule> {
866 self.modules
867 .iter_modules()
868 .find_map(|(_, module_kind, module)| {
869 if *module_kind == kind {
870 Some(module)
871 } else {
872 None
873 }
874 })
875 }
876
877 async fn fedimintd_version(&self) -> String {
878 self.code_version_str.clone()
879 }
880
881 async fn fedimintd_version_hash(&self) -> Option<String> {
882 non_zero_version_hash(&self.code_version_hash).map(str::to_owned)
883 }
884}
885
886pub fn server_endpoints() -> Vec<ApiEndpoint<ConsensusApi>> {
887 vec![
888 public_api_endpoint! {
889 VERSION_ENDPOINT,
890 ApiVersion::new(0, 0),
891 async |fedimint: &ConsensusApi, _context, _v: ()| -> SupportedApiVersionsSummary {
892 Ok(fedimint.api_versions_summary().to_owned())
893 }
894 },
895 public_api_endpoint! {
896 SUBMIT_TRANSACTION_ENDPOINT,
897 ApiVersion::new(0, 0),
898 async |fedimint: &ConsensusApi, _context, transaction: SerdeTransaction| -> SerdeModuleEncoding<TransactionSubmissionOutcome> {
899 let transaction = transaction
900 .try_into_inner(&fedimint.modules.decoder_registry())
901 .map_err(|e| ApiError::bad_request(e.to_string()))?;
902
903 Ok((&TransactionSubmissionOutcome(fedimint.submit_transaction(transaction).await)).into())
906 }
907 },
908 public_api_endpoint! {
909 AWAIT_TRANSACTION_ENDPOINT,
910 ApiVersion::new(0, 0),
911 async |fedimint: &ConsensusApi, _context, tx_hash: TransactionId| -> TransactionId {
912 fedimint.await_transaction(tx_hash).await;
913
914 Ok(tx_hash)
915 }
916 },
917 public_api_endpoint! {
918 AWAIT_OUTPUT_OUTCOME_ENDPOINT,
919 ApiVersion::new(0, 0),
920 async |fedimint: &ConsensusApi, _context, outpoint: OutPoint| -> SerdeModuleEncoding<DynOutputOutcome> {
921 let outcome = fedimint
922 .await_output_outcome(outpoint)
923 .await
924 .map_err(|e| ApiError::bad_request(e.to_string()))?;
925
926 Ok(outcome)
927 }
928 },
929 public_api_endpoint! {
930 AWAIT_OUTPUTS_OUTCOMES_ENDPOINT,
931 ApiVersion::new(0, 8),
932 async |fedimint: &ConsensusApi, _context, outpoint_range: OutPointRange| -> Vec<Option<SerdeModuleEncoding<DynOutputOutcome>>> {
933 let outcomes = fedimint
934 .await_outputs_outcomes(outpoint_range)
935 .await
936 .map_err(|e| ApiError::bad_request(e.to_string()))?;
937
938 Ok(outcomes)
939 }
940 },
941 public_api_endpoint! {
942 INVITE_CODE_ENDPOINT,
943 ApiVersion::new(0, 0),
944 async |fedimint: &ConsensusApi, _context, _v: ()| -> String {
945 Ok(fedimint.get_invite_code(fedimint.get_active_api_secret()).await.to_string())
946 }
947 },
948 public_api_endpoint! {
949 FEDERATION_ID_ENDPOINT,
950 ApiVersion::new(0, 2),
951 async |fedimint: &ConsensusApi, _context, _v: ()| -> String {
952 Ok(fedimint.cfg.calculate_federation_id().to_string())
953 }
954 },
955 public_api_endpoint! {
956 CLIENT_CONFIG_ENDPOINT,
957 ApiVersion::new(0, 0),
958 async |fedimint: &ConsensusApi, _context, _v: ()| -> ClientConfig {
959 Ok(fedimint.client_cfg.clone())
960 }
961 },
962 public_api_endpoint! {
964 CLIENT_CONFIG_JSON_ENDPOINT,
965 ApiVersion::new(0, 0),
966 async |fedimint: &ConsensusApi, _context, _v: ()| -> JsonClientConfig {
967 Ok(fedimint.client_cfg.to_json())
968 }
969 },
970 public_api_endpoint! {
971 SERVER_CONFIG_CONSENSUS_HASH_ENDPOINT,
972 ApiVersion::new(0, 0),
973 async |fedimint: &ConsensusApi, _context, _v: ()| -> sha256::Hash {
974 Ok(legacy_consensus_config_hash(&fedimint.cfg.consensus))
975 }
976 },
977 public_api_endpoint! {
978 STATUS_ENDPOINT,
979 ApiVersion::new(0, 0),
980 async |fedimint: &ConsensusApi, _context, _v: ()| -> StatusResponse {
981 Ok(StatusResponse {
982 server: ServerStatusLegacy::ConsensusRunning,
983 federation: Some(fedimint.get_federation_status().await?)
984 })}
985 },
986 public_api_endpoint! {
987 SETUP_STATUS_ENDPOINT,
988 ApiVersion::new(0, 0),
989 async |_f: &ConsensusApi, _c, _v: ()| -> SetupStatus {
990 Ok(SetupStatus::ConsensusIsRunning)
991 }
992 },
993 public_api_endpoint! {
994 CONSENSUS_ORD_LATENCY_ENDPOINT,
995 ApiVersion::new(0, 0),
996 async |fedimint: &ConsensusApi, _c, _v: ()| -> Option<Duration> {
997 Ok(*fedimint.ord_latency_receiver.borrow())
998 }
999 },
1000 public_api_endpoint! {
1001 P2P_CONNECTION_STATUS_ENDPOINT,
1002 ApiVersion::new(0, 0),
1003 async |fedimint: &ConsensusApi, _c, _v: ()| -> BTreeMap<PeerId, Option<P2PConnectionStatus>> {
1004 Ok(fedimint.p2p_status_receivers
1005 .iter()
1006 .map(|(peer, receiver)| (*peer, receiver.borrow().connected.clone()))
1007 .collect())
1008 }
1009 },
1010 public_api_endpoint! {
1011 SESSION_COUNT_ENDPOINT,
1012 ApiVersion::new(0, 0),
1013 async |fedimint: &ConsensusApi, _context, _v: ()| -> u64 {
1014 Ok(fedimint.session_count().await)
1015 }
1016 },
1017 public_api_endpoint! {
1018 AWAIT_SESSION_OUTCOME_ENDPOINT,
1019 ApiVersion::new(0, 0),
1020 async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SessionOutcome> {
1021 Ok((&fedimint.await_signed_session_outcome(index).await.session_outcome).into())
1022 }
1023 },
1024 public_api_endpoint! {
1025 AWAIT_SIGNED_SESSION_OUTCOME_ENDPOINT,
1026 ApiVersion::new(0, 0),
1027 async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SignedSessionOutcome> {
1028 Ok((&fedimint.await_signed_session_outcome(index).await).into())
1029 }
1030 },
1031 public_api_endpoint! {
1032 SESSION_STATUS_ENDPOINT,
1033 ApiVersion::new(0, 1),
1034 async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SessionStatus> {
1035 Ok((&SessionStatus::from(fedimint.session_status(index).await)).into())
1036 }
1037 },
1038 public_api_endpoint! {
1039 SESSION_STATUS_V2_ENDPOINT,
1040 ApiVersion::new(0, 5),
1041 async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncodingBase64<SessionStatusV2> {
1042 Ok((&fedimint.session_status(index).await).into())
1043 }
1044 },
1045 admin_api_endpoint! {
1046 SHUTDOWN_ENDPOINT,
1047 ApiVersion::new(0, 3),
1048 async |fedimint: &ConsensusApi, context, index: Option<u64>| -> () {
1049 fedimint.shutdown(index);
1050 Ok(())
1051 }
1052 },
1053 admin_api_endpoint! {
1054 AUDIT_ENDPOINT,
1055 ApiVersion::new(0, 0),
1056 async |fedimint: &ConsensusApi, context, _v: ()| -> AuditSummary {
1057 Ok(fedimint.get_federation_audit().await?)
1058 }
1059 },
1060 admin_api_endpoint! {
1061 GUARDIAN_CONFIG_BACKUP_ENDPOINT,
1062 ApiVersion::new(0, 2),
1063 async |fedimint: &ConsensusApi, context, auth, _v: ()| -> GuardianConfigBackup {
1064 Ok(fedimint.get_guardian_config_backup(&auth))
1065 }
1066 },
1067 public_api_endpoint! {
1068 BACKUP_ENDPOINT,
1069 ApiVersion::new(0, 0),
1070 async |fedimint: &ConsensusApi, context, request: SignedBackupRequest| -> () {
1071 let db = context.db();
1072 let mut dbtx = db.begin_transaction().await;
1073 fedimint
1074 .handle_backup_request(&mut dbtx.to_ref_nc(), request).await?;
1075 dbtx.commit_tx_result().await?;
1076 Ok(())
1077
1078 }
1079 },
1080 public_api_endpoint! {
1081 RECOVER_ENDPOINT,
1082 ApiVersion::new(0, 0),
1083 async |fedimint: &ConsensusApi, context, id: PublicKey| -> Option<ClientBackupSnapshot> {
1084 let db = context.db();
1085 let mut dbtx = db.begin_transaction_nc().await;
1086 Ok(fedimint
1087 .handle_recover_request(&mut dbtx, id).await)
1088 }
1089 },
1090 admin_api_endpoint! {
1091 AUTH_ENDPOINT,
1092 ApiVersion::new(0, 0),
1093 async |_fedimint: &ConsensusApi, context, _v: ()| -> () {
1094 Ok(())
1095 }
1096 },
1097 public_api_endpoint! {
1098 API_ANNOUNCEMENTS_ENDPOINT,
1099 ApiVersion::new(0, 3),
1100 async |fedimint: &ConsensusApi, _context, _v: ()| -> BTreeMap<PeerId, SignedApiAnnouncement> {
1101 Ok(fedimint.api_announcements().await)
1102 }
1103 },
1104 public_api_endpoint! {
1105 SUBMIT_API_ANNOUNCEMENT_ENDPOINT,
1106 ApiVersion::new(0, 3),
1107 async |fedimint: &ConsensusApi, _context, submission: SignedApiAnnouncementSubmission| -> () {
1108 fedimint.submit_api_announcement(submission.peer_id, submission.signed_api_announcement).await
1109 }
1110 },
1111 admin_api_endpoint! {
1112 SIGN_API_ANNOUNCEMENT_ENDPOINT,
1113 ApiVersion::new(0, 3),
1114 async |fedimint: &ConsensusApi, context, new_url: SafeUrl| -> SignedApiAnnouncement {
1115 Ok(fedimint.sign_api_announcement(new_url).await)
1116 }
1117 },
1118 public_api_endpoint! {
1119 GUARDIAN_METADATA_ENDPOINT,
1120 ApiVersion::new(0, 9),
1121 async |fedimint: &ConsensusApi, _context, _v: ()| -> BTreeMap<PeerId, fedimint_core::net::guardian_metadata::SignedGuardianMetadata> {
1122 Ok(fedimint.guardian_metadata_list().await)
1123 }
1124 },
1125 public_api_endpoint! {
1126 SUBMIT_GUARDIAN_METADATA_ENDPOINT,
1127 ApiVersion::new(0, 9),
1128 async |fedimint: &ConsensusApi, _context, submission: fedimint_core::net::guardian_metadata::SignedGuardianMetadataSubmission| -> () {
1129 fedimint.submit_guardian_metadata(submission.peer_id, submission.signed_guardian_metadata).await
1130 }
1131 },
1132 admin_api_endpoint! {
1133 SIGN_GUARDIAN_METADATA_ENDPOINT,
1134 ApiVersion::new(0, 9),
1135 async |fedimint: &ConsensusApi, context, metadata: fedimint_core::net::guardian_metadata::GuardianMetadata| -> fedimint_core::net::guardian_metadata::SignedGuardianMetadata {
1136 Ok(fedimint.sign_guardian_metadata(metadata).await)
1137 }
1138 },
1139 public_api_endpoint! {
1140 FEDIMINTD_VERSION_ENDPOINT,
1141 ApiVersion::new(0, 4),
1142 async |fedimint: &ConsensusApi, _context, _v: ()| -> String {
1143 Ok(fedimint.fedimintd_version())
1144 }
1145 },
1146 admin_api_endpoint! {
1147 BACKUP_STATISTICS_ENDPOINT,
1148 ApiVersion::new(0, 5),
1149 async |_fedimint: &ConsensusApi, context, _v: ()| -> BackupStatistics {
1150 let db = context.db();
1151 let mut dbtx = db.begin_transaction_nc().await;
1152 Ok(backup_statistics_static(&mut dbtx).await)
1153 }
1154 },
1155 public_api_endpoint! {
1156 CHAIN_ID_ENDPOINT,
1157 ApiVersion::new(0, 9),
1158 async |fedimint: &ConsensusApi, _context, _v: ()| -> ChainId {
1159 fedimint
1160 .bitcoin_rpc_connection
1161 .get_chain_id()
1162 .await
1163 .map_err(|e| ApiError::server_error(e.to_string()))
1164 }
1165 },
1166 ]
1167}
1168
1169pub(crate) async fn backup_statistics_static(
1170 dbtx: &mut DatabaseTransaction<'_>,
1171) -> BackupStatistics {
1172 const DAY_SECS: u64 = 24 * 60 * 60;
1173 const WEEK_SECS: u64 = 7 * DAY_SECS;
1174 const MONTH_SECS: u64 = 30 * DAY_SECS;
1175 const QUARTER_SECS: u64 = 3 * MONTH_SECS;
1176
1177 let mut backup_stats = BackupStatistics::default();
1178
1179 let mut all_backups_stream = dbtx.find_by_prefix(&ClientBackupKeyPrefix).await;
1180 while let Some((_, backup)) = all_backups_stream.next().await {
1181 backup_stats.num_backups += 1;
1182 backup_stats.total_size += backup.data.len();
1183
1184 let age_secs = backup.timestamp.elapsed().unwrap_or_default().as_secs();
1185 if age_secs < DAY_SECS {
1186 backup_stats.refreshed_1d += 1;
1187 }
1188 if age_secs < WEEK_SECS {
1189 backup_stats.refreshed_1w += 1;
1190 }
1191 if age_secs < MONTH_SECS {
1192 backup_stats.refreshed_1m += 1;
1193 }
1194 if age_secs < QUARTER_SECS {
1195 backup_stats.refreshed_3m += 1;
1196 }
1197 }
1198
1199 backup_stats
1200}
1201
1202#[cfg(test)]
1203mod tests {
1204 use fedimint_core::db::IRawDatabaseExt as _;
1205 use fedimint_core::db::mem_impl::MemDatabase;
1206 use fedimint_core::net::guardian_metadata::GuardianMetadata;
1207 use fedimint_core::{BitcoinHash as _, IdxRange, TransactionId};
1208
1209 use super::*;
1210 use crate::net::api::guardian_metadata::GuardianMetadataKey;
1211
1212 #[test]
1216 fn outputs_outcomes_range_is_bounded() {
1217 let txid = TransactionId::from_slice(&[0; 32]).expect("32 bytes is a valid txid");
1218 let range = |start, end| OutPointRange::new(txid, IdxRange::from(start..end));
1219
1220 assert_eq!(
1221 checked_outputs_outcomes_count(range(0, MAX_OUTPUTS_OUTCOMES_BATCH as u64))
1222 .expect("a range at the limit is served"),
1223 MAX_OUTPUTS_OUTCOMES_BATCH
1224 );
1225
1226 for rejected in [
1227 range(0, u64::MAX),
1228 range(0, MAX_OUTPUTS_OUTCOMES_BATCH as u64 + 1),
1229 range(u64::MAX, 0),
1232 range(5, 4),
1233 ] {
1234 assert!(
1235 checked_outputs_outcomes_count(rejected).is_err(),
1236 "{rejected:?} must be rejected"
1237 );
1238 }
1239 }
1240
1241 #[tokio::test]
1242 async fn admin_metadata_update_preserves_persisted_iroh_endpoint() {
1243 let db: Database = MemDatabase::new().into_database();
1244 let identity = PeerId::from(0);
1245 let broadcast_secret_key =
1246 secp256k1::SecretKey::from_slice(&[42; 32]).expect("valid test key");
1247 let ctx = secp256k1::Secp256k1::new();
1248
1249 let existing = GuardianMetadata::new(
1250 vec!["wss://old.example".parse().expect("valid URL")],
1251 "old-pkarr".to_owned(),
1252 1,
1253 )
1254 .with_iroh_next_endpoint("persisted-iroh-id".to_owned())
1255 .sign(&ctx, &broadcast_secret_key.keypair(&ctx));
1256 let mut dbtx = db.begin_transaction().await;
1257 dbtx.insert_entry(&GuardianMetadataKey(identity), &existing)
1258 .await;
1259 dbtx.commit_tx().await;
1260
1261 let updated = GuardianMetadata::new(
1262 vec!["wss://new.example".parse().expect("valid URL")],
1263 "new-pkarr".to_owned(),
1264 2,
1265 );
1266 let signed = sign_guardian_metadata_preserving_iroh_endpoint(
1267 &db,
1268 identity,
1269 &broadcast_secret_key,
1270 updated,
1271 )
1272 .await;
1273
1274 assert_eq!(
1275 signed.guardian_metadata().iroh_next_endpoint.as_deref(),
1276 Some("persisted-iroh-id")
1277 );
1278 assert_eq!(
1279 signed.guardian_metadata().api_urls,
1280 vec!["wss://new.example".parse().expect("valid URL")]
1281 );
1282 assert_eq!(signed.guardian_metadata().pkarr_id_z32, "new-pkarr");
1283 assert_eq!(
1284 db.begin_transaction_nc()
1285 .await
1286 .get_value(&GuardianMetadataKey(identity))
1287 .await
1288 .expect("metadata was persisted")
1289 .tagged_hash(),
1290 signed.tagged_hash()
1291 );
1292 }
1293}