Skip to main content

fedimint_client/
backup.rs

1use std::cmp::Reverse;
2use std::collections::{BTreeMap, BTreeSet};
3use std::io;
4use std::io::{Cursor, Write};
5
6use bitcoin::secp256k1::{Keypair, PublicKey, Secp256k1, SignOnly};
7use fedimint_api_client::api::{DynGlobalApi, FederationError};
8use fedimint_client_module::module::recovery::DynModuleBackup;
9use fedimint_core::core::ModuleInstanceId;
10use fedimint_core::core::backup::{
11    BACKUP_REQUEST_MAX_PAYLOAD_SIZE_BYTES, BackupRequest, SignedBackupRequest,
12};
13use fedimint_core::db::IDatabaseTransactionOpsCoreTyped;
14use fedimint_core::encoding::{Decodable, DecodeContext as _, DecodeError, Encodable};
15use fedimint_core::module::registry::ModuleDecoderRegistry;
16use fedimint_core::module::serde_json;
17use fedimint_core::util::FmtCompact as _;
18use fedimint_derive_secret::DerivableSecret;
19use fedimint_eventlog::{Event, EventKind, EventPersistence};
20use fedimint_logging::{LOG_CLIENT, LOG_CLIENT_BACKUP, LOG_CLIENT_RECOVERY};
21use serde::{Deserialize, Serialize};
22use tracing::{debug, info, warn};
23
24use super::Client;
25use crate::db::LastBackupKey;
26use crate::error::{BackupError, ClientSecretError};
27use crate::secret::DeriveableSecretClientExt;
28
29/// Backup metadata
30///
31/// A backup can have a blob of extra data encoded in it. We provide methods to
32/// use json encoding, but clients are free to use their own encoding.
33#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Encodable, Decodable, Clone)]
34pub struct Metadata(Vec<u8>);
35
36impl Metadata {
37    /// Create empty metadata
38    pub fn empty() -> Self {
39        Self(vec![])
40    }
41
42    pub fn from_raw(bytes: Vec<u8>) -> Self {
43        Self(bytes)
44    }
45
46    pub fn into_raw(self) -> Vec<u8> {
47        self.0
48    }
49
50    /// Is metadata empty
51    pub fn is_empty(&self) -> bool {
52        self.0.is_empty()
53    }
54
55    /// Create metadata as json from typed `val`
56    pub fn from_json_serialized<T: Serialize>(val: T) -> Self {
57        Self(serde_json::to_vec(&val).expect("serializing to vec can't fail"))
58    }
59
60    /// Attempt to deserialize metadata as typed json
61    pub fn to_json_deserialized<T: serde::de::DeserializeOwned>(
62        &self,
63    ) -> Result<T, serde_json::Error> {
64        serde_json::from_slice(&self.0)
65    }
66
67    /// Attempt to deserialize metadata as untyped json (`serde_json::Value`)
68    pub fn to_json_value(&self) -> Result<serde_json::Value, serde_json::Error> {
69        serde_json::from_slice(&self.0)
70    }
71}
72
73/// Client state backup
74#[derive(PartialEq, Eq, Debug, Clone)]
75pub struct ClientBackup {
76    /// Session count taken right before taking the backup
77    /// used to timestamp the backup file. Used for finding the
78    /// most recent backup from all available ones.
79    ///
80    /// Warning: Each particular module backup for each instance
81    /// in `Self::modules` could have been taken earlier than
82    /// that (e.g. older one used due to size limits), so modules
83    /// MUST maintain their own `session_count`s.
84    pub session_count: u64,
85    /// Application metadata
86    pub metadata: Metadata,
87    // TODO: remove redundant ModuleInstanceId
88    /// Module specific-backup (if supported)
89    pub modules: BTreeMap<ModuleInstanceId, DynModuleBackup>,
90}
91
92impl Encodable for ClientBackup {
93    fn consensus_encode<W: io::Write>(&self, writer: &mut W) -> std::result::Result<(), io::Error> {
94        self.session_count.consensus_encode(writer)?;
95        self.metadata.consensus_encode(writer)?;
96        self.modules.consensus_encode(writer)?;
97
98        // Old-style padding.
99        //
100        // Older version of the client used a custom zero-filled vec to
101        // pad `ClientBackup` to alignment-size here. This has a problem
102        // that the size of encoding of the padding can have different sizes
103        // (due to var-ints).
104        //
105        // Conceptually serialization is also a the wrong place to do padding
106        // anyway, so we moved padding to encrypt/decryption. But for abundance of
107        // caution, we'll keep the old padding around, and always fill it
108        // with an empty Vec.
109        Vec::<u8>::new().consensus_encode(writer)?;
110
111        Ok(())
112    }
113}
114
115impl Decodable for ClientBackup {
116    fn consensus_decode_partial<R: io::Read>(
117        r: &mut R,
118        modules: &ModuleDecoderRegistry,
119    ) -> std::result::Result<Self, DecodeError> {
120        let session_count = u64::consensus_decode_partial(r, modules).context("session_count")?;
121        let metadata = Metadata::consensus_decode_partial(r, modules).context("metadata")?;
122        let module_backups =
123            BTreeMap::<ModuleInstanceId, DynModuleBackup>::consensus_decode_partial(r, modules)
124                .context("module_backups")?;
125        let _padding = Vec::<u8>::consensus_decode_partial(r, modules).context("padding")?;
126
127        Ok(Self {
128            session_count,
129            metadata,
130            modules: module_backups,
131        })
132    }
133}
134
135impl ClientBackup {
136    pub const PADDING_ALIGNMENT: usize = 4 * 1024;
137
138    /// "32kiB is enough for any module backup" --dpc
139    ///
140    /// Federation storage is scarce, and since we can take older versions of
141    /// the backup, temporarily going over the limit is not a big problem.
142    pub const PER_MODULE_SIZE_LIMIT_BYTES: usize = 32 * 1024;
143
144    /// Align an encoded message size up for better privacy
145    fn get_alignment_size(len: usize) -> usize {
146        let padding_alignment = Self::PADDING_ALIGNMENT;
147        ((len.saturating_sub(1) / padding_alignment) + 1) * padding_alignment
148    }
149
150    /// Encrypt with a key and turn into [`EncryptedClientBackup`]
151    pub fn encrypt_to(
152        &self,
153        key: &fedimint_aead::LessSafeKey,
154    ) -> Result<EncryptedClientBackup, BackupError> {
155        let mut encoded = Encodable::consensus_encode_to_vec(self);
156
157        let alignment_size = Self::get_alignment_size(encoded.len());
158        let padding_size = alignment_size - encoded.len();
159        encoded
160            .write_all(&vec![0u8; padding_size])
161            .expect("Writing to a Vec cannot fail");
162
163        let encrypted = fedimint_aead::encrypt(encoded, key)
164            .map_err(|err| BackupError::Encryption(err.into()))?;
165        Ok(EncryptedClientBackup(encrypted))
166    }
167
168    /// Validate and fallback invalid parts of the backup
169    ///
170    /// Given the size constraints and possible 3rd party modules,
171    /// it seems to use older, but smaller versions of backups when
172    /// current ones do not fit (either globally or in per-module limit).
173    fn validate_and_fallback_module_backups(
174        self,
175        last_backup: Option<&ClientBackup>,
176    ) -> ClientBackup {
177        // take all module ids from both backup and add them together
178        let all_ids: BTreeSet<_> = self
179            .modules
180            .keys()
181            .chain(last_backup.iter().flat_map(|b| b.modules.keys()))
182            .copied()
183            .collect();
184
185        let mut modules = BTreeMap::new();
186        for module_id in all_ids {
187            if let Some(module_backup) = self
188                .modules
189                .get(&module_id)
190                .or_else(|| last_backup.and_then(|lb| lb.modules.get(&module_id)))
191            {
192                let size = module_backup.consensus_encode_to_len();
193                let limit = Self::PER_MODULE_SIZE_LIMIT_BYTES;
194                if size < u64::try_from(limit).expect("Can't fail") {
195                    modules.insert(module_id, module_backup.clone());
196                } else if let Some(last_module_backup) =
197                    last_backup.and_then(|lb| lb.modules.get(&module_id))
198                {
199                    let size_previous = last_module_backup.consensus_encode_to_len();
200                    warn!(
201                        target: LOG_CLIENT_BACKUP,
202                        size,
203                        limit,
204                        %module_id,
205                        size_previous,
206                        "Module backup too large, will use previous version"
207                    );
208                    modules.insert(module_id, last_module_backup.clone());
209                } else {
210                    warn!(
211                        target: LOG_CLIENT_BACKUP,
212                        size,
213                        limit,
214                        %module_id,
215                        "Module backup too large, no previous version available to fall-back to"
216                    );
217                }
218            }
219        }
220        ClientBackup {
221            session_count: self.session_count,
222            metadata: self.metadata,
223            modules,
224        }
225    }
226}
227
228/// Encrypted version of [`ClientBackup`].
229#[derive(Clone)]
230pub struct EncryptedClientBackup(Vec<u8>);
231
232impl EncryptedClientBackup {
233    pub fn decrypt_with(
234        mut self,
235        key: &fedimint_aead::LessSafeKey,
236        decoders: &ModuleDecoderRegistry,
237    ) -> Result<ClientBackup, BackupError> {
238        let decrypted = fedimint_aead::decrypt(&mut self.0, key)
239            .map_err(|err| BackupError::Encryption(err.into()))?;
240        let mut cursor = Cursor::new(decrypted);
241        // We specifically want to ignore the padding in the backup here.
242        let client_backup = ClientBackup::consensus_decode_partial(&mut cursor, decoders)?;
243        debug!(
244            target: LOG_CLIENT_BACKUP,
245            len = decrypted.len(),
246            padding = u64::try_from(decrypted.len()).expect("Can't fail") - cursor.position(),
247            "Decrypted client backup"
248        );
249        Ok(client_backup)
250    }
251
252    pub fn into_backup_request(self, keypair: &Keypair) -> SignedBackupRequest {
253        let request = BackupRequest {
254            id: keypair.public_key(),
255            timestamp: fedimint_core::time::now(),
256            payload: self.0,
257        };
258
259        request.sign(keypair)
260    }
261
262    pub fn len(&self) -> usize {
263        self.0.len()
264    }
265
266    #[must_use]
267    pub fn is_empty(&self) -> bool {
268        self.len() == 0
269    }
270}
271
272#[derive(Serialize, Deserialize)]
273pub struct EventBackupDone;
274
275impl Event for EventBackupDone {
276    const MODULE: Option<fedimint_core::core::ModuleKind> = None;
277
278    const KIND: EventKind = EventKind::from_static("backup-done");
279    const PERSISTENCE: EventPersistence = EventPersistence::Persistent;
280}
281
282impl Client {
283    /// Create a backup, include provided `metadata`
284    #[deprecated(
285        note = "Recovery is now efficient enough that backups are no longer necessary. Backups will be removed in v0.13.0 due to backups being inherently complicated and brittle."
286    )]
287    pub async fn create_backup(&self, metadata: Metadata) -> Result<ClientBackup, BackupError> {
288        let session_count = self.api.session_count().await?;
289        let mut modules = BTreeMap::new();
290        for (id, kind, module) in self.modules.iter_modules() {
291            debug!(target: LOG_CLIENT_BACKUP, module_id=id, module_kind=%kind, "Preparing module backup");
292            if module.supports_backup() {
293                let backup = module
294                    .backup(id)
295                    .await
296                    .map_err(|source| BackupError::Module {
297                        instance_id: id,
298                        source,
299                    })?;
300
301                debug!(target: LOG_CLIENT_BACKUP, module_id=id, module_kind=%kind, "Prepared module backup");
302                modules.insert(id, backup);
303            } else {
304                debug!(target: LOG_CLIENT_BACKUP, module_id=id, module_kind=%kind, "Module does not support backup");
305            }
306        }
307
308        Ok(ClientBackup {
309            session_count,
310            metadata,
311            modules,
312        })
313    }
314
315    async fn load_previous_backup(&self) -> Option<ClientBackup> {
316        let mut dbtx = self.db().begin_transaction_nc().await;
317        dbtx.get_value(&LastBackupKey).await
318    }
319
320    async fn store_last_backup(&self, backup: &ClientBackup) {
321        let mut dbtx = self.db().begin_transaction().await;
322        dbtx.insert_entry(&LastBackupKey, backup).await;
323        dbtx.commit_tx().await;
324    }
325
326    /// Prepare an encrypted backup and send it to federation for storing
327    #[deprecated(
328        note = "Recovery is now efficient enough that backups are no longer necessary. Backups will be removed in v0.13.0 due to backups being inherently complicated and brittle."
329    )]
330    #[allow(deprecated)]
331    pub async fn backup_to_federation(&self, metadata: Metadata) -> Result<(), BackupError> {
332        if self.has_pending_recoveries() {
333            return Err(BackupError::PendingRecoveries);
334        }
335
336        let last_backup = self.load_previous_backup().await;
337        let new_backup = self.create_backup(metadata).await?;
338
339        let new_backup = new_backup.validate_and_fallback_module_backups(last_backup.as_ref());
340
341        let encrypted = new_backup.encrypt_to(&self.get_derived_backup_encryption_key())?;
342
343        self.validate_backup(&encrypted)?;
344
345        self.store_last_backup(&new_backup).await;
346
347        self.upload_backup(&encrypted).await?;
348
349        self.log_event(None, EventBackupDone).await;
350
351        Ok(())
352    }
353
354    /// Validate backup before sending it to federation
355    #[deprecated(
356        note = "Recovery is now efficient enough that backups are no longer necessary. Backups will be removed in v0.13.0 due to backups being inherently complicated and brittle."
357    )]
358    pub fn validate_backup(&self, backup: &EncryptedClientBackup) -> Result<(), BackupError> {
359        if BACKUP_REQUEST_MAX_PAYLOAD_SIZE_BYTES < backup.len() {
360            return Err(BackupError::TooLarge {
361                size: backup.len(),
362                max: BACKUP_REQUEST_MAX_PAYLOAD_SIZE_BYTES,
363            });
364        }
365        Ok(())
366    }
367
368    /// Upload `backup` to federation
369    #[deprecated(
370        note = "Recovery is now efficient enough that backups are no longer necessary. Backups will be removed in v0.13.0 due to backups being inherently complicated and brittle."
371    )]
372    #[allow(deprecated)]
373    pub async fn upload_backup(&self, backup: &EncryptedClientBackup) -> Result<(), BackupError> {
374        self.validate_backup(backup)?;
375        let size = backup.len();
376        info!(
377            target: LOG_CLIENT_BACKUP,
378            size, "Uploading backup to federation"
379        );
380        let backup_request = backup
381            .clone()
382            .into_backup_request(&self.get_derived_backup_signing_key());
383        self.api.upload_backup(&backup_request).await?;
384        info!(
385            target: LOG_CLIENT_BACKUP,
386            size, "Uploaded backup to federation"
387        );
388        Ok(())
389    }
390
391    #[deprecated(
392        note = "Recovery is now efficient enough that backups are no longer necessary. Backups will be removed in v0.13.0 due to backups being inherently complicated and brittle."
393    )]
394    #[allow(deprecated)]
395    pub async fn download_backup_from_federation(
396        &self,
397    ) -> Result<Option<ClientBackup>, FederationError> {
398        Self::download_backup_from_federation_static(
399            &self.api,
400            &self.root_secret(),
401            self.decoders(),
402        )
403        .await
404    }
405
406    /// Download most recent valid backup found from the Federation
407    #[deprecated(
408        note = "Recovery is now efficient enough that backups are no longer necessary. Backups will be removed in v0.13.0 due to backups being inherently complicated and brittle."
409    )]
410    #[allow(deprecated)]
411    pub async fn download_backup_from_federation_static(
412        api: &DynGlobalApi,
413        root_secret: &DerivableSecret,
414        decoders: &ModuleDecoderRegistry,
415    ) -> Result<Option<ClientBackup>, FederationError> {
416        debug!(target: LOG_CLIENT, "Downloading backup from the federation");
417        let mut responses: Vec<_> = api
418            .download_backup(&Client::get_backup_id_static(root_secret))
419            .await?
420            .into_iter()
421            .filter_map(|(peer, backup)| {
422                match EncryptedClientBackup(backup?.data).decrypt_with(
423                    &Self::get_derived_backup_encryption_key_static(root_secret),
424                    decoders,
425                ) {
426                    Ok(valid) => Some(valid),
427                    Err(e) => {
428                        warn!(
429                            target: LOG_CLIENT_RECOVERY,
430                            err = %e.fmt_compact(),
431                            %peer,
432                            "Invalid backup returned by peer"
433                        );
434                        None
435                    }
436                }
437            })
438            .collect();
439
440        debug!(
441            target: LOG_CLIENT_RECOVERY,
442            "Received {} valid responses",
443            responses.len()
444        );
445        // Use the newest (highest epoch)
446        responses.sort_by_key(|backup| Reverse(backup.session_count));
447
448        Ok(responses.into_iter().next())
449    }
450
451    /// Backup id derived from the root secret key (public key used to self-sign
452    /// backup requests)
453    #[deprecated(
454        note = "Recovery is now efficient enough that backups are no longer necessary. Backups will be removed in v0.13.0 due to backups being inherently complicated and brittle."
455    )]
456    pub fn get_backup_id(&self) -> PublicKey {
457        self.get_derived_backup_signing_key().public_key()
458    }
459
460    #[deprecated(
461        note = "Recovery is now efficient enough that backups are no longer necessary. Backups will be removed in v0.13.0 due to backups being inherently complicated and brittle."
462    )]
463    pub fn get_backup_id_static(root_secret: &DerivableSecret) -> PublicKey {
464        Self::get_derived_backup_signing_key_static(root_secret).public_key()
465    }
466    /// Static version of [`Self::get_derived_backup_encryption_key`] for
467    /// testing without creating whole `MintClient`
468    fn get_derived_backup_encryption_key_static(
469        secret: &DerivableSecret,
470    ) -> fedimint_aead::LessSafeKey {
471        fedimint_aead::LessSafeKey::new(secret.derive_backup_secret().to_chacha20_poly1305_key())
472    }
473
474    /// Static version of [`Self::get_derived_backup_signing_key`] for testing
475    /// without creating whole `MintClient`
476    fn get_derived_backup_signing_key_static(secret: &DerivableSecret) -> Keypair {
477        secret
478            .derive_backup_secret()
479            .to_secp_key(&Secp256k1::<SignOnly>::gen_new())
480    }
481
482    fn get_derived_backup_encryption_key(&self) -> fedimint_aead::LessSafeKey {
483        Self::get_derived_backup_encryption_key_static(&self.root_secret())
484    }
485
486    fn get_derived_backup_signing_key(&self) -> Keypair {
487        Self::get_derived_backup_signing_key_static(&self.root_secret())
488    }
489
490    pub async fn get_decoded_client_secret<T: Decodable>(&self) -> Result<T, ClientSecretError> {
491        crate::db::get_decoded_client_secret::<T>(self.db()).await
492    }
493}
494
495#[cfg(test)]
496mod tests;