1use std::cmp::Reverse;
2use std::collections::{BTreeMap, BTreeSet};
3use std::io;
4use std::io::{Cursor, Write};
5
6use anyhow::{Context, Result, bail, ensure};
7use bitcoin::secp256k1::{Keypair, PublicKey, Secp256k1, SignOnly};
8use fedimint_api_client::api::DynGlobalApi;
9use fedimint_client_module::module::recovery::DynModuleBackup;
10use fedimint_core::core::ModuleInstanceId;
11use fedimint_core::core::backup::{
12 BACKUP_REQUEST_MAX_PAYLOAD_SIZE_BYTES, BackupRequest, SignedBackupRequest,
13};
14use fedimint_core::db::IDatabaseTransactionOpsCoreTyped;
15use fedimint_core::encoding::{Decodable, DecodeError, Encodable};
16use fedimint_core::module::registry::ModuleDecoderRegistry;
17use fedimint_core::module::serde_json;
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::secret::DeriveableSecretClientExt;
27
28#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Encodable, Decodable, Clone)]
33pub struct Metadata(Vec<u8>);
34
35impl Metadata {
36 pub fn empty() -> Self {
38 Self(vec![])
39 }
40
41 pub fn from_raw(bytes: Vec<u8>) -> Self {
42 Self(bytes)
43 }
44
45 pub fn into_raw(self) -> Vec<u8> {
46 self.0
47 }
48
49 pub fn is_empty(&self) -> bool {
51 self.0.is_empty()
52 }
53
54 pub fn from_json_serialized<T: Serialize>(val: T) -> Self {
56 Self(serde_json::to_vec(&val).expect("serializing to vec can't fail"))
57 }
58
59 pub fn to_json_deserialized<T: serde::de::DeserializeOwned>(&self) -> Result<T> {
61 Ok(serde_json::from_slice(&self.0)?)
62 }
63
64 pub fn to_json_value(&self) -> Result<serde_json::Value> {
66 Ok(serde_json::from_slice(&self.0)?)
67 }
68}
69
70#[derive(PartialEq, Eq, Debug, Clone)]
72pub struct ClientBackup {
73 pub session_count: u64,
82 pub metadata: Metadata,
84 pub modules: BTreeMap<ModuleInstanceId, DynModuleBackup>,
87}
88
89impl Encodable for ClientBackup {
90 fn consensus_encode<W: io::Write>(&self, writer: &mut W) -> std::result::Result<(), io::Error> {
91 self.session_count.consensus_encode(writer)?;
92 self.metadata.consensus_encode(writer)?;
93 self.modules.consensus_encode(writer)?;
94
95 Vec::<u8>::new().consensus_encode(writer)?;
107
108 Ok(())
109 }
110}
111
112impl Decodable for ClientBackup {
113 fn consensus_decode_partial<R: io::Read>(
114 r: &mut R,
115 modules: &ModuleDecoderRegistry,
116 ) -> std::result::Result<Self, DecodeError> {
117 let session_count = u64::consensus_decode_partial(r, modules).context("session_count")?;
118 let metadata = Metadata::consensus_decode_partial(r, modules).context("metadata")?;
119 let module_backups =
120 BTreeMap::<ModuleInstanceId, DynModuleBackup>::consensus_decode_partial(r, modules)
121 .context("module_backups")?;
122 let _padding = Vec::<u8>::consensus_decode_partial(r, modules).context("padding")?;
123
124 Ok(Self {
125 session_count,
126 metadata,
127 modules: module_backups,
128 })
129 }
130}
131
132impl ClientBackup {
133 pub const PADDING_ALIGNMENT: usize = 4 * 1024;
134
135 pub const PER_MODULE_SIZE_LIMIT_BYTES: usize = 32 * 1024;
140
141 fn get_alignment_size(len: usize) -> usize {
143 let padding_alignment = Self::PADDING_ALIGNMENT;
144 ((len.saturating_sub(1) / padding_alignment) + 1) * padding_alignment
145 }
146
147 pub fn encrypt_to(&self, key: &fedimint_aead::LessSafeKey) -> Result<EncryptedClientBackup> {
149 let mut encoded = Encodable::consensus_encode_to_vec(self);
150
151 let alignment_size = Self::get_alignment_size(encoded.len());
152 let padding_size = alignment_size - encoded.len();
153 encoded.write_all(&vec![0u8; padding_size])?;
154
155 let encrypted = fedimint_aead::encrypt(encoded, key)?;
156 Ok(EncryptedClientBackup(encrypted))
157 }
158
159 fn validate_and_fallback_module_backups(
165 self,
166 last_backup: Option<&ClientBackup>,
167 ) -> ClientBackup {
168 let all_ids: BTreeSet<_> = self
170 .modules
171 .keys()
172 .chain(last_backup.iter().flat_map(|b| b.modules.keys()))
173 .copied()
174 .collect();
175
176 let mut modules = BTreeMap::new();
177 for module_id in all_ids {
178 if let Some(module_backup) = self
179 .modules
180 .get(&module_id)
181 .or_else(|| last_backup.and_then(|lb| lb.modules.get(&module_id)))
182 {
183 let size = module_backup.consensus_encode_to_len();
184 let limit = Self::PER_MODULE_SIZE_LIMIT_BYTES;
185 if size < u64::try_from(limit).expect("Can't fail") {
186 modules.insert(module_id, module_backup.clone());
187 } else if let Some(last_module_backup) =
188 last_backup.and_then(|lb| lb.modules.get(&module_id))
189 {
190 let size_previous = last_module_backup.consensus_encode_to_len();
191 warn!(
192 target: LOG_CLIENT_BACKUP,
193 size,
194 limit,
195 %module_id,
196 size_previous,
197 "Module backup too large, will use previous version"
198 );
199 modules.insert(module_id, last_module_backup.clone());
200 } else {
201 warn!(
202 target: LOG_CLIENT_BACKUP,
203 size,
204 limit,
205 %module_id,
206 "Module backup too large, no previous version available to fall-back to"
207 );
208 }
209 }
210 }
211 ClientBackup {
212 session_count: self.session_count,
213 metadata: self.metadata,
214 modules,
215 }
216 }
217}
218
219#[derive(Clone)]
221pub struct EncryptedClientBackup(Vec<u8>);
222
223impl EncryptedClientBackup {
224 pub fn decrypt_with(
225 mut self,
226 key: &fedimint_aead::LessSafeKey,
227 decoders: &ModuleDecoderRegistry,
228 ) -> Result<ClientBackup> {
229 let decrypted = fedimint_aead::decrypt(&mut self.0, key)?;
230 let mut cursor = Cursor::new(decrypted);
231 let client_backup = ClientBackup::consensus_decode_partial(&mut cursor, decoders)?;
233 debug!(
234 target: LOG_CLIENT_BACKUP,
235 len = decrypted.len(),
236 padding = u64::try_from(decrypted.len()).expect("Can't fail") - cursor.position(),
237 "Decrypted client backup"
238 );
239 Ok(client_backup)
240 }
241
242 pub fn into_backup_request(self, keypair: &Keypair) -> Result<SignedBackupRequest> {
243 let request = BackupRequest {
244 id: keypair.public_key(),
245 timestamp: fedimint_core::time::now(),
246 payload: self.0,
247 };
248
249 request.sign(keypair)
250 }
251
252 pub fn len(&self) -> usize {
253 self.0.len()
254 }
255
256 #[must_use]
257 pub fn is_empty(&self) -> bool {
258 self.len() == 0
259 }
260}
261
262#[derive(Serialize, Deserialize)]
263pub struct EventBackupDone;
264
265impl Event for EventBackupDone {
266 const MODULE: Option<fedimint_core::core::ModuleKind> = None;
267
268 const KIND: EventKind = EventKind::from_static("backup-done");
269 const PERSISTENCE: EventPersistence = EventPersistence::Persistent;
270}
271
272impl Client {
273 #[deprecated(
275 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."
276 )]
277 pub async fn create_backup(&self, metadata: Metadata) -> anyhow::Result<ClientBackup> {
278 let session_count = self.api.session_count().await?;
279 let mut modules = BTreeMap::new();
280 for (id, kind, module) in self.modules.iter_modules() {
281 debug!(target: LOG_CLIENT_BACKUP, module_id=id, module_kind=%kind, "Preparing module backup");
282 if module.supports_backup() {
283 let backup = module.backup(id).await?;
284
285 debug!(target: LOG_CLIENT_BACKUP, module_id=id, module_kind=%kind, "Prepared module backup");
286 modules.insert(id, backup);
287 } else {
288 debug!(target: LOG_CLIENT_BACKUP, module_id=id, module_kind=%kind, "Module does not support backup");
289 }
290 }
291
292 Ok(ClientBackup {
293 session_count,
294 metadata,
295 modules,
296 })
297 }
298
299 async fn load_previous_backup(&self) -> Option<ClientBackup> {
300 let mut dbtx = self.db().begin_transaction_nc().await;
301 dbtx.get_value(&LastBackupKey).await
302 }
303
304 async fn store_last_backup(&self, backup: &ClientBackup) {
305 let mut dbtx = self.db().begin_transaction().await;
306 dbtx.insert_entry(&LastBackupKey, backup).await;
307 dbtx.commit_tx().await;
308 }
309
310 #[deprecated(
312 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."
313 )]
314 #[allow(deprecated)]
315 pub async fn backup_to_federation(&self, metadata: Metadata) -> Result<()> {
316 ensure!(
317 !self.has_pending_recoveries(),
318 "Cannot backup while there are pending recoveries"
319 );
320
321 let last_backup = self.load_previous_backup().await;
322 let new_backup = self.create_backup(metadata).await?;
323
324 let new_backup = new_backup.validate_and_fallback_module_backups(last_backup.as_ref());
325
326 let encrypted = new_backup.encrypt_to(&self.get_derived_backup_encryption_key())?;
327
328 self.validate_backup(&encrypted)?;
329
330 self.store_last_backup(&new_backup).await;
331
332 self.upload_backup(&encrypted).await?;
333
334 self.log_event(None, EventBackupDone).await;
335
336 Ok(())
337 }
338
339 #[deprecated(
341 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."
342 )]
343 pub fn validate_backup(&self, backup: &EncryptedClientBackup) -> Result<()> {
344 if BACKUP_REQUEST_MAX_PAYLOAD_SIZE_BYTES < backup.len() {
345 bail!("Backup payload too large");
346 }
347 Ok(())
348 }
349
350 #[deprecated(
352 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."
353 )]
354 #[allow(deprecated)]
355 pub async fn upload_backup(&self, backup: &EncryptedClientBackup) -> Result<()> {
356 self.validate_backup(backup)?;
357 let size = backup.len();
358 info!(
359 target: LOG_CLIENT_BACKUP,
360 size, "Uploading backup to federation"
361 );
362 let backup_request = backup
363 .clone()
364 .into_backup_request(&self.get_derived_backup_signing_key())?;
365 self.api.upload_backup(&backup_request).await?;
366 info!(
367 target: LOG_CLIENT_BACKUP,
368 size, "Uploaded backup to federation"
369 );
370 Ok(())
371 }
372
373 #[deprecated(
374 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."
375 )]
376 #[allow(deprecated)]
377 pub async fn download_backup_from_federation(&self) -> Result<Option<ClientBackup>> {
378 Self::download_backup_from_federation_static(
379 &self.api,
380 &self.root_secret(),
381 self.decoders(),
382 )
383 .await
384 }
385
386 #[deprecated(
388 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."
389 )]
390 #[allow(deprecated)]
391 pub async fn download_backup_from_federation_static(
392 api: &DynGlobalApi,
393 root_secret: &DerivableSecret,
394 decoders: &ModuleDecoderRegistry,
395 ) -> Result<Option<ClientBackup>> {
396 debug!(target: LOG_CLIENT, "Downloading backup from the federation");
397 let mut responses: Vec<_> = api
398 .download_backup(&Client::get_backup_id_static(root_secret))
399 .await?
400 .into_iter()
401 .filter_map(|(peer, backup)| {
402 match EncryptedClientBackup(backup?.data).decrypt_with(
403 &Self::get_derived_backup_encryption_key_static(root_secret),
404 decoders,
405 ) {
406 Ok(valid) => Some(valid),
407 Err(e) => {
408 warn!(
409 target: LOG_CLIENT_RECOVERY,
410 "Invalid backup returned by {peer}: {e}"
411 );
412 None
413 }
414 }
415 })
416 .collect();
417
418 debug!(
419 target: LOG_CLIENT_RECOVERY,
420 "Received {} valid responses",
421 responses.len()
422 );
423 responses.sort_by_key(|backup| Reverse(backup.session_count));
425
426 Ok(responses.into_iter().next())
427 }
428
429 #[deprecated(
432 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."
433 )]
434 pub fn get_backup_id(&self) -> PublicKey {
435 self.get_derived_backup_signing_key().public_key()
436 }
437
438 #[deprecated(
439 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."
440 )]
441 pub fn get_backup_id_static(root_secret: &DerivableSecret) -> PublicKey {
442 Self::get_derived_backup_signing_key_static(root_secret).public_key()
443 }
444 fn get_derived_backup_encryption_key_static(
447 secret: &DerivableSecret,
448 ) -> fedimint_aead::LessSafeKey {
449 fedimint_aead::LessSafeKey::new(secret.derive_backup_secret().to_chacha20_poly1305_key())
450 }
451
452 fn get_derived_backup_signing_key_static(secret: &DerivableSecret) -> Keypair {
455 secret
456 .derive_backup_secret()
457 .to_secp_key(&Secp256k1::<SignOnly>::gen_new())
458 }
459
460 fn get_derived_backup_encryption_key(&self) -> fedimint_aead::LessSafeKey {
461 Self::get_derived_backup_encryption_key_static(&self.root_secret())
462 }
463
464 fn get_derived_backup_signing_key(&self) -> Keypair {
465 Self::get_derived_backup_signing_key_static(&self.root_secret())
466 }
467
468 pub async fn get_decoded_client_secret<T: Decodable>(&self) -> anyhow::Result<T> {
469 crate::db::get_decoded_client_secret::<T>(self.db()).await
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use std::collections::BTreeMap;
476
477 use anyhow::Result;
478 use fedimint_core::encoding::{Decodable, Encodable};
479 use fedimint_core::module::registry::ModuleRegistry;
480 use fedimint_derive_secret::DerivableSecret;
481
482 use crate::Client;
483 use crate::backup::{ClientBackup, Metadata};
484
485 #[test]
486 fn sanity_ecash_backup_align() {
487 assert_eq!(
488 ClientBackup::get_alignment_size(1),
489 ClientBackup::PADDING_ALIGNMENT
490 );
491 assert_eq!(
492 ClientBackup::get_alignment_size(ClientBackup::PADDING_ALIGNMENT),
493 ClientBackup::PADDING_ALIGNMENT
494 );
495 assert_eq!(
496 ClientBackup::get_alignment_size(ClientBackup::PADDING_ALIGNMENT + 1),
497 ClientBackup::PADDING_ALIGNMENT * 2
498 );
499 }
500
501 #[test]
502 fn sanity_ecash_backup_decode_encode() -> Result<()> {
503 let orig = ClientBackup {
504 session_count: 0,
505 metadata: Metadata::from_raw(vec![1, 2, 3]),
506 modules: BTreeMap::new(),
507 };
508
509 let encoded = orig.consensus_encode_to_vec();
510 assert_eq!(
511 orig,
512 ClientBackup::consensus_decode_whole(&encoded, &ModuleRegistry::default())?
513 );
514
515 Ok(())
516 }
517
518 #[test]
519 fn sanity_ecash_backup_encrypt_decrypt() -> Result<()> {
520 const ENCRYPTION_HEADER_LEN: usize = 28;
521
522 let orig = ClientBackup {
523 modules: BTreeMap::new(),
524 session_count: 1,
525 metadata: Metadata::from_raw(vec![1, 2, 3]),
526 };
527
528 let secret = DerivableSecret::new_root(&[1; 32], &[1, 32]);
529 let key = Client::get_derived_backup_encryption_key_static(&secret);
530
531 let empty_encrypted = fedimint_aead::encrypt(vec![], &key)?;
532 assert_eq!(empty_encrypted.len(), ENCRYPTION_HEADER_LEN);
533
534 let encrypted = orig.encrypt_to(&key)?;
535 assert_eq!(
536 encrypted.len(),
537 ClientBackup::PADDING_ALIGNMENT + ENCRYPTION_HEADER_LEN
538 );
539
540 let decrypted = encrypted.decrypt_with(&key, &ModuleRegistry::default())?;
541
542 assert_eq!(orig, decrypted);
543
544 Ok(())
545 }
546}