fedimint_wallet_client/backup/
recovery_history_tracker.rs

1use std::collections::{BTreeMap, BTreeSet, VecDeque};
2
3use fedimint_core::encoding::{Decodable, Encodable};
4use fedimint_logging::LOG_CLIENT_MODULE_WALLET;
5use tracing::debug;
6
7use crate::WalletClientModuleData;
8use crate::backup::FEDERATION_RECOVER_MAX_GAP;
9use crate::client_db::TweakIdx;
10
11/// Tracks addresses `TweakIdx`s/addresses that are expected to have been used
12/// against the stream of addresses that were actually used for peg-ins in the
13/// Federation.
14///
15/// Since replaying Federation history is entirely private, the goal here
16/// is to find the last peg-in address already used without compromising
17/// privacy like when querying Bitcoin node.
18///
19/// While at it, collect some addresses that were actually used for peg-ins by
20/// other clients, just to query for them as decoys and thus hopefully make the
21/// malicious Bitcoin node operator have less confidence about which addresses
22/// are actually linked with each other.
23#[derive(Clone, Debug, Encodable, Decodable)]
24pub struct ConsensusPegInTweakIdxesUsedTracker {
25    /// Any time we detect one of the scripts in `pending_pubkey_scripts` was
26    /// used we insert the `tweak_idx`, so we can skip asking network about
27    /// them (which would be bad for privacy)
28    used_tweak_idxes: BTreeSet<TweakIdx>,
29    /// All the pubkey scripts we are looking for in the federation history, to
30    /// detect previous successful peg-ins.
31    pending_pubkey_scripts: BTreeMap<bitcoin::ScriptBuf, TweakIdx>,
32    /// Next tweak idx to add to `pending_pubkey_scripts`
33    next_pending_tweak_idx: TweakIdx,
34
35    /// Collection of recent scripts from federation history that do not belong
36    /// to us
37    decoys: VecDeque<bitcoin::ScriptBuf>,
38    // To avoid updating `decoys` for the whole recovery, which might be a lot of extra updates
39    // most of which will be thrown away, ignore script pubkeys from before this `session_idx`
40    decoy_session_threshold: u64,
41}
42
43impl ConsensusPegInTweakIdxesUsedTracker {
44    pub(crate) fn new(
45        previous_next_unused_idx: TweakIdx,
46        start_session_idx: u64,
47        current_session_count: u64,
48        data: &WalletClientModuleData,
49    ) -> Self {
50        debug_assert!(start_session_idx <= current_session_count);
51
52        let mut s = Self {
53            next_pending_tweak_idx: previous_next_unused_idx,
54            pending_pubkey_scripts: BTreeMap::new(),
55            decoys: VecDeque::new(),
56            decoy_session_threshold: current_session_count
57                .saturating_sub((current_session_count.saturating_sub(current_session_count)) / 20),
58            used_tweak_idxes: BTreeSet::new(),
59        };
60
61        s.init(data);
62
63        s
64    }
65
66    fn init(&mut self, data: &WalletClientModuleData) {
67        for _ in 0..super::ONCHAIN_RECOVER_MAX_GAP {
68            self.generate_next_pending_tweak_idx(data);
69        }
70        debug_assert_eq!(
71            self.pending_pubkey_scripts.len(),
72            super::ONCHAIN_RECOVER_MAX_GAP as usize
73        );
74    }
75
76    pub fn used_tweak_idxes(&self) -> &BTreeSet<TweakIdx> {
77        &self.used_tweak_idxes
78    }
79
80    fn generate_next_pending_tweak_idx(&mut self, data: &WalletClientModuleData) {
81        let (script, _address, _tweak_key, _operation_id) =
82            data.derive_peg_in_script(self.next_pending_tweak_idx);
83
84        self.pending_pubkey_scripts
85            .insert(script, self.next_pending_tweak_idx);
86        self.next_pending_tweak_idx = self.next_pending_tweak_idx.next();
87    }
88
89    fn refill_pending_pool_up_to_tweak_idx(
90        &mut self,
91        data: &WalletClientModuleData,
92        tweak_idx: TweakIdx,
93    ) {
94        while self.next_pending_tweak_idx < tweak_idx {
95            self.generate_next_pending_tweak_idx(data);
96        }
97    }
98
99    pub(crate) fn handle_script(
100        &mut self,
101        data: &WalletClientModuleData,
102        script: &bitcoin::ScriptBuf,
103        session_idx: u64,
104    ) {
105        if let Some(tweak_idx) = self.pending_pubkey_scripts.get(script).copied() {
106            debug!(target: LOG_CLIENT_MODULE_WALLET, %session_idx, ?tweak_idx, "Found previously used tweak_idx in federation history");
107
108            self.used_tweak_idxes.insert(tweak_idx);
109
110            self.refill_pending_pool_up_to_tweak_idx(
111                data,
112                tweak_idx.advance(FEDERATION_RECOVER_MAX_GAP),
113            );
114        } else if self.decoy_session_threshold < session_idx {
115            self.push_decoy(script);
116        }
117    }
118
119    /// Write a someone-elses used deposit address to use a decoy
120    fn push_decoy(&mut self, script: &bitcoin::ScriptBuf) {
121        self.decoys.push_front(script.clone());
122        if 50 < self.decoys.len() {
123            self.decoys.pop_back();
124        }
125    }
126
127    /// Pop a someone-elses used deposit address to use a decoy
128    pub(crate) fn pop_decoy(&mut self) -> Option<bitcoin::ScriptBuf> {
129        self.decoys.pop_front()
130    }
131}