fedimint_mint_client/
repair_wallet.rs1use std::collections::BTreeMap;
2
3use fedimint_api_client::api::{DynModuleApi, FederationError, FederationResult};
4use fedimint_core::db::{AutocommitError, IDatabaseTransactionOpsCoreTyped};
5use fedimint_core::util::backoff_util::aggressive_backoff;
6use fedimint_core::util::retry;
7use fedimint_core::{Amount, TieredCounts};
8use futures::{StreamExt, TryStreamExt, stream};
9
10use crate::api::MintFederationApi;
11use crate::client_db::{
12 NextECashNoteIndexKey, NextECashNoteIndexKeyPrefix, NoteKey, NoteKeyPrefix,
13};
14use crate::error::RepairWalletError;
15use crate::output::NoteIssuanceRequest;
16use crate::{MintClientModule, NoteIndex};
17
18const CHECK_PARALLELISM: usize = 16;
19
20#[derive(Debug, Clone, Default)]
21pub struct RepairSummary {
22 pub spent_notes: TieredCounts,
25 pub used_indices: TieredCounts,
32}
33
34impl MintClientModule {
35 pub async fn try_repair_wallet(
47 &self,
48 gap_limit: u64,
49 ) -> Result<RepairSummary, RepairWalletError> {
50 let module_api = self.client_ctx.module_api();
51
52 self.client_ctx
53 .module_db()
54 .autocommit(
55 |dbtx, _| {
56 let module_api = module_api.clone();
57
58 Box::pin(async move {
59 let mut summary = RepairSummary::default();
60
61 let note_keys = dbtx
62 .find_by_prefix_sorted_descending(&NoteKeyPrefix)
63 .await
64 .map(|(key, _)| key)
65 .collect::<Vec<_>>()
66 .await;
67
68 let spent_notes = Self::find_spent_notes(&module_api, note_keys).await?;
69
70 for note_key in spent_notes {
71 summary.spent_notes.inc(note_key.amount, 1);
72 dbtx.remove_entry(¬e_key).await;
73 }
74
75 let next_indices: BTreeMap<_, _> = {
76 let mut db_next_indexes = dbtx
77 .find_by_prefix_sorted_descending(&NextECashNoteIndexKeyPrefix)
78 .await
79 .map(|(key, idx)| (key.0, idx))
80 .collect::<BTreeMap<_, _>>()
81 .await;
82
83 self.cfg
84 .tbs_pks
85 .tiers()
86 .map(|&denomination| {
87 (
88 denomination,
89 db_next_indexes.remove(&denomination).unwrap_or_default(),
90 )
91 })
92 .collect()
93 };
94
95 let used_nonces = self
96 .find_used_nonces(&module_api, next_indices, gap_limit)
97 .await?;
98
99 for (amount, next_index) in used_nonces {
100 let old_index = dbtx
101 .insert_entry(&NextECashNoteIndexKey(amount), &next_index)
102 .await
103 .unwrap_or_default();
104 summary
105 .used_indices
106 .inc(amount, (next_index - old_index) as usize);
107 }
108
109 Ok::<_, RepairWalletError>(summary)
110 })
111 },
112 Some(100),
113 )
114 .await
115 .map_err(|e| match e {
116 AutocommitError::ClosureError { error, .. } => error,
117 AutocommitError::CommitFailed { last_error, .. } => {
118 RepairWalletError::Database(last_error)
119 }
120 })
121 }
122
123 async fn find_spent_notes(
124 module_api: &DynModuleApi,
125 note_keys: Vec<NoteKey>,
126 ) -> FederationResult<Vec<NoteKey>> {
127 stream::iter(note_keys.into_iter())
128 .map(|key| {
129 let module_api_inner = module_api.clone();
130 async move {
131 let spent = retry("fetch e-cash spentness", aggressive_backoff(), || async {
132 module_api_inner.check_note_spent(key.nonce).await
133 })
134 .await?;
135 Ok(if spent { Some(key) } else { None })
136 }
137 })
138 .buffer_unordered(CHECK_PARALLELISM)
139 .try_filter_map(|result| async move { Ok(result) })
140 .try_collect()
141 .await
142 }
143
144 async fn find_used_nonces(
145 &self,
146 module_api: &DynModuleApi,
147 next_indices: BTreeMap<Amount, u64>,
148 gap_limit: u64,
149 ) -> FederationResult<Vec<(Amount, u64)>> {
150 stream::iter(next_indices.into_iter())
151 .map(|(amount, original_next_index)| {
152 let module_api_inner = module_api.clone();
153 async move {
154 let mut next_index = original_next_index;
155 let maybe_advanced_index = loop {
156 let maybe_nonce_gap = self
157 .gap_till_next_nonce_used(
158 &module_api_inner,
159 amount,
160 next_index,
161 gap_limit,
162 )
163 .await?;
164
165 if let Some(gap) = maybe_nonce_gap {
166 next_index += gap + 1;
168 } else if original_next_index == next_index {
169 break None;
171 } else {
172 break Some((amount, next_index));
175 }
176 };
177
178 Result::<_, FederationError>::Ok(maybe_advanced_index)
179 }
180 })
181 .buffer_unordered(CHECK_PARALLELISM)
182 .try_filter_map(|advanced_index| async move { Ok(advanced_index) })
183 .try_collect::<Vec<_>>()
184 .await
185 }
186
187 async fn gap_till_next_nonce_used(
194 &self,
195 module_api: &DynModuleApi,
196 amount: Amount,
197 base_index: u64,
198 gap_limit: u64,
199 ) -> FederationResult<Option<u64>> {
200 for gap in 0..gap_limit {
201 let idx = base_index + gap;
202 let note_secret = Self::new_note_secret_static(&self.secret, amount, NoteIndex(idx));
203 let (_, blind_nonce) = NoteIssuanceRequest::new(&self.secp, ¬e_secret);
204 let nonce_used = retry(
205 "checking if blind nonce was already used",
206 aggressive_backoff(),
207 || async { module_api.check_blind_nonce_used(blind_nonce).await },
208 )
209 .await?;
210 if nonce_used {
211 return Ok(Some(gap));
212 }
213 }
214 Ok(None)
215 }
216}