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