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#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Encodable, Decodable, Clone)]
34pub struct Metadata(Vec<u8>);
35
36impl Metadata {
37 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 pub fn is_empty(&self) -> bool {
52 self.0.is_empty()
53 }
54
55 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 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 pub fn to_json_value(&self) -> Result<serde_json::Value, serde_json::Error> {
69 serde_json::from_slice(&self.0)
70 }
71}
72
73#[derive(PartialEq, Eq, Debug, Clone)]
75pub struct ClientBackup {
76 pub session_count: u64,
85 pub metadata: Metadata,
87 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 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 pub const PER_MODULE_SIZE_LIMIT_BYTES: usize = 32 * 1024;
143
144 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 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 fn validate_and_fallback_module_backups(
174 self,
175 last_backup: Option<&ClientBackup>,
176 ) -> ClientBackup {
177 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#[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 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 #[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 #[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 #[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 #[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 #[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 responses.sort_by_key(|backup| Reverse(backup.session_count));
447
448 Ok(responses.into_iter().next())
449 }
450
451 #[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 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 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;