Skip to main content

fedimint_client/
db.rs

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