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 pub async fn create_backup(&self, metadata: Metadata) -> anyhow::Result<ClientBackup> {
275 let session_count = self.api.session_count().await?;
276 let mut modules = BTreeMap::new();
277 for (id, kind, module) in self.modules.iter_modules() {
278 debug!(target: LOG_CLIENT_BACKUP, module_id=id, module_kind=%kind, "Preparing module backup");
279 if module.supports_backup() {
280 let backup = module.backup(id).await?;
281
282 debug!(target: LOG_CLIENT_BACKUP, module_id=id, module_kind=%kind, "Prepared module backup");
283 modules.insert(id, backup);
284 } else {
285 debug!(target: LOG_CLIENT_BACKUP, module_id=id, module_kind=%kind, "Module does not support backup");
286 }
287 }
288
289 Ok(ClientBackup {
290 session_count,
291 metadata,
292 modules,
293 })
294 }
295
296 async fn load_previous_backup(&self) -> Option<ClientBackup> {
297 let mut dbtx = self.db().begin_transaction_nc().await;
298 dbtx.get_value(&LastBackupKey).await
299 }
300
301 async fn store_last_backup(&self, backup: &ClientBackup) {
302 let mut dbtx = self.db().begin_transaction().await;
303 dbtx.insert_entry(&LastBackupKey, backup).await;
304 dbtx.commit_tx().await;
305 }
306
307 pub async fn backup_to_federation(&self, metadata: Metadata) -> Result<()> {
309 ensure!(
310 !self.has_pending_recoveries(),
311 "Cannot backup while there are pending recoveries"
312 );
313
314 let last_backup = self.load_previous_backup().await;
315 let new_backup = self.create_backup(metadata).await?;
316
317 let new_backup = new_backup.validate_and_fallback_module_backups(last_backup.as_ref());
318
319 let encrypted = new_backup.encrypt_to(&self.get_derived_backup_encryption_key())?;
320
321 self.validate_backup(&encrypted)?;
322
323 self.store_last_backup(&new_backup).await;
324
325 self.upload_backup(&encrypted).await?;
326
327 self.log_event(None, EventBackupDone).await;
328
329 Ok(())
330 }
331
332 pub fn validate_backup(&self, backup: &EncryptedClientBackup) -> Result<()> {
334 if BACKUP_REQUEST_MAX_PAYLOAD_SIZE_BYTES < backup.len() {
335 bail!("Backup payload too large");
336 }
337 Ok(())
338 }
339
340 pub async fn upload_backup(&self, backup: &EncryptedClientBackup) -> Result<()> {
342 self.validate_backup(backup)?;
343 let size = backup.len();
344 info!(
345 target: LOG_CLIENT_BACKUP,
346 size, "Uploading backup to federation"
347 );
348 let backup_request = backup
349 .clone()
350 .into_backup_request(&self.get_derived_backup_signing_key())?;
351 self.api.upload_backup(&backup_request).await?;
352 info!(
353 target: LOG_CLIENT_BACKUP,
354 size, "Uploaded backup to federation"
355 );
356 Ok(())
357 }
358
359 pub async fn download_backup_from_federation(&self) -> Result<Option<ClientBackup>> {
360 Self::download_backup_from_federation_static(
361 &self.api,
362 &self.root_secret(),
363 self.decoders(),
364 )
365 .await
366 }
367
368 pub async fn download_backup_from_federation_static(
370 api: &DynGlobalApi,
371 root_secret: &DerivableSecret,
372 decoders: &ModuleDecoderRegistry,
373 ) -> Result<Option<ClientBackup>> {
374 debug!(target: LOG_CLIENT, "Downloading backup from the federation");
375 let mut responses: Vec<_> = api
376 .download_backup(&Client::get_backup_id_static(root_secret))
377 .await?
378 .into_iter()
379 .filter_map(|(peer, backup)| {
380 match EncryptedClientBackup(backup?.data).decrypt_with(
381 &Self::get_derived_backup_encryption_key_static(root_secret),
382 decoders,
383 ) {
384 Ok(valid) => Some(valid),
385 Err(e) => {
386 warn!(
387 target: LOG_CLIENT_RECOVERY,
388 "Invalid backup returned by {peer}: {e}"
389 );
390 None
391 }
392 }
393 })
394 .collect();
395
396 debug!(
397 target: LOG_CLIENT_RECOVERY,
398 "Received {} valid responses",
399 responses.len()
400 );
401 responses.sort_by_key(|backup| Reverse(backup.session_count));
403
404 Ok(responses.into_iter().next())
405 }
406
407 pub fn get_backup_id(&self) -> PublicKey {
410 self.get_derived_backup_signing_key().public_key()
411 }
412
413 pub fn get_backup_id_static(root_secret: &DerivableSecret) -> PublicKey {
414 Self::get_derived_backup_signing_key_static(root_secret).public_key()
415 }
416 fn get_derived_backup_encryption_key_static(
419 secret: &DerivableSecret,
420 ) -> fedimint_aead::LessSafeKey {
421 fedimint_aead::LessSafeKey::new(secret.derive_backup_secret().to_chacha20_poly1305_key())
422 }
423
424 fn get_derived_backup_signing_key_static(secret: &DerivableSecret) -> Keypair {
427 secret
428 .derive_backup_secret()
429 .to_secp_key(&Secp256k1::<SignOnly>::gen_new())
430 }
431
432 fn get_derived_backup_encryption_key(&self) -> fedimint_aead::LessSafeKey {
433 Self::get_derived_backup_encryption_key_static(&self.root_secret())
434 }
435
436 fn get_derived_backup_signing_key(&self) -> Keypair {
437 Self::get_derived_backup_signing_key_static(&self.root_secret())
438 }
439
440 pub async fn get_decoded_client_secret<T: Decodable>(&self) -> anyhow::Result<T> {
441 crate::db::get_decoded_client_secret::<T>(self.db()).await
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 use std::collections::BTreeMap;
448
449 use anyhow::Result;
450 use fedimint_core::encoding::{Decodable, Encodable};
451 use fedimint_core::module::registry::ModuleRegistry;
452 use fedimint_derive_secret::DerivableSecret;
453
454 use crate::Client;
455 use crate::backup::{ClientBackup, Metadata};
456
457 #[test]
458 fn sanity_ecash_backup_align() {
459 assert_eq!(
460 ClientBackup::get_alignment_size(1),
461 ClientBackup::PADDING_ALIGNMENT
462 );
463 assert_eq!(
464 ClientBackup::get_alignment_size(ClientBackup::PADDING_ALIGNMENT),
465 ClientBackup::PADDING_ALIGNMENT
466 );
467 assert_eq!(
468 ClientBackup::get_alignment_size(ClientBackup::PADDING_ALIGNMENT + 1),
469 ClientBackup::PADDING_ALIGNMENT * 2
470 );
471 }
472
473 #[test]
474 fn sanity_ecash_backup_decode_encode() -> Result<()> {
475 let orig = ClientBackup {
476 session_count: 0,
477 metadata: Metadata::from_raw(vec![1, 2, 3]),
478 modules: BTreeMap::new(),
479 };
480
481 let encoded = orig.consensus_encode_to_vec();
482 assert_eq!(
483 orig,
484 ClientBackup::consensus_decode_whole(&encoded, &ModuleRegistry::default())?
485 );
486
487 Ok(())
488 }
489
490 #[test]
491 fn sanity_ecash_backup_encrypt_decrypt() -> Result<()> {
492 const ENCRYPTION_HEADER_LEN: usize = 28;
493
494 let orig = ClientBackup {
495 modules: BTreeMap::new(),
496 session_count: 1,
497 metadata: Metadata::from_raw(vec![1, 2, 3]),
498 };
499
500 let secret = DerivableSecret::new_root(&[1; 32], &[1, 32]);
501 let key = Client::get_derived_backup_encryption_key_static(&secret);
502
503 let empty_encrypted = fedimint_aead::encrypt(vec![], &key)?;
504 assert_eq!(empty_encrypted.len(), ENCRYPTION_HEADER_LEN);
505
506 let encrypted = orig.encrypt_to(&key)?;
507 assert_eq!(
508 encrypted.len(),
509 ClientBackup::PADDING_ALIGNMENT + ENCRYPTION_HEADER_LEN
510 );
511
512 let decrypted = encrypted.decrypt_with(&key, &ModuleRegistry::default())?;
513
514 assert_eq!(orig, decrypted);
515
516 Ok(())
517 }
518}