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_logging::{LOG_CLIENT, LOG_CLIENT_BACKUP, LOG_CLIENT_RECOVERY};
17use serde::{Deserialize, Serialize};
18use tracing::{debug, info, warn};
19
20use super::Client;
21use crate::db::event_log::{Event, EventKind};
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 encoded = Encodable::consensus_encode_to_vec(self);
106
107 let encrypted = fedimint_aead::encrypt(encoded, key)?;
108 Ok(EncryptedClientBackup(encrypted))
109 }
110
111 fn validate_and_fallback_module_backups(
117 self,
118 last_backup: Option<&ClientBackup>,
119 ) -> ClientBackup {
120 let all_ids: BTreeSet<_> = self
122 .modules
123 .keys()
124 .chain(last_backup.iter().flat_map(|b| b.modules.keys()))
125 .copied()
126 .collect();
127
128 let mut modules = BTreeMap::new();
129 for module_id in all_ids {
130 if let Some(module_backup) = self
131 .modules
132 .get(&module_id)
133 .or_else(|| last_backup.and_then(|lb| lb.modules.get(&module_id)))
134 {
135 let size = module_backup.consensus_encode_to_len();
136 let limit = Self::PER_MODULE_SIZE_LIMIT_BYTES;
137 if size < limit {
138 modules.insert(module_id, module_backup.clone());
139 } else if let Some(last_module_backup) =
140 last_backup.and_then(|lb| lb.modules.get(&module_id))
141 {
142 let size_previous = last_module_backup.consensus_encode_to_len();
143 warn!(
144 size,
145 limit,
146 %module_id,
147 size_previous,
148 "Module backup too large, will use previous version"
149 );
150 modules.insert(module_id, last_module_backup.clone());
151 } else {
152 warn!(
153 size,
154 limit,
155 %module_id,
156 "Module backup too large, no previous version available to fall-back to"
157 );
158 }
159 }
160 }
161 ClientBackup {
162 session_count: self.session_count,
163 metadata: self.metadata,
164 modules,
165 }
166 }
167}
168
169impl Encodable for ClientBackup {
170 fn consensus_encode<W: Write>(&self, writer: &mut W) -> std::result::Result<usize, Error> {
171 let mut len = 0;
172 len += self.session_count.consensus_encode(writer)?;
173 len += self.metadata.consensus_encode(writer)?;
174 len += self.modules.consensus_encode(writer)?;
175
176 let estimated_len = len + 3;
179
180 let alignment_size = Self::get_alignment_size(estimated_len); let padding = vec![0u8; alignment_size - estimated_len];
183 len += padding.consensus_encode(writer)?;
184
185 Ok(len)
186 }
187}
188
189impl Decodable for ClientBackup {
190 fn consensus_decode<R: Read>(
191 r: &mut R,
192 modules: &ModuleDecoderRegistry,
193 ) -> std::result::Result<Self, DecodeError> {
194 let session_count = u64::consensus_decode(r, modules).context("session_count")?;
195 let metadata = Metadata::consensus_decode(r, modules).context("metadata")?;
196 let module_backups =
197 BTreeMap::<ModuleInstanceId, DynModuleBackup>::consensus_decode(r, modules)
198 .context("module_backups")?;
199 let _padding = Vec::<u8>::consensus_decode(r, modules).context("padding")?;
200
201 Ok(Self {
202 session_count,
203 metadata,
204 modules: module_backups,
205 })
206 }
207}
208
209#[derive(Clone)]
211pub struct EncryptedClientBackup(Vec<u8>);
212
213impl EncryptedClientBackup {
214 pub fn decrypt_with(
215 mut self,
216 key: &fedimint_aead::LessSafeKey,
217 decoders: &ModuleDecoderRegistry,
218 ) -> Result<ClientBackup> {
219 let decrypted = fedimint_aead::decrypt(&mut self.0, key)?;
220 Ok(ClientBackup::consensus_decode(
221 &mut Cursor::new(decrypted),
222 decoders,
223 )?)
224 }
225
226 pub fn into_backup_request(self, keypair: &Keypair) -> Result<SignedBackupRequest> {
227 let request = BackupRequest {
228 id: keypair.public_key(),
229 timestamp: fedimint_core::time::now(),
230 payload: self.0,
231 };
232
233 request.sign(keypair)
234 }
235
236 pub fn len(&self) -> usize {
237 self.0.len()
238 }
239
240 #[must_use]
241 pub fn is_empty(&self) -> bool {
242 self.len() == 0
243 }
244}
245
246#[derive(Serialize, Deserialize)]
247pub struct EventBackupDone;
248
249impl Event for EventBackupDone {
250 const MODULE: Option<fedimint_core::core::ModuleKind> = None;
251
252 const KIND: EventKind = EventKind::from_static("backup-done");
253}
254
255impl Client {
256 pub async fn create_backup(&self, metadata: Metadata) -> anyhow::Result<ClientBackup> {
258 let session_count = self.api.session_count().await?;
259 let mut modules = BTreeMap::new();
260 for (id, kind, module) in self.modules.iter_modules() {
261 debug!(target: LOG_CLIENT_BACKUP, module_id=id, module_kind=%kind, "Preparing module backup");
262 if module.supports_backup() {
263 let backup = module.backup(id).await?;
264
265 debug!(target: LOG_CLIENT_BACKUP, module_id=id, module_kind=%kind, "Prepared module backup");
266 modules.insert(id, backup);
267 } else {
268 debug!(target: LOG_CLIENT_BACKUP, module_id=id, module_kind=%kind, "Module does not support backup");
269 }
270 }
271
272 Ok(ClientBackup {
273 session_count,
274 metadata,
275 modules,
276 })
277 }
278
279 async fn load_previous_backup(&self) -> Option<ClientBackup> {
280 let mut dbtx = self.db.begin_transaction_nc().await;
281 dbtx.get_value(&LastBackupKey).await
282 }
283
284 async fn store_last_backup(&self, backup: &ClientBackup) {
285 let mut dbtx = self.db.begin_transaction().await;
286 dbtx.insert_entry(&LastBackupKey, backup).await;
287 dbtx.commit_tx().await;
288 }
289
290 pub async fn backup_to_federation(&self, metadata: Metadata) -> Result<()> {
292 ensure!(
293 !self.has_pending_recoveries(),
294 "Cannot backup while there are pending recoveries"
295 );
296
297 let last_backup = self.load_previous_backup().await;
298 let new_backup = self.create_backup(metadata).await?;
299
300 let new_backup = new_backup.validate_and_fallback_module_backups(last_backup.as_ref());
301
302 let encrypted = new_backup.encrypt_to(&self.get_derived_backup_encryption_key())?;
303
304 self.validate_backup(&encrypted)?;
305
306 self.store_last_backup(&new_backup).await;
307
308 self.upload_backup(&encrypted).await?;
309
310 self.log_event(None, EventBackupDone).await;
311
312 Ok(())
313 }
314
315 pub fn validate_backup(&self, backup: &EncryptedClientBackup) -> Result<()> {
317 if BACKUP_REQUEST_MAX_PAYLOAD_SIZE_BYTES < backup.len() {
318 bail!("Backup payload too large");
319 }
320 Ok(())
321 }
322
323 pub async fn upload_backup(&self, backup: &EncryptedClientBackup) -> Result<()> {
325 self.validate_backup(backup)?;
326 let size = backup.len();
327 info!(
328 target: LOG_CLIENT_BACKUP,
329 size, "Uploading backup to federation"
330 );
331 let backup_request = backup
332 .clone()
333 .into_backup_request(&self.get_derived_backup_signing_key())?;
334 self.api.upload_backup(&backup_request).await?;
335 info!(
336 target: LOG_CLIENT_BACKUP,
337 size, "Uploaded backup to federation"
338 );
339 Ok(())
340 }
341
342 pub async fn download_backup_from_federation(&self) -> Result<Option<ClientBackup>> {
343 Self::download_backup_from_federation_static(&self.api, &self.root_secret(), &self.decoders)
344 .await
345 }
346
347 pub async fn download_backup_from_federation_static(
349 api: &DynGlobalApi,
350 root_secret: &DerivableSecret,
351 decoders: &ModuleDecoderRegistry,
352 ) -> Result<Option<ClientBackup>> {
353 debug!(target: LOG_CLIENT, "Downloading backup from the federation");
354 let mut responses: Vec<_> = api
355 .download_backup(&Client::get_backup_id_static(root_secret))
356 .await?
357 .into_iter()
358 .filter_map(|(peer, backup)| {
359 match EncryptedClientBackup(backup?.data).decrypt_with(
360 &Self::get_derived_backup_encryption_key_static(root_secret),
361 decoders,
362 ) {
363 Ok(valid) => Some(valid),
364 Err(e) => {
365 warn!(
366 target: LOG_CLIENT_RECOVERY,
367 "Invalid backup returned by {peer}: {e}"
368 );
369 None
370 }
371 }
372 })
373 .collect();
374
375 debug!(
376 target: LOG_CLIENT_RECOVERY,
377 "Received {} valid responses",
378 responses.len()
379 );
380 responses.sort_by_key(|backup| Reverse(backup.session_count));
382
383 Ok(responses.into_iter().next())
384 }
385
386 pub fn get_backup_id(&self) -> PublicKey {
389 self.get_derived_backup_signing_key().public_key()
390 }
391
392 pub fn get_backup_id_static(root_secret: &DerivableSecret) -> PublicKey {
393 Self::get_derived_backup_signing_key_static(root_secret).public_key()
394 }
395 fn get_derived_backup_encryption_key_static(
398 secret: &DerivableSecret,
399 ) -> fedimint_aead::LessSafeKey {
400 fedimint_aead::LessSafeKey::new(secret.derive_backup_secret().to_chacha20_poly1305_key())
401 }
402
403 fn get_derived_backup_signing_key_static(secret: &DerivableSecret) -> Keypair {
406 secret
407 .derive_backup_secret()
408 .to_secp_key(&Secp256k1::<SignOnly>::gen_new())
409 }
410
411 fn get_derived_backup_encryption_key(&self) -> fedimint_aead::LessSafeKey {
412 Self::get_derived_backup_encryption_key_static(&self.root_secret())
413 }
414
415 fn get_derived_backup_signing_key(&self) -> Keypair {
416 Self::get_derived_backup_signing_key_static(&self.root_secret())
417 }
418
419 pub async fn get_decoded_client_secret<T: Decodable>(&self) -> anyhow::Result<T> {
420 get_decoded_client_secret::<T>(self.db()).await
421 }
422}
423
424#[cfg(test)]
425mod tests;