Skip to main content

fedimint_client/
db.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::time::SystemTime;
3
4use anyhow::{anyhow, bail};
5use bitcoin::hex::DisplayHex as _;
6use fedimint_api_client::api::ApiVersionSet;
7use fedimint_client_module::db::ClientModuleMigrationFn;
8use fedimint_client_module::module::recovery::RecoveryProgress;
9use fedimint_client_module::oplog::{JsonStringed, OperationLogEntry, OperationOutcome};
10use fedimint_client_module::sm::{ActiveStateMeta, InactiveStateMeta};
11use fedimint_core::config::{ClientConfig, ClientConfigV0, FederationId, GlobalClientConfig};
12use fedimint_core::core::{ModuleInstanceId, OperationId};
13use fedimint_core::db::{
14    Database, DatabaseTransaction, DatabaseVersion, DatabaseVersionKey,
15    IDatabaseTransactionOpsCore, IDatabaseTransactionOpsCoreTyped, MODULE_GLOBAL_PREFIX,
16    apply_migrations_dbtx, create_database_version_dbtx, get_current_database_version,
17};
18use fedimint_core::encoding::{Decodable, Encodable};
19use fedimint_core::module::registry::ModuleRegistry;
20use fedimint_core::module::{Amounts, SupportedApiVersionsSummary};
21use fedimint_core::{ChainId, PeerId, TransactionId, impl_db_lookup, impl_db_record};
22use fedimint_eventlog::{
23    DB_KEY_PREFIX_EVENT_LOG, DB_KEY_PREFIX_UNORDERED_EVENT_LOG, EventLogId, UnordedEventLogId,
24};
25use fedimint_logging::LOG_CLIENT_DB;
26use futures::StreamExt;
27use serde::{Deserialize, Serialize};
28use strum::IntoEnumIterator as _;
29use strum_macros::EnumIter;
30use tracing::{debug, info, trace, warn};
31
32use crate::backup::{ClientBackup, Metadata};
33use crate::sm::executor::{
34    ActiveStateKeyBytes, ActiveStateKeyPrefixBytes, ExecutorDbPrefixes, InactiveStateKeyBytes,
35    InactiveStateKeyPrefixBytes,
36};
37
38#[repr(u8)]
39#[derive(Clone, EnumIter, Debug)]
40pub enum DbKeyPrefix {
41    EncodedClientSecret = 0x28,
42    ClientSecret = 0x29, // Unused
43    ClientPreRootSecretHash = 0x2a,
44    OperationLog = 0x2c,
45    ChronologicalOperationLog = 0x2d,
46    CommonApiVersionCache = 0x2e,
47    ClientConfig = 0x2f,
48    PendingClientConfig = 0x3b,
49    ClientInviteCode = 0x30, // Unused; clean out remnant data before re-using!
50    ClientInitState = 0x31,
51    ClientMetadata = 0x32,
52    ClientLastBackup = 0x33,
53    ClientMetaField = 0x34,
54    ClientMetaServiceInfo = 0x35,
55    ApiSecret = 0x36,
56    PeerLastApiVersionsSummaryCache = 0x37,
57    ApiUrlAnnouncement = 0x38,
58    EventLog = fedimint_eventlog::DB_KEY_PREFIX_EVENT_LOG,
59    UnorderedEventLog = fedimint_eventlog::DB_KEY_PREFIX_UNORDERED_EVENT_LOG,
60    EventLogTrimable = fedimint_eventlog::DB_KEY_PREFIX_EVENT_LOG_TRIMABLE,
61    ChainId = 0x3c,
62    ClientModuleRecovery = 0x40,
63    GuardianMetadata = 0x42,
64    TransactionFees = 0x43,
65
66    DatabaseVersion = fedimint_core::db::DbKeyPrefix::DatabaseVersion as u8,
67    ClientBackup = fedimint_core::db::DbKeyPrefix::ClientBackup as u8,
68
69    ActiveStates = ExecutorDbPrefixes::ActiveStates as u8,
70    InactiveStates = ExecutorDbPrefixes::InactiveStates as u8,
71
72    /// Arbitrary data of the applications integrating Fedimint client and
73    /// wanting to store some Federation-specific data in Fedimint client
74    /// database.
75    ///
76    /// New users are encouraged to use this single prefix only.
77    //
78    // TODO: https://github.com/fedimint/fedimint/issues/4444
79    //       in the future, we should make all global access to the db private
80    //       and only expose a getter returning isolated database.
81    UserData = 0xb0,
82    /// Prefixes between 0xb1..=0xcf shall all be considered allocated for
83    /// historical and future external use
84    ExternalReservedStart = 0xb1,
85    /// Prefixes between 0xb1..=0xcf shall all be considered allocated for
86    /// historical and future external use
87    ExternalReservedEnd = 0xcf,
88    /// 0xd0.. reserved for Fedimint internal use
89    // (see [`DbKeyPrefixInternalReserved`] for *internal* details)
90    InternalReservedStart = 0xd0,
91    /// Per-module instance data
92    ModuleGlobalPrefix = 0xff,
93}
94
95#[repr(u8)]
96#[derive(Clone, EnumIter, Debug)]
97pub(crate) enum DbKeyPrefixInternalReserved {
98    /// [`crate::Client::built_in_application_event_log_tracker`]
99    DefaultApplicationEventLogPos = 0xd0,
100}
101
102pub(crate) async fn verify_client_db_integrity_dbtx(dbtx: &mut DatabaseTransaction<'_>) {
103    let prefixes: BTreeSet<u8> = DbKeyPrefix::iter().map(|prefix| prefix as u8).collect();
104
105    let mut records = dbtx.raw_find_by_prefix(&[]).await.expect("DB fail");
106    while let Some((k, v)) = records.next().await {
107        // from here and above, we don't want to spend time verifying it
108        if DbKeyPrefix::UserData as u8 <= k[0] {
109            break;
110        }
111
112        assert!(
113            prefixes.contains(&k[0]),
114            "Unexpected client db record found: {}: {}",
115            k.as_hex(),
116            v.as_hex()
117        );
118    }
119}
120
121impl std::fmt::Display for DbKeyPrefix {
122    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
123        write!(f, "{self:?}")
124    }
125}
126
127#[derive(Debug, Encodable, Decodable)]
128pub struct EncodedClientSecretKey;
129
130#[derive(Debug, Encodable, Decodable)]
131pub struct EncodedClientSecretKeyPrefix;
132
133impl_db_record!(
134    key = EncodedClientSecretKey,
135    value = Vec<u8>,
136    db_prefix = DbKeyPrefix::EncodedClientSecret,
137);
138impl_db_lookup!(
139    key = EncodedClientSecretKey,
140    query_prefix = EncodedClientSecretKeyPrefix
141);
142
143#[derive(Debug, Encodable, Decodable, Serialize)]
144pub struct OperationLogKey {
145    pub operation_id: OperationId,
146}
147
148impl_db_record!(
149    key = OperationLogKey,
150    value = OperationLogEntry,
151    db_prefix = DbKeyPrefix::OperationLog
152);
153
154#[derive(Debug, Encodable)]
155pub struct OperationLogKeyPrefix;
156
157impl_db_lookup!(key = OperationLogKey, query_prefix = OperationLogKeyPrefix);
158
159#[derive(Debug, Encodable, Decodable, Serialize)]
160pub struct OperationLogKeyV0 {
161    pub operation_id: OperationId,
162}
163
164#[derive(Debug, Encodable)]
165pub struct OperationLogKeyPrefixV0;
166
167impl_db_record!(
168    key = OperationLogKeyV0,
169    value = OperationLogEntryV0,
170    db_prefix = DbKeyPrefix::OperationLog
171);
172
173impl_db_lookup!(
174    key = OperationLogKeyV0,
175    query_prefix = OperationLogKeyPrefixV0
176);
177
178#[derive(Debug, Encodable, Decodable, Serialize)]
179pub struct ClientPreRootSecretHashKey;
180
181impl_db_record!(
182    key = ClientPreRootSecretHashKey,
183    value = [u8; 8],
184    db_prefix = DbKeyPrefix::ClientPreRootSecretHash
185);
186
187/// Key used to lookup operation log entries in chronological order
188#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
189pub struct ChronologicalOperationLogKey {
190    pub creation_time: std::time::SystemTime,
191    pub operation_id: OperationId,
192}
193
194#[derive(Debug, Encodable)]
195pub struct ChronologicalOperationLogKeyPrefix;
196
197impl_db_record!(
198    key = ChronologicalOperationLogKey,
199    value = (),
200    db_prefix = DbKeyPrefix::ChronologicalOperationLog
201);
202
203impl_db_lookup!(
204    key = ChronologicalOperationLogKey,
205    query_prefix = ChronologicalOperationLogKeyPrefix
206);
207
208#[derive(Debug, Encodable, Decodable)]
209pub struct CachedApiVersionSetKey;
210
211#[derive(Debug, Encodable, Decodable)]
212pub struct CachedApiVersionSet(pub ApiVersionSet);
213
214impl_db_record!(
215    key = CachedApiVersionSetKey,
216    value = CachedApiVersionSet,
217    db_prefix = DbKeyPrefix::CommonApiVersionCache
218);
219
220#[derive(Debug, Encodable, Decodable)]
221pub struct PeerLastApiVersionsSummaryKey(pub PeerId);
222
223#[derive(Debug, Encodable, Decodable)]
224pub struct PeerLastApiVersionsSummary(pub SupportedApiVersionsSummary);
225
226impl_db_record!(
227    key = PeerLastApiVersionsSummaryKey,
228    value = PeerLastApiVersionsSummary,
229    db_prefix = DbKeyPrefix::PeerLastApiVersionsSummaryCache
230);
231
232#[derive(Debug, Encodable, Decodable, Serialize)]
233pub struct ClientConfigKey;
234
235impl_db_record!(
236    key = ClientConfigKey,
237    value = ClientConfig,
238    db_prefix = DbKeyPrefix::ClientConfig
239);
240
241#[derive(Debug, Encodable, Decodable, Serialize)]
242pub struct PendingClientConfigKey;
243
244impl_db_record!(
245    key = PendingClientConfigKey,
246    value = ClientConfig,
247    db_prefix = DbKeyPrefix::PendingClientConfig
248);
249
250#[derive(Debug, Encodable, Decodable, Serialize)]
251pub struct ClientConfigKeyV0 {
252    pub id: FederationId,
253}
254
255#[derive(Debug, Encodable)]
256pub struct ClientConfigKeyPrefixV0;
257
258impl_db_record!(
259    key = ClientConfigKeyV0,
260    value = ClientConfigV0,
261    db_prefix = DbKeyPrefix::ClientConfig
262);
263
264impl_db_lookup!(
265    key = ClientConfigKeyV0,
266    query_prefix = ClientConfigKeyPrefixV0
267);
268
269#[derive(Debug, Encodable, Decodable, Serialize)]
270pub struct ApiSecretKey;
271
272#[derive(Debug, Encodable)]
273pub struct ApiSecretKeyPrefix;
274
275impl_db_record!(
276    key = ApiSecretKey,
277    value = String,
278    db_prefix = DbKeyPrefix::ApiSecret
279);
280
281impl_db_lookup!(key = ApiSecretKey, query_prefix = ApiSecretKeyPrefix);
282
283/// Cached chain ID (bitcoin block hash at height 1) from the federation
284#[derive(Debug, Encodable, Decodable, Serialize)]
285pub struct ChainIdKey;
286
287impl_db_record!(
288    key = ChainIdKey,
289    value = ChainId,
290    db_prefix = DbKeyPrefix::ChainId
291);
292
293/// Fees paid per transaction, stored at finalization time
294#[derive(Debug, Encodable, Decodable)]
295pub struct TransactionFeesKey(pub TransactionId);
296
297impl_db_record!(
298    key = TransactionFeesKey,
299    value = Amounts,
300    db_prefix = DbKeyPrefix::TransactionFees,
301);
302
303/// Client metadata that will be stored/restored on backup&recovery
304#[derive(Debug, Encodable, Decodable, Serialize)]
305pub struct ClientMetadataKey;
306
307#[derive(Debug, Encodable)]
308pub struct ClientMetadataPrefix;
309
310impl_db_record!(
311    key = ClientMetadataKey,
312    value = Metadata,
313    db_prefix = DbKeyPrefix::ClientMetadata
314);
315
316impl_db_lookup!(key = ClientMetadataKey, query_prefix = ClientMetadataPrefix);
317
318/// Does the client modules need to run recovery before being usable?
319#[derive(Debug, Encodable, Decodable, Serialize)]
320pub struct ClientInitStateKey;
321
322#[derive(Debug, Encodable)]
323pub struct ClientInitStatePrefix;
324
325/// Client initialization mode
326#[derive(Debug, Encodable, Decodable)]
327pub enum InitMode {
328    /// Should only be used with freshly generated root secret
329    Fresh,
330    /// Should be used with root secrets provided by the user to recover a
331    /// (even if just possibly) already used secret.
332    Recover { snapshot: Option<ClientBackup> },
333}
334
335/// Like `InitMode`, but without no longer required data.
336///
337/// This is distinct from `InitMode` to prevent holding on to `snapshot`
338/// forever both for user's privacy and space use. In case user get hacked
339/// or phone gets stolen.
340#[derive(Debug, Encodable, Decodable)]
341pub enum InitModeComplete {
342    Fresh,
343    Recover,
344}
345
346/// The state of the client initialization
347#[derive(Debug, Encodable, Decodable)]
348pub enum InitState {
349    /// Client data initialization might still require some work (e.g. client
350    /// recovery)
351    Pending(InitMode),
352    /// Client initialization was complete
353    Complete(InitModeComplete),
354}
355
356impl InitState {
357    pub fn into_complete(self) -> Self {
358        match self {
359            InitState::Pending(p) => InitState::Complete(match p {
360                InitMode::Fresh => InitModeComplete::Fresh,
361                InitMode::Recover { .. } => InitModeComplete::Recover,
362            }),
363            InitState::Complete(t) => InitState::Complete(t),
364        }
365    }
366
367    pub fn does_require_recovery(&self) -> Option<Option<ClientBackup>> {
368        match self {
369            InitState::Pending(p) => match p {
370                InitMode::Fresh => None,
371                InitMode::Recover { snapshot } => Some(snapshot.clone()),
372            },
373            InitState::Complete(_) => None,
374        }
375    }
376
377    pub fn is_pending(&self) -> bool {
378        match self {
379            InitState::Pending(_) => true,
380            InitState::Complete(_) => false,
381        }
382    }
383}
384
385impl_db_record!(
386    key = ClientInitStateKey,
387    value = InitState,
388    db_prefix = DbKeyPrefix::ClientInitState
389);
390
391impl_db_lookup!(
392    key = ClientInitStateKey,
393    query_prefix = ClientInitStatePrefix
394);
395
396#[derive(Debug, Encodable, Decodable, Serialize)]
397pub struct ClientModuleRecovery {
398    pub module_instance_id: ModuleInstanceId,
399}
400
401#[derive(Debug, Clone, Encodable, Decodable)]
402pub struct ClientModuleRecoveryState {
403    pub progress: RecoveryProgress,
404}
405
406impl ClientModuleRecoveryState {
407    pub fn is_done(&self) -> bool {
408        self.progress.is_done()
409    }
410}
411
412impl_db_record!(
413    key = ClientModuleRecovery,
414    value = ClientModuleRecoveryState,
415    db_prefix = DbKeyPrefix::ClientModuleRecovery,
416);
417
418/// Old (incorrect) version of the [`ClientModuleRecoveryState`]
419/// that used the wrong prefix.
420///
421/// See <https://github.com/fedimint/fedimint/issues/7367>.
422///
423/// Used only for the migration.
424#[derive(Debug, Encodable, Decodable, Serialize)]
425pub struct ClientModuleRecoveryIncorrectDoNotUse {
426    pub module_instance_id: ModuleInstanceId,
427}
428
429impl_db_record!(
430    key = ClientModuleRecoveryIncorrectDoNotUse,
431    value = ClientModuleRecoveryState,
432    // This was wrong and we keep it wrong for a migration.
433    db_prefix = DbKeyPrefix::ClientInitState,
434);
435
436/// Last valid backup the client attempted to make
437///
438/// Can be used to find previous valid versions of
439/// module backup.
440#[derive(Debug, Encodable, Decodable)]
441pub struct LastBackupKey;
442
443impl_db_record!(
444    key = LastBackupKey,
445    value = ClientBackup,
446    db_prefix = DbKeyPrefix::ClientLastBackup
447);
448
449#[derive(Encodable, Decodable, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
450pub(crate) struct MetaFieldPrefix;
451
452#[derive(Encodable, Decodable, Debug)]
453pub struct MetaServiceInfoKey;
454
455#[derive(Encodable, Decodable, Debug)]
456pub struct MetaServiceInfo {
457    pub last_updated: SystemTime,
458    pub revision: u64,
459}
460
461#[derive(
462    Encodable, Decodable, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize,
463)]
464pub(crate) struct MetaFieldKey(pub fedimint_client_module::meta::MetaFieldKey);
465
466#[derive(Encodable, Decodable, Debug, Clone, Serialize, Deserialize)]
467pub(crate) struct MetaFieldValue(pub fedimint_client_module::meta::MetaFieldValue);
468
469impl_db_record!(
470    key = MetaFieldKey,
471    value = MetaFieldValue,
472    db_prefix = DbKeyPrefix::ClientMetaField
473);
474
475impl_db_record!(
476    key = MetaServiceInfoKey,
477    value = MetaServiceInfo,
478    db_prefix = DbKeyPrefix::ClientMetaServiceInfo
479);
480
481impl_db_lookup!(key = MetaFieldKey, query_prefix = MetaFieldPrefix);
482
483pub fn get_core_client_database_migrations()
484-> BTreeMap<DatabaseVersion, fedimint_core::db::ClientCoreDbMigrationFn> {
485    let mut migrations: BTreeMap<DatabaseVersion, fedimint_core::db::ClientCoreDbMigrationFn> =
486        BTreeMap::new();
487    migrations.insert(
488        DatabaseVersion(0),
489        Box::new(|mut ctx| {
490            Box::pin(async move {
491                let mut dbtx = ctx.dbtx();
492
493                let config_v0 = dbtx
494                    .find_by_prefix(&ClientConfigKeyPrefixV0)
495                    .await
496                    .collect::<Vec<_>>()
497                    .await;
498
499                assert!(config_v0.len() <= 1);
500                let Some((id, config_v0)) = config_v0.into_iter().next() else {
501                    return Ok(());
502                };
503
504                let global = GlobalClientConfig {
505                    api_endpoints: config_v0.global.api_endpoints,
506                    broadcast_public_keys: None,
507                    consensus_version: config_v0.global.consensus_version,
508                    meta: config_v0.global.meta,
509                };
510
511                let config = ClientConfig {
512                    global,
513                    modules: config_v0.modules,
514                };
515
516                dbtx.remove_entry(&id).await;
517                dbtx.insert_new_entry(&ClientConfigKey, &config).await;
518                Ok(())
519            })
520        }),
521    );
522
523    // Migration to add outcome_time to OperationLogEntry
524    migrations.insert(
525        DatabaseVersion(1),
526        Box::new(|mut ctx| {
527            Box::pin(async move {
528                let mut dbtx = ctx.dbtx();
529
530                // Read all OperationLogEntries using V0 format
531                let operation_logs = dbtx
532                    .find_by_prefix(&OperationLogKeyPrefixV0)
533                    .await
534                    .collect::<Vec<_>>()
535                    .await;
536
537                // Build a map from operation_id -> max_time of inactive state
538                let mut op_id_max_time = BTreeMap::new();
539
540                // Process inactive states
541                {
542                    let mut inactive_states_stream =
543                        dbtx.find_by_prefix(&InactiveStateKeyPrefixBytes).await;
544
545                    while let Some((state, meta)) = inactive_states_stream.next().await {
546                        let entry = op_id_max_time
547                            .entry(state.operation_id)
548                            .or_insert(meta.exited_at);
549                        *entry = (*entry).max(meta.exited_at);
550                    }
551                }
552                // Migrate each V0 operation log entry to the new format
553                for (op_key_v0, log_entry_v0) in operation_logs {
554                    let new_entry = OperationLogEntry::new(
555                        log_entry_v0.operation_module_kind,
556                        log_entry_v0.meta,
557                        log_entry_v0.outcome.map(|outcome| {
558                            OperationOutcome {
559                                outcome,
560                                // If we found state times, use the max, otherwise use
561                                // current time
562                                time: op_id_max_time
563                                    .get(&op_key_v0.operation_id)
564                                    .copied()
565                                    .unwrap_or_else(fedimint_core::time::now),
566                            }
567                        }),
568                    );
569
570                    dbtx.remove_entry(&op_key_v0).await;
571                    dbtx.insert_entry(
572                        &OperationLogKey {
573                            operation_id: op_key_v0.operation_id,
574                        },
575                        &new_entry,
576                    )
577                    .await;
578                }
579
580                Ok(())
581            })
582        }),
583    );
584
585    // Fix #6948
586    migrations.insert(
587        DatabaseVersion(2),
588        Box::new(|mut ctx: fedimint_core::db::DbMigrationFnContext<'_, _>| {
589            Box::pin(async move {
590                let mut dbtx = ctx.dbtx();
591
592                // Migrate unordered keys that got written to ordered table
593                {
594                    let mut ordered_log_entries = dbtx
595                        .raw_find_by_prefix(&[DB_KEY_PREFIX_EVENT_LOG])
596                        .await
597                        .expect("DB operation failed");
598                    let mut keys_to_migrate = vec![];
599                    while let Some((k, _v)) = ordered_log_entries.next().await {
600                        trace!(target: LOG_CLIENT_DB,
601                            k=%k.as_hex(),
602                            "Checking ordered log key"
603                        );
604                        if EventLogId::consensus_decode_whole(&k[1..], &Default::default()).is_err()
605                        {
606                            assert!(
607                                UnordedEventLogId::consensus_decode_whole(
608                                    &k[1..],
609                                    &Default::default()
610                                )
611                                .is_ok()
612                            );
613                            keys_to_migrate.push(k);
614                        }
615                    }
616                    drop(ordered_log_entries);
617                    for mut key_to_migrate in keys_to_migrate {
618                        warn!(target: LOG_CLIENT_DB,
619                            k=%key_to_migrate.as_hex(),
620                            "Migrating unordered event log entry written to an ordered log"
621                        );
622                        let v = dbtx
623                            .raw_remove_entry(&key_to_migrate)
624                            .await
625                            .expect("DB operation failed")
626                            .expect("Was there a moment ago");
627                        assert_eq!(key_to_migrate[0], 0x39);
628                        key_to_migrate[0] = DB_KEY_PREFIX_UNORDERED_EVENT_LOG;
629                        assert_eq!(key_to_migrate[0], 0x3a);
630                        dbtx.raw_insert_bytes(&key_to_migrate, &v)
631                            .await
632                            .expect("DB operation failed");
633                    }
634                }
635
636                // Migrate ordered keys that got written to unordered table
637                {
638                    let mut unordered_log_entries = dbtx
639                        .raw_find_by_prefix(&[DB_KEY_PREFIX_UNORDERED_EVENT_LOG])
640                        .await
641                        .expect("DB operation failed");
642                    let mut keys_to_migrate = vec![];
643                    while let Some((k, _v)) = unordered_log_entries.next().await {
644                        trace!(target: LOG_CLIENT_DB,
645                            k=%k.as_hex(),
646                            "Checking unordered log key"
647                        );
648                        if UnordedEventLogId::consensus_decode_whole(&k[1..], &Default::default())
649                            .is_err()
650                        {
651                            assert!(
652                                EventLogId::consensus_decode_whole(&k[1..], &Default::default())
653                                    .is_ok()
654                            );
655                            keys_to_migrate.push(k);
656                        }
657                    }
658                    drop(unordered_log_entries);
659                    for mut key_to_migrate in keys_to_migrate {
660                        warn!(target: LOG_CLIENT_DB,
661                            k=%key_to_migrate.as_hex(),
662                            "Migrating ordered event log entry written to an unordered log"
663                        );
664                        let v = dbtx
665                            .raw_remove_entry(&key_to_migrate)
666                            .await
667                            .expect("DB operation failed")
668                            .expect("Was there a moment ago");
669                        assert_eq!(key_to_migrate[0], 0x3a);
670                        key_to_migrate[0] = DB_KEY_PREFIX_EVENT_LOG;
671                        assert_eq!(key_to_migrate[0], 0x39);
672                        dbtx.raw_insert_bytes(&key_to_migrate, &v)
673                            .await
674                            .expect("DB operation failed");
675                    }
676                }
677                Ok(())
678            })
679        }),
680    );
681
682    // Fix #7367
683    migrations.insert(
684        DatabaseVersion(3),
685        Box::new(|mut ctx: fedimint_core::db::DbMigrationFnContext<'_, _>| {
686            Box::pin(async move {
687                let mut dbtx = ctx.dbtx();
688
689                for module_id in 0..u16::MAX {
690                    let old_key = ClientModuleRecoveryIncorrectDoNotUse {
691                        module_instance_id: module_id,
692                    };
693                    let new_key = ClientModuleRecovery {
694                        module_instance_id: module_id,
695                    };
696                    let Some(value) = dbtx.get_value(&old_key).await else {
697                        debug!(target: LOG_CLIENT_DB, %module_id, "No more ClientModuleRecovery keys found for migartion");
698                        break;
699                    };
700
701                    debug!(target: LOG_CLIENT_DB, %module_id, "Migrating old ClientModuleRecovery key");
702                    dbtx.remove_entry(&old_key).await.expect("Is there.");
703                    assert!(dbtx.insert_entry(&new_key, &value).await.is_none());
704                }
705
706                Ok(())
707            })
708        }),
709    );
710    migrations
711}
712
713/// Apply core client database migrations
714///
715/// TODO: This should be private.
716pub async fn apply_migrations_core_client_dbtx(
717    dbtx: &mut DatabaseTransaction<'_>,
718    kind: String,
719) -> Result<(), anyhow::Error> {
720    apply_migrations_dbtx(
721        dbtx,
722        (),
723        kind,
724        get_core_client_database_migrations(),
725        None,
726        Some(DbKeyPrefix::UserData as u8),
727    )
728    .await
729}
730
731/// `apply_migrations_client` iterates from the on disk database version for the
732/// client module up to `target_db_version` and executes all of the migrations
733/// that exist in the migrations map, including state machine migrations.
734/// Each migration in the migrations map updates the database to have the
735/// correct on-disk data structures that the code is expecting. The entire
736/// process is atomic, (i.e migration from 0->1 and 1->2 happen atomically).
737/// This function is called before the module is initialized and as long as the
738/// correct migrations are supplied in the migrations map, the module
739/// will be able to read and write from the database successfully.
740pub async fn apply_migrations_client_module(
741    db: &Database,
742    kind: String,
743    migrations: BTreeMap<DatabaseVersion, ClientModuleMigrationFn>,
744    module_instance_id: ModuleInstanceId,
745) -> Result<(), anyhow::Error> {
746    let mut dbtx = db.begin_transaction().await;
747    apply_migrations_client_module_dbtx(
748        &mut dbtx.to_ref_nc(),
749        kind,
750        migrations,
751        module_instance_id,
752    )
753    .await?;
754    dbtx.commit_tx_result()
755        .await
756        .map_err(|e| anyhow::Error::msg(e.to_string()))
757}
758
759pub async fn apply_migrations_client_module_dbtx(
760    dbtx: &mut DatabaseTransaction<'_>,
761    kind: String,
762    migrations: BTreeMap<DatabaseVersion, ClientModuleMigrationFn>,
763    module_instance_id: ModuleInstanceId,
764) -> Result<(), anyhow::Error> {
765    // Newly created databases will not have any data underneath the
766    // `MODULE_GLOBAL_PREFIX` since they have just been instantiated.
767    let is_new_db = dbtx
768        .raw_find_by_prefix(&[MODULE_GLOBAL_PREFIX])
769        .await?
770        .next()
771        .await
772        .is_none();
773
774    let target_version = get_current_database_version(&migrations);
775
776    // First write the database version to disk if it does not exist.
777    create_database_version_dbtx(
778        dbtx,
779        target_version,
780        Some(module_instance_id),
781        kind.clone(),
782        is_new_db,
783    )
784    .await?;
785
786    let current_version = dbtx
787        .get_value(&DatabaseVersionKey(module_instance_id))
788        .await;
789
790    let db_version = if let Some(mut current_version) = current_version {
791        if current_version == target_version {
792            trace!(
793                target: LOG_CLIENT_DB,
794                %current_version,
795                %target_version,
796                module_instance_id,
797                kind,
798                "Database version up to date"
799            );
800            return Ok(());
801        }
802
803        if target_version < current_version {
804            return Err(anyhow!(format!(
805                "On disk database version for module {kind} was higher ({}) than the target database version ({}).",
806                current_version, target_version,
807            )));
808        }
809
810        info!(
811            target: LOG_CLIENT_DB,
812            %current_version,
813            %target_version,
814            module_instance_id,
815            kind,
816            "Migrating client module database"
817        );
818        let mut active_states = get_active_states(&mut dbtx.to_ref_nc(), module_instance_id).await;
819        let mut inactive_states =
820            get_inactive_states(&mut dbtx.to_ref_nc(), module_instance_id).await;
821
822        while current_version < target_version {
823            let new_states = if let Some(migration) = migrations.get(&current_version) {
824                debug!(
825                     target: LOG_CLIENT_DB,
826                     module_instance_id,
827                     %kind,
828                     %current_version,
829                     %target_version,
830                     "Running module db migration");
831
832                migration(
833                    &mut dbtx
834                        .to_ref_with_prefix_module_id(module_instance_id)
835                        .0
836                        .into_nc(),
837                    active_states.clone(),
838                    inactive_states.clone(),
839                )
840                .await?
841            } else {
842                warn!(
843                    target: LOG_CLIENT_DB,
844                    ?current_version, "Missing client db migration");
845                None
846            };
847
848            // If the client migration returned new states, a state machine migration has
849            // occurred, and the new states need to be persisted to the database.
850            if let Some((new_active_states, new_inactive_states)) = new_states {
851                remove_old_and_persist_new_active_states(
852                    &mut dbtx.to_ref_nc(),
853                    new_active_states.clone(),
854                    active_states.clone(),
855                    module_instance_id,
856                )
857                .await;
858                remove_old_and_persist_new_inactive_states(
859                    &mut dbtx.to_ref_nc(),
860                    new_inactive_states.clone(),
861                    inactive_states.clone(),
862                    module_instance_id,
863                )
864                .await;
865
866                // the new states become the old states for the next migration
867                active_states = new_active_states;
868                inactive_states = new_inactive_states;
869            }
870
871            current_version = current_version.increment();
872            dbtx.insert_entry(&DatabaseVersionKey(module_instance_id), &current_version)
873                .await;
874        }
875
876        current_version
877    } else {
878        target_version
879    };
880
881    debug!(
882        target: LOG_CLIENT_DB,
883        ?kind, ?db_version, "Client DB Version");
884    Ok(())
885}
886
887/// Reads all active states from the database and returns `Vec<DynState>`.
888/// TODO: It is unfortunate that we can't read states by the module's instance
889/// id so we are forced to return all active states. Once we do a db migration
890/// to add `module_instance_id` to `ActiveStateKey`, this can be improved to
891/// only read the module's relevant states.
892pub async fn get_active_states(
893    dbtx: &mut DatabaseTransaction<'_>,
894    module_instance_id: ModuleInstanceId,
895) -> Vec<(Vec<u8>, OperationId)> {
896    dbtx.find_by_prefix(&ActiveStateKeyPrefixBytes)
897        .await
898        .filter_map(|(state, _)| async move {
899            if module_instance_id == state.module_instance_id {
900                Some((state.state, state.operation_id))
901            } else {
902                None
903            }
904        })
905        .collect::<Vec<_>>()
906        .await
907}
908
909/// Reads all inactive states from the database and returns `Vec<DynState>`.
910/// TODO: It is unfortunate that we can't read states by the module's instance
911/// id so we are forced to return all inactive states. Once we do a db migration
912/// to add `module_instance_id` to `InactiveStateKey`, this can be improved to
913/// only read the module's relevant states.
914pub async fn get_inactive_states(
915    dbtx: &mut DatabaseTransaction<'_>,
916    module_instance_id: ModuleInstanceId,
917) -> Vec<(Vec<u8>, OperationId)> {
918    dbtx.find_by_prefix(&InactiveStateKeyPrefixBytes)
919        .await
920        .filter_map(|(state, _)| async move {
921            if module_instance_id == state.module_instance_id {
922                Some((state.state, state.operation_id))
923            } else {
924                None
925            }
926        })
927        .collect::<Vec<_>>()
928        .await
929}
930
931/// Persists new active states by first removing all current active states, and
932/// re-writing with the new set of active states. `new_active_states` is
933/// expected to contain all active states, not just the newly created states.
934pub async fn remove_old_and_persist_new_active_states(
935    dbtx: &mut DatabaseTransaction<'_>,
936    new_active_states: Vec<(Vec<u8>, OperationId)>,
937    states_to_remove: Vec<(Vec<u8>, OperationId)>,
938    module_instance_id: ModuleInstanceId,
939) {
940    // Remove all existing active states
941    for (bytes, operation_id) in states_to_remove {
942        dbtx.remove_entry(&ActiveStateKeyBytes {
943            operation_id,
944            module_instance_id,
945            state: bytes,
946        })
947        .await
948        .expect("Did not delete anything");
949    }
950
951    // Insert new "migrated" active states
952    for (bytes, operation_id) in new_active_states {
953        dbtx.insert_new_entry(
954            &ActiveStateKeyBytes {
955                operation_id,
956                module_instance_id,
957                state: bytes,
958            },
959            &ActiveStateMeta::default(),
960        )
961        .await;
962    }
963}
964
965/// Persists new inactive states by first removing all current inactive states,
966/// and re-writing with the new set of inactive states. `new_inactive_states` is
967/// expected to contain all inactive states, not just the newly created states.
968pub async fn remove_old_and_persist_new_inactive_states(
969    dbtx: &mut DatabaseTransaction<'_>,
970    new_inactive_states: Vec<(Vec<u8>, OperationId)>,
971    states_to_remove: Vec<(Vec<u8>, OperationId)>,
972    module_instance_id: ModuleInstanceId,
973) {
974    // Remove all existing active states
975    for (bytes, operation_id) in states_to_remove {
976        dbtx.remove_entry(&InactiveStateKeyBytes {
977            operation_id,
978            module_instance_id,
979            state: bytes,
980        })
981        .await
982        .expect("Did not delete anything");
983    }
984
985    // Insert new "migrated" inactive states
986    for (bytes, operation_id) in new_inactive_states {
987        dbtx.insert_new_entry(
988            &InactiveStateKeyBytes {
989                operation_id,
990                module_instance_id,
991                state: bytes,
992            },
993            &InactiveStateMeta {
994                created_at: fedimint_core::time::now(),
995                exited_at: fedimint_core::time::now(),
996            },
997        )
998        .await;
999    }
1000}
1001
1002/// Fetches the encoded client secret from the database and decodes it.
1003/// If an encoded client secret is not present in the database, or if
1004/// decoding fails, an error is returned.
1005pub async fn get_decoded_client_secret<T: Decodable>(db: &Database) -> anyhow::Result<T> {
1006    let mut tx = db.begin_transaction_nc().await;
1007    let client_secret = tx.get_value(&EncodedClientSecretKey).await;
1008
1009    match client_secret {
1010        Some(client_secret) => {
1011            T::consensus_decode_whole(&client_secret, &ModuleRegistry::default())
1012                .map_err(|e| anyhow!("Decoding failed: {e}"))
1013        }
1014        None => bail!("Encoded client secret not present in DB"),
1015    }
1016}
1017
1018/// V0 version of operation log entry for migration purposes
1019#[derive(Debug, Serialize, Deserialize, Encodable, Decodable)]
1020pub struct OperationLogEntryV0 {
1021    pub(crate) operation_module_kind: String,
1022    pub(crate) meta: JsonStringed,
1023    pub(crate) outcome: Option<JsonStringed>,
1024}