Skip to main content

fedimint_mint_client/
repair_wallet.rs

1use 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    /// Number of e-cash notes that were found to be spent and removed from the
23    /// wallet per denomination
24    pub spent_notes: TieredCounts,
25    /// Denomination of which e-cash nonces were found to be used already and
26    /// were skipped
27    ///
28    /// Note: if this is non-empty the correct approach is doing a full
29    /// from-scratch recovery, otherwise we might not be aware of unspent notes
30    /// issued to us.
31    pub used_indices: TieredCounts,
32}
33
34impl MintClientModule {
35    /// Attempts to fix inconsistent wallet states. **Breaks privacy guarantees
36    /// and is destructive!**
37    ///
38    /// Invalid states that are fixable this way:
39    ///   * Already-spent e-cash being in the wallet
40    ///   * E-cash nonces that would be used to issue new notes already being
41    ///     used
42    ///
43    /// When invalid notes are found, they are removed from the wallet. Make
44    /// sure that the user has a backup of their seed before running this
45    /// function.
46    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(&note_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                            // If the nonce was already used, try again with the next index
167                            next_index += gap + 1;
168                        } else if original_next_index == next_index {
169                            // If the initial nonce wasn't used we are good, nothing to be done
170                            break None;
171                        } else {
172                            // If the initial nonce was used but we found an unused one by now,
173                            // report the used index
174                            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    /// Checks up to `gap_limit` nonces starting from `base_index` for having
188    /// being used already.
189    ///
190    /// If the nonce at `base_index` is used, returns `Some(0)`, if it's unused
191    /// returns `None`. If there's an unused nonce and then a used one returns
192    /// `Some(1)`.
193    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, &note_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}