fedimint_mint_client/
repair_wallet.rs

1use std::collections::BTreeMap;
2
3use fedimint_core::TieredCounts;
4use fedimint_core::db::IDatabaseTransactionOpsCoreTyped;
5use fedimint_core::util::backoff_util::aggressive_backoff;
6use fedimint_core::util::retry;
7use futures::{StreamExt, TryStreamExt, stream};
8
9use crate::api::MintFederationApi;
10use crate::client_db::{
11    NextECashNoteIndexKey, NextECashNoteIndexKeyPrefix, NoteKey, NoteKeyPrefix,
12};
13use crate::output::NoteIssuanceRequest;
14use crate::{MintClientModule, NoteIndex};
15
16const CHECK_PARALLELISM: usize = 16;
17
18#[derive(Debug, Clone, Default)]
19pub struct RepairSummary {
20    /// Number of e-cash notes that were found to be spent and removed from the
21    /// wallet per denomination
22    pub spent_notes: TieredCounts,
23    /// Denomination of which e-cash nonces were found to be used already and
24    /// were skipped
25    ///
26    /// Note: if this is non-empty the correct approach is doing a full
27    /// from-scratch recovery, otherwise we might not be aware of unspent notes
28    /// issued to us.
29    pub used_indices: TieredCounts,
30}
31
32impl MintClientModule {
33    /// Attempts to fix inconsistent wallet states. **Breaks privacy guarantees
34    /// and is destructive!**
35    ///
36    /// Invalid states that are fixable this way:
37    ///   * Already-spent e-cash being in the wallet
38    ///   * E-cash nonces that would be used to issue new notes already being
39    ///     used
40    ///
41    /// When invalid notes are found, they are removed from the wallet. Make
42    /// sure that the user has a backup of their seed before running this
43    /// function.
44    pub async fn try_repair_wallet(&self) -> anyhow::Result<RepairSummary> {
45        let mut summary = RepairSummary::default();
46
47        let module_api = self.client_ctx.module_api();
48        let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
49
50        // First check if any of our notes are already spent and remove them
51        let spent_notes: Vec<NoteKey> = dbtx
52            .find_by_prefix_sorted_descending(&NoteKeyPrefix)
53            .await
54            .map(|(key, _)| {
55                let module_api_inner = module_api.clone();
56                async move {
57                    let spent = retry("fetch e-cash spentness", aggressive_backoff(), || async {
58                        Ok(module_api_inner.check_note_spent(key.nonce).await?)
59                    })
60                    .await?;
61                    anyhow::Ok(if spent { Some(key) } else { None })
62                }
63            })
64            .buffer_unordered(CHECK_PARALLELISM)
65            .try_filter_map(|result| async move { Ok(result) })
66            .try_collect()
67            .await?;
68
69        for note_key in spent_notes {
70            summary.spent_notes.inc(note_key.amount, 1);
71            dbtx.remove_entry(&note_key).await;
72        }
73
74        let next_indices: BTreeMap<_, _> = {
75            let mut db_next_indexes = dbtx
76                .find_by_prefix_sorted_descending(&NextECashNoteIndexKeyPrefix)
77                .await
78                .map(|(key, idx)| (key.0, idx))
79                .collect::<BTreeMap<_, _>>()
80                .await;
81
82            self.cfg
83                .tbs_pks
84                .tiers()
85                .map(|&denomination| {
86                    (
87                        denomination,
88                        db_next_indexes.remove(&denomination).unwrap_or_default(),
89                    )
90                })
91                .collect()
92        };
93
94        // Next check if any of the indices for issuing new notes are already used
95        let used_nonces = stream::iter(next_indices.into_iter())
96            .map(|(amount, original_next_index)| {
97                let module_api_inner = module_api.clone();
98                async move {
99                    let mut next_index = original_next_index;
100                    let maybe_advanced_index = loop {
101                        let note_secret = Self::new_note_secret_static(
102                            &self.secret,
103                            amount,
104                            NoteIndex(next_index),
105                        );
106                        let (_, blind_nonce) = NoteIssuanceRequest::new(&self.secp, &note_secret);
107                        let nonce_used = retry(
108                            "checking if blind nonce was already used",
109                            aggressive_backoff(),
110                            || async {
111                                Ok(module_api_inner.check_blind_nonce_used(blind_nonce).await?)
112                            },
113                        )
114                        .await?;
115
116                        if nonce_used {
117                            // If the nonce was already used, try again with the next index
118                            next_index += 1;
119                        } else if original_next_index == next_index {
120                            // If the initial nonce wasn't used we are good, nothing to be done
121                            break None;
122                        } else {
123                            // If the initial nonce was used but we found an unused one by now,
124                            // report the used index
125                            break Some((amount, next_index));
126                        }
127                    };
128
129                    Result::<_, anyhow::Error>::Ok(maybe_advanced_index)
130                }
131            })
132            .buffer_unordered(CHECK_PARALLELISM)
133            .try_filter_map(|advanced_index| async move { Ok(advanced_index) })
134            .try_collect::<Vec<_>>()
135            .await?;
136
137        for (amount, next_index) in used_nonces {
138            let old_index = dbtx
139                .insert_entry(&NextECashNoteIndexKey(amount), &next_index)
140                .await
141                .unwrap_or_default();
142            summary
143                .used_indices
144                .inc(amount, (next_index - old_index) as usize);
145        }
146
147        dbtx.commit_tx().await;
148        Ok(summary)
149    }
150}