fedimint_wallet_client/backup/
recovery_history_tracker.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
use std::collections::{BTreeMap, BTreeSet, VecDeque};

use fedimint_core::encoding::{Decodable, Encodable};
use fedimint_logging::LOG_CLIENT_MODULE_WALLET;
use tracing::debug;

use crate::backup::FEDERATION_RECOVER_MAX_GAP;
use crate::client_db::TweakIdx;
use crate::WalletClientModuleData;

/// Tracks addresses `TweakIdx`s/addresses that are expected to have been used
/// against the stream of addresses that were actually used for peg-ins in the
/// Federation.
///
/// Since replaying Federation history is entirely private, the goal here
/// is to find the last peg-in address already used without compromising
/// privacy like when querying Bitcoin node.
///
/// While at it, collect some addresses that were actually used for peg-ins by
/// other clients, just to query for them as decoys and thus hopefully make the
/// malicious Bitcoin node operator have less confidence about which addresses
/// are actually linked with each other.
#[derive(Clone, Debug, Encodable, Decodable)]
pub struct ConsensusPegInTweakIdxesUsedTracker {
    /// Any time we detect one of the scripts in `pending_pubkey_scripts` was
    /// used we insert the `tweak_idx`, so we can skip asking network about
    /// them (which would be bad for privacy)
    used_tweak_idxes: BTreeSet<TweakIdx>,
    /// All the pubkey scripts we are looking for in the federation history, to
    /// detect previous successful peg-ins.
    pending_pubkey_scripts: BTreeMap<bitcoin::ScriptBuf, TweakIdx>,
    /// Next tweak idx to add to `pending_pubkey_scripts`
    next_pending_tweak_idx: TweakIdx,

    /// Collection of recent scripts from federation history that do not belong
    /// to us
    decoys: VecDeque<bitcoin::ScriptBuf>,
    // To avoid updating `decoys` for the whole recovery, which might be a lot of extra updates
    // most of which will be thrown away, ignore script pubkeys from before this `session_idx`
    decoy_session_threshold: u64,
}

impl ConsensusPegInTweakIdxesUsedTracker {
    pub(crate) fn new(
        previous_next_unused_idx: TweakIdx,
        start_session_idx: u64,
        current_session_count: u64,
        data: &WalletClientModuleData,
    ) -> Self {
        debug_assert!(start_session_idx <= current_session_count);

        let mut s = Self {
            next_pending_tweak_idx: previous_next_unused_idx,
            pending_pubkey_scripts: BTreeMap::new(),
            decoys: VecDeque::new(),
            decoy_session_threshold: current_session_count
                .saturating_sub((current_session_count.saturating_sub(current_session_count)) / 20),
            used_tweak_idxes: BTreeSet::new(),
        };

        s.init(data);

        s
    }

    fn init(&mut self, data: &WalletClientModuleData) {
        for _ in 0..super::ONCHAIN_RECOVER_MAX_GAP {
            self.generate_next_pending_tweak_idx(data);
        }
        debug_assert_eq!(
            self.pending_pubkey_scripts.len(),
            super::ONCHAIN_RECOVER_MAX_GAP as usize
        );
    }

    pub fn used_tweak_idxes(&self) -> &BTreeSet<TweakIdx> {
        &self.used_tweak_idxes
    }

    fn generate_next_pending_tweak_idx(&mut self, data: &WalletClientModuleData) {
        let (script, _address, _tweak_key, _operation_id) =
            data.derive_peg_in_script(self.next_pending_tweak_idx);

        self.pending_pubkey_scripts
            .insert(script, self.next_pending_tweak_idx);
        self.next_pending_tweak_idx = self.next_pending_tweak_idx.next();
    }

    fn refill_pending_pool_up_to_tweak_idx(
        &mut self,
        data: &WalletClientModuleData,
        tweak_idx: TweakIdx,
    ) {
        while self.next_pending_tweak_idx < tweak_idx {
            self.generate_next_pending_tweak_idx(data);
        }
    }

    pub(crate) fn handle_script(
        &mut self,
        data: &WalletClientModuleData,
        script: &bitcoin::ScriptBuf,
        session_idx: u64,
    ) {
        if let Some(tweak_idx) = self.pending_pubkey_scripts.get(script).copied() {
            debug!(target: LOG_CLIENT_MODULE_WALLET, %session_idx, ?tweak_idx, "Found previously used tweak_idx in federation history");

            self.used_tweak_idxes.insert(tweak_idx);

            self.refill_pending_pool_up_to_tweak_idx(
                data,
                tweak_idx.advance(FEDERATION_RECOVER_MAX_GAP),
            );
        } else if self.decoy_session_threshold < session_idx {
            self.push_decoy(script);
        }
    }

    /// Write a someone-elses used deposit address to use a decoy
    fn push_decoy(&mut self, script: &bitcoin::ScriptBuf) {
        self.decoys.push_front(script.clone());
        if 50 < self.decoys.len() {
            self.decoys.pop_back();
        }
    }

    /// Pop a someone-elses used deposit address to use a decoy
    pub(crate) fn pop_decoy(&mut self) -> Option<bitcoin::ScriptBuf> {
        self.decoys.pop_front()
    }
}