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