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