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