fedimint_client/
backup.rs

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