Skip to main content

fedimint_mint_client/
repair_wallet.rs

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