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