1use std::collections::BTreeMap;
2use std::time::Duration;
3use std::{cmp, ops};
4
5use bitcoin::secp256k1::PublicKey;
6use fedimint_api_client::api::{
7 DynGlobalApi, VERSION_THAT_INTRODUCED_GET_SESSION_STATUS,
8 VERSION_THAT_INTRODUCED_GET_SESSION_STATUS_V2,
9};
10use fedimint_core::db::{AutocommitError, DatabaseTransaction};
11use fedimint_core::encoding::{Decodable, Encodable};
12use fedimint_core::module::registry::ModuleDecoderRegistry;
13use fedimint_core::module::{ApiVersion, ModuleCommon};
14use fedimint_core::session_outcome::{AcceptedItem, ConsensusItem, SessionStatus};
15use fedimint_core::task::{MaybeSend, MaybeSync, ShuttingDownError, TaskGroup};
16use fedimint_core::transaction::Transaction;
17use fedimint_core::util::FmtCompact as _;
18use fedimint_core::{Amount, OutPoint, PeerId, apply, async_trait_maybe_send};
19use fedimint_logging::LOG_CLIENT_RECOVERY;
20use futures::{Stream, StreamExt as _};
21use rand::{Rng as _, thread_rng};
22use serde::{Deserialize, Serialize};
23use tracing::{debug, trace, warn};
24
25use super::{ClientModuleInit, ClientModuleRecoverArgs};
26use crate::error::ClientModuleError;
27use crate::module::recovery::RecoveryProgress;
28use crate::module::{ClientContext, ClientModule};
29
30#[allow(clippy::struct_field_names)]
31#[derive(Debug, Clone, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
32pub struct RecoveryFromHistoryCommon {
34 start_session: u64,
35 next_session: u64,
36 end_session: u64,
37}
38
39impl RecoveryFromHistoryCommon {
40 pub fn new(start_session: u64, next_session: u64, end_session: u64) -> Self {
41 Self {
42 start_session,
43 next_session,
44 end_session,
45 }
46 }
47}
48
49#[apply(async_trait_maybe_send!)]
53pub trait RecoveryFromHistory: std::fmt::Debug + MaybeSend + MaybeSync + Clone {
54 type Init: ClientModuleInit;
56
57 async fn new(
59 init: &Self::Init,
60 args: &ClientModuleRecoverArgs<Self::Init>,
61 snapshot: Option<&<<Self::Init as ClientModuleInit>::Module as ClientModule>::Backup>,
62 ) -> Result<(Self, u64), ClientModuleError>;
63
64 async fn load_dbtx(
70 init: &Self::Init,
71 dbtx: &mut DatabaseTransaction<'_>,
72 args: &ClientModuleRecoverArgs<Self::Init>,
73 ) -> Result<Option<(Self, RecoveryFromHistoryCommon)>, ClientModuleError>;
74
75 async fn store_dbtx(
79 &self,
80 dbtx: &mut DatabaseTransaction<'_>,
81 common: &RecoveryFromHistoryCommon,
82 );
83
84 async fn delete_dbtx(&self, dbtx: &mut DatabaseTransaction<'_>);
88
89 async fn load_finalized(dbtx: &mut DatabaseTransaction<'_>) -> Option<bool>;
93
94 async fn store_finalized(dbtx: &mut DatabaseTransaction<'_>, state: bool);
98
99 async fn handle_session(
109 &mut self,
110 client_ctx: &ClientContext<<Self::Init as ClientModuleInit>::Module>,
111 session_idx: u64,
112 session_items: &Vec<AcceptedItem>,
113 ) -> Result<(), ClientModuleError> {
114 for accepted_item in session_items {
115 if let ConsensusItem::Transaction(ref transaction) = accepted_item.item {
116 self.handle_transaction(client_ctx, transaction, session_idx)
117 .await?;
118 }
119 }
120 Ok(())
121 }
122
123 async fn handle_transaction(
134 &mut self,
135 client_ctx: &ClientContext<<Self::Init as ClientModuleInit>::Module>,
136 transaction: &Transaction,
137 session_idx: u64,
138 ) -> Result<(), ClientModuleError> {
139 trace!(
140 target: LOG_CLIENT_RECOVERY,
141 tx_hash = %transaction.tx_hash(),
142 input_num = transaction.inputs.len(),
143 output_num = transaction.outputs.len(),
144 "processing transaction"
145 );
146
147 for (idx, input) in transaction.inputs.iter().enumerate() {
148 trace!(
149 target: LOG_CLIENT_RECOVERY,
150 tx_hash = %transaction.tx_hash(),
151 idx,
152 module_id = input.module_instance_id(),
153 "found transaction input"
154 );
155
156 if let Some(own_input) = client_ctx.input_from_dyn(input) {
157 self.handle_input(client_ctx, idx, own_input, session_idx)
158 .await?;
159 }
160 }
161
162 for (out_idx, output) in transaction.outputs.iter().enumerate() {
163 trace!(
164 target: LOG_CLIENT_RECOVERY,
165 tx_hash = %transaction.tx_hash(),
166 idx = out_idx,
167 module_id = output.module_instance_id(),
168 "found transaction output"
169 );
170
171 if let Some(own_output) = client_ctx.output_from_dyn(output) {
172 let out_point = OutPoint {
173 txid: transaction.tx_hash(),
174 out_idx: out_idx as u64,
175 };
176
177 self.handle_output(client_ctx, out_point, own_output, session_idx)
178 .await?;
179 }
180 }
181
182 Ok(())
183 }
184
185 async fn handle_input(
189 &mut self,
190 _client_ctx: &ClientContext<<Self::Init as ClientModuleInit>::Module>,
191 _idx: usize,
192 _input: &<<<Self::Init as ClientModuleInit>::Module as ClientModule>::Common as ModuleCommon>::Input,
193 _session_idx: u64,
194 ) -> Result<(), ClientModuleError> {
195 Ok(())
196 }
197
198 async fn handle_output(
202 &mut self,
203 _client_ctx: &ClientContext<<Self::Init as ClientModuleInit>::Module>,
204 _out_point: OutPoint,
205 _output: &<<<Self::Init as ClientModuleInit>::Module as ClientModule>::Common as ModuleCommon>::Output,
206 _session_idx: u64,
207 ) -> Result<(), ClientModuleError> {
208 Ok(())
209 }
210
211 async fn pre_finalize(&mut self) -> Result<(), ClientModuleError> {
214 Ok(())
215 }
216
217 async fn finalize_dbtx(
230 &self,
231 dbtx: &mut DatabaseTransaction<'_>,
232 ) -> Result<Option<Amount>, ClientModuleError>;
233}
234
235impl<Init> ClientModuleRecoverArgs<Init>
236where
237 Init: ClientModuleInit,
238{
239 pub async fn recover_from_history<Recovery>(
254 &self,
255 init: &Init,
256 snapshot: Option<&<<Init as ClientModuleInit>::Module as ClientModule>::Backup>,
257 ) -> Result<Option<Amount>, ClientModuleError>
258 where
259 Recovery: RecoveryFromHistory<Init = Init> + std::fmt::Debug,
260 {
261 fn fetch_block_stream<'a>(
266 api: DynGlobalApi,
267 core_api_version: ApiVersion,
268 decoders: ModuleDecoderRegistry,
269 epoch_range: ops::Range<u64>,
270 broadcast_public_keys: Option<BTreeMap<PeerId, PublicKey>>,
271 task_group: TaskGroup,
272 ) -> impl futures::Stream<Item = Result<(u64, Vec<AcceptedItem>), ShuttingDownError>> + 'a
273 {
274 let parallelism_level =
276 if core_api_version < VERSION_THAT_INTRODUCED_GET_SESSION_STATUS_V2 {
277 64
278 } else {
279 128
280 };
281
282 futures::stream::iter(epoch_range.clone())
283 .map(move |session_idx| {
284 let api = api.clone();
285 let decoders = decoders.clone().with_fallback();
288 let task_group = task_group.clone();
289 let broadcast_public_keys = broadcast_public_keys.clone();
290
291 Box::pin(async move {
292 task_group.spawn_cancellable("recovery fetch block", async move {
296
297 let mut retry_sleep = Duration::from_millis(10);
298 let block = loop {
299 trace!(target: LOG_CLIENT_RECOVERY, session_idx, "Awaiting signed block");
300
301 let items_res = if core_api_version < VERSION_THAT_INTRODUCED_GET_SESSION_STATUS {
302 api.await_block(session_idx, &decoders).await.map(|s| s.items)
303 } else {
304 api.get_session_status(session_idx, &decoders, core_api_version, broadcast_public_keys.as_ref()).await.map(|s| match s {
305 SessionStatus::Initial => panic!("Federation missing session that existed when we started recovery"),
306 SessionStatus::Pending(items) => items,
307 SessionStatus::Complete(s) => s.items,
308 })
309 };
310
311 match items_res {
312 Ok(block) => {
313 trace!(target: LOG_CLIENT_RECOVERY, session_idx, "Got signed session");
314 break block
315 },
316 Err(err) => {
317 const MAX_SLEEP: Duration = Duration::from_mins(2);
318
319 warn!(target: LOG_CLIENT_RECOVERY, err = %err.fmt_compact(), session_idx, "Error trying to fetch signed block");
320 if retry_sleep <= MAX_SLEEP {
323 retry_sleep = retry_sleep
324 + thread_rng().gen_range(Duration::ZERO..=retry_sleep);
325 }
326 fedimint_core::runtime::sleep(cmp::min(retry_sleep, MAX_SLEEP))
327 .await;
328 }
329 }
330 };
331
332 (session_idx, block)
333 }).await.expect("Can't fail")
334 })
335 })
336 .buffered(parallelism_level)
337 }
338
339 async fn make_progress<Init, Recovery: RecoveryFromHistory<Init = Init>>(
341 client_ctx: &ClientContext<<Init as ClientModuleInit>::Module>,
342 common_state: &mut RecoveryFromHistoryCommon,
343 state: &mut Recovery,
344 block_stream: &mut (
345 impl Stream<Item = Result<(u64, Vec<AcceptedItem>), ShuttingDownError>> + Unpin
346 ),
347 ) -> Result<(), ClientModuleError>
348 where
349 Init: ClientModuleInit,
350 {
351 const PROGRESS_SNAPSHOT_BLOCKS: u64 = 5000;
356
357 let start = fedimint_core::time::now();
358
359 let block_range = common_state.next_session
360 ..cmp::min(
361 common_state
362 .next_session
363 .wrapping_add(PROGRESS_SNAPSHOT_BLOCKS),
364 common_state.end_session,
365 );
366
367 for _ in block_range {
368 let Some(res) = block_stream.next().await else {
369 break;
370 };
371
372 let (session_idx, accepted_items) = res.map_err(ClientModuleError::other)?;
373
374 assert_eq!(common_state.next_session, session_idx);
375 state
376 .handle_session(client_ctx, session_idx, &accepted_items)
377 .await?;
378
379 common_state.next_session += 1;
380
381 if Duration::from_secs(10)
382 < fedimint_core::time::now()
383 .duration_since(start)
384 .unwrap_or_default()
385 {
386 break;
387 }
388 }
389
390 Ok(())
391 }
392
393 let db = self.db();
394 let client_ctx = self.context();
395
396 if Recovery::load_finalized(&mut db.begin_transaction_nc().await)
397 .await
398 .unwrap_or_default()
399 {
400 warn!(
421 target: LOG_CLIENT_RECOVERY,
422 "Previously finalized, exiting"
423 );
424 return Ok(None);
425 }
426 let current_session_count = client_ctx
427 .global_api()
428 .session_count()
429 .await
430 .map_err(ClientModuleError::other)?;
431 debug!(target: LOG_CLIENT_RECOVERY, session_count = current_session_count, "Current session count");
432
433 let (mut state, mut common_state) =
434 if let Some((state, common_state)) = Recovery::load_dbtx(init, &mut db.begin_transaction_nc().await, self).await? {
437 (state, common_state)
438 } else {
439 let (state, start_session) = Recovery::new(init, self, snapshot).await?;
440
441 debug!(target: LOG_CLIENT_RECOVERY, start_session, "Recovery start session");
442 (state,
443 RecoveryFromHistoryCommon {
444 start_session,
445 next_session: start_session,
446 end_session: current_session_count + 1,
447 })
448 };
449
450 let block_stream_session_range = common_state.next_session..common_state.end_session;
451 debug!(target: LOG_CLIENT_RECOVERY, range = ?block_stream_session_range, "Starting block streaming");
452
453 let mut block_stream = fetch_block_stream(
454 self.api().clone(),
455 *self.core_api_version(),
456 client_ctx.decoders(),
457 block_stream_session_range,
458 client_ctx
459 .get_config()
460 .await
461 .global
462 .broadcast_public_keys
463 .clone(),
464 self.task_group().clone(),
465 );
466 let client_ctx = self.context();
467
468 while common_state.next_session < common_state.end_session {
469 make_progress(
470 &client_ctx,
471 &mut common_state,
472 &mut state,
473 &mut block_stream,
474 )
475 .await?;
476
477 let mut dbtx = db.begin_transaction().await;
478 state.store_dbtx(&mut dbtx.to_ref_nc(), &common_state).await;
479 dbtx.commit_tx().await;
480
481 self.update_recovery_progress(RecoveryProgress {
482 complete: (common_state.next_session - common_state.start_session)
483 .try_into()
484 .unwrap_or(u32::MAX),
485 total: (common_state.end_session - common_state.start_session)
486 .try_into()
487 .unwrap_or(u32::MAX),
488 });
489 }
490
491 state.pre_finalize().await?;
492
493 let mut dbtx = db.begin_transaction().await;
494 state.store_dbtx(&mut dbtx.to_ref_nc(), &common_state).await;
495 dbtx.commit_tx().await;
496
497 debug!(
498 target: LOG_CLIENT_RECOVERY,
499 ?state,
500 "Finalizing restore"
501 );
502
503 let recovered_amount = db
504 .autocommit(
505 |dbtx, _| {
506 let state = state.clone();
507 {
508 Box::pin(async move {
509 state.delete_dbtx(dbtx).await;
510 let recovered_amount = state.finalize_dbtx(dbtx).await?;
511 Recovery::store_finalized(dbtx, true).await;
512
513 Ok::<_, ClientModuleError>(recovered_amount)
514 })
515 }
516 },
517 None,
518 )
519 .await
520 .map_err(|error| match error {
521 AutocommitError::ClosureError { error, .. } => error,
522 AutocommitError::CommitFailed { last_error, .. } => {
523 ClientModuleError::other(last_error)
524 }
525 })?;
526
527 Ok(recovered_amount)
528 }
529}