1use std::cmp::Reverse;
2use std::collections::{BTreeMap, BTreeSet};
3use std::io::{Cursor, Error, Read, Write};
4
5use anyhow::{bail, ensure, Context, Result};
6use bitcoin::secp256k1::{Keypair, PublicKey, Secp256k1, SignOnly};
7use fedimint_api_client::api::DynGlobalApi;
8use fedimint_core::core::backup::{
9 BackupRequest, SignedBackupRequest, BACKUP_REQUEST_MAX_PAYLOAD_SIZE_BYTES,
10};
11use fedimint_core::core::ModuleInstanceId;
12use fedimint_core::db::IDatabaseTransactionOpsCoreTyped;
13use fedimint_core::encoding::{Decodable, DecodeError, Encodable};
14use fedimint_core::module::registry::ModuleDecoderRegistry;
15use fedimint_derive_secret::DerivableSecret;
16use fedimint_eventlog::{Event, EventKind};
17use fedimint_logging::{LOG_CLIENT, LOG_CLIENT_BACKUP, LOG_CLIENT_RECOVERY};
18use serde::{Deserialize, Serialize};
19use tracing::{debug, info, warn};
20
21use super::Client;
22use crate::db::LastBackupKey;
23use crate::get_decoded_client_secret;
24use crate::module::recovery::DynModuleBackup;
25use crate::secret::DeriveableSecretClientExt;
26
27#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Encodable, Decodable, Clone)]
32pub struct Metadata(Vec<u8>);
33
34impl Metadata {
35 pub fn empty() -> Self {
37 Self(vec![])
38 }
39
40 pub fn from_raw(bytes: Vec<u8>) -> Self {
41 Self(bytes)
42 }
43
44 pub fn into_raw(self) -> Vec<u8> {
45 self.0
46 }
47
48 pub fn is_empty(&self) -> bool {
50 self.0.is_empty()
51 }
52
53 pub fn from_json_serialized<T: Serialize>(val: T) -> Self {
55 Self(serde_json::to_vec(&val).expect("serializing to vec can't fail"))
56 }
57
58 pub fn to_json_deserialized<T: serde::de::DeserializeOwned>(&self) -> Result<T> {
60 Ok(serde_json::from_slice(&self.0)?)
61 }
62
63 pub fn to_json_value(&self) -> Result<serde_json::Value> {
65 Ok(serde_json::from_slice(&self.0)?)
66 }
67}
68
69#[derive(PartialEq, Eq, Debug, Clone)]
71pub struct ClientBackup {
72 pub session_count: u64,
81 pub metadata: Metadata,
83 pub modules: BTreeMap<ModuleInstanceId, DynModuleBackup>,
86}
87
88impl ClientBackup {
89 pub const PADDING_ALIGNMENT: usize = 4 * 1024;
90
91 pub const PER_MODULE_SIZE_LIMIT_BYTES: usize = 32 * 1024;
96
97 fn get_alignment_size(len: usize) -> usize {
99 let padding_alignment = Self::PADDING_ALIGNMENT;
100 ((len.saturating_sub(1) / padding_alignment) + 1) * padding_alignment
101 }
102
103 pub fn encrypt_to(&self, key: &fedimint_aead::LessSafeKey) -> Result<EncryptedClientBackup> {
105 let mut encoded = Encodable::consensus_encode_to_vec(self);
106
107 let alignment_size = Self::get_alignment_size(encoded.len());
108 let padding_size = alignment_size - encoded.len();
109 encoded.write_all(&vec![0u8; padding_size])?;
110
111 let encrypted = fedimint_aead::encrypt(encoded, key)?;
112 Ok(EncryptedClientBackup(encrypted))
113 }
114
115 fn validate_and_fallback_module_backups(
121 self,
122 last_backup: Option<&ClientBackup>,
123 ) -> ClientBackup {
124 let all_ids: BTreeSet<_> = self
126 .modules
127 .keys()
128 .chain(last_backup.iter().flat_map(|b| b.modules.keys()))
129 .copied()
130 .collect();
131
132 let mut modules = BTreeMap::new();
133 for module_id in all_ids {
134 if let Some(module_backup) = self
135 .modules
136 .get(&module_id)
137 .or_else(|| last_backup.and_then(|lb| lb.modules.get(&module_id)))
138 {
139 let size = module_backup.consensus_encode_to_len();
140 let limit = Self::PER_MODULE_SIZE_LIMIT_BYTES;
141 if size < limit {
142 modules.insert(module_id, module_backup.clone());
143 } else if let Some(last_module_backup) =
144 last_backup.and_then(|lb| lb.modules.get(&module_id))
145 {
146 let size_previous = last_module_backup.consensus_encode_to_len();
147 warn!(
148 target: LOG_CLIENT_BACKUP,
149 size,
150 limit,
151 %module_id,
152 size_previous,
153 "Module backup too large, will use previous version"
154 );
155 modules.insert(module_id, last_module_backup.clone());
156 } else {
157 warn!(
158 target: LOG_CLIENT_BACKUP,
159 size,
160 limit,
161 %module_id,
162 "Module backup too large, no previous version available to fall-back to"
163 );
164 }
165 }
166 }
167 ClientBackup {
168 session_count: self.session_count,
169 metadata: self.metadata,
170 modules,
171 }
172 }
173}
174
175impl Encodable for ClientBackup {
176 fn consensus_encode<W: Write>(&self, writer: &mut W) -> std::result::Result<usize, Error> {
177 let mut len = 0;
178 len += self.session_count.consensus_encode(writer)?;
179 len += self.metadata.consensus_encode(writer)?;
180 len += self.modules.consensus_encode(writer)?;
181
182 len += Vec::<u8>::new().consensus_encode(writer)?;
194
195 Ok(len)
196 }
197}
198
199impl Decodable for ClientBackup {
200 fn consensus_decode_partial<R: Read>(
201 r: &mut R,
202 modules: &ModuleDecoderRegistry,
203 ) -> std::result::Result<Self, DecodeError> {
204 let session_count = u64::consensus_decode_partial(r, modules).context("session_count")?;
205 let metadata = Metadata::consensus_decode_partial(r, modules).context("metadata")?;
206 let module_backups =
207 BTreeMap::<ModuleInstanceId, DynModuleBackup>::consensus_decode_partial(r, modules)
208 .context("module_backups")?;
209 let _padding = Vec::<u8>::consensus_decode_partial(r, modules).context("padding")?;
210
211 Ok(Self {
212 session_count,
213 metadata,
214 modules: module_backups,
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(&self.api, &self.root_secret(), &self.decoders)
360 .await
361 }
362
363 pub async fn download_backup_from_federation_static(
365 api: &DynGlobalApi,
366 root_secret: &DerivableSecret,
367 decoders: &ModuleDecoderRegistry,
368 ) -> Result<Option<ClientBackup>> {
369 debug!(target: LOG_CLIENT, "Downloading backup from the federation");
370 let mut responses: Vec<_> = api
371 .download_backup(&Client::get_backup_id_static(root_secret))
372 .await?
373 .into_iter()
374 .filter_map(|(peer, backup)| {
375 match EncryptedClientBackup(backup?.data).decrypt_with(
376 &Self::get_derived_backup_encryption_key_static(root_secret),
377 decoders,
378 ) {
379 Ok(valid) => Some(valid),
380 Err(e) => {
381 warn!(
382 target: LOG_CLIENT_RECOVERY,
383 "Invalid backup returned by {peer}: {e}"
384 );
385 None
386 }
387 }
388 })
389 .collect();
390
391 debug!(
392 target: LOG_CLIENT_RECOVERY,
393 "Received {} valid responses",
394 responses.len()
395 );
396 responses.sort_by_key(|backup| Reverse(backup.session_count));
398
399 Ok(responses.into_iter().next())
400 }
401
402 pub fn get_backup_id(&self) -> PublicKey {
405 self.get_derived_backup_signing_key().public_key()
406 }
407
408 pub fn get_backup_id_static(root_secret: &DerivableSecret) -> PublicKey {
409 Self::get_derived_backup_signing_key_static(root_secret).public_key()
410 }
411 fn get_derived_backup_encryption_key_static(
414 secret: &DerivableSecret,
415 ) -> fedimint_aead::LessSafeKey {
416 fedimint_aead::LessSafeKey::new(secret.derive_backup_secret().to_chacha20_poly1305_key())
417 }
418
419 fn get_derived_backup_signing_key_static(secret: &DerivableSecret) -> Keypair {
422 secret
423 .derive_backup_secret()
424 .to_secp_key(&Secp256k1::<SignOnly>::gen_new())
425 }
426
427 fn get_derived_backup_encryption_key(&self) -> fedimint_aead::LessSafeKey {
428 Self::get_derived_backup_encryption_key_static(&self.root_secret())
429 }
430
431 fn get_derived_backup_signing_key(&self) -> Keypair {
432 Self::get_derived_backup_signing_key_static(&self.root_secret())
433 }
434
435 pub async fn get_decoded_client_secret<T: Decodable>(&self) -> anyhow::Result<T> {
436 get_decoded_client_secret::<T>(self.db()).await
437 }
438}
439
440#[cfg(test)]
441mod tests {
442 use std::collections::BTreeMap;
443
444 use anyhow::Result;
445 use fedimint_core::encoding::{Decodable, Encodable};
446 use fedimint_core::module::registry::ModuleRegistry;
447 use fedimint_derive_secret::DerivableSecret;
448
449 use crate::backup::{ClientBackup, Metadata};
450 use crate::Client;
451
452 #[test]
453 fn sanity_ecash_backup_align() {
454 assert_eq!(
455 ClientBackup::get_alignment_size(1),
456 ClientBackup::PADDING_ALIGNMENT
457 );
458 assert_eq!(
459 ClientBackup::get_alignment_size(ClientBackup::PADDING_ALIGNMENT),
460 ClientBackup::PADDING_ALIGNMENT
461 );
462 assert_eq!(
463 ClientBackup::get_alignment_size(ClientBackup::PADDING_ALIGNMENT + 1),
464 ClientBackup::PADDING_ALIGNMENT * 2
465 );
466 }
467
468 #[test]
469 fn sanity_ecash_backup_decode_encode() -> Result<()> {
470 let orig = ClientBackup {
471 session_count: 0,
472 metadata: Metadata::from_raw(vec![1, 2, 3]),
473 modules: BTreeMap::new(),
474 };
475
476 let encoded = orig.consensus_encode_to_vec();
477 assert_eq!(
478 orig,
479 ClientBackup::consensus_decode_whole(&encoded, &ModuleRegistry::default())?
480 );
481
482 Ok(())
483 }
484
485 #[test]
486 fn sanity_ecash_backup_encrypt_decrypt() -> Result<()> {
487 const ENCRYPTION_HEADER_LEN: usize = 28;
488
489 let orig = ClientBackup {
490 modules: BTreeMap::new(),
491 session_count: 1,
492 metadata: Metadata::from_raw(vec![1, 2, 3]),
493 };
494
495 let secret = DerivableSecret::new_root(&[1; 32], &[1, 32]);
496 let key = Client::get_derived_backup_encryption_key_static(&secret);
497
498 let empty_encrypted = fedimint_aead::encrypt(vec![], &key)?;
499 assert_eq!(empty_encrypted.len(), ENCRYPTION_HEADER_LEN);
500
501 let encrypted = orig.encrypt_to(&key)?;
502 assert_eq!(
503 encrypted.len(),
504 ClientBackup::PADDING_ALIGNMENT + ENCRYPTION_HEADER_LEN
505 );
506
507 let decrypted = encrypted.decrypt_with(&key, &ModuleRegistry::default())?;
508
509 assert_eq!(orig, decrypted);
510
511 Ok(())
512 }
513}