• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bitcoindevkit / bdk / 5984736767

26 Aug 2023 12:20PM CUT coverage: 78.856% (+0.2%) from 78.694%
5984736767

Pull #1084

github

web-flow
Merge 8bdb5a43d into 8f978f86b
Pull Request #1084: Enhance bdk chain structures

86 of 86 new or added lines in 5 files covered. (100.0%)

8022 of 10173 relevant lines covered (78.86%)

5091.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.53
/crates/chain/src/spk_txout_index.rs
1
use core::ops::RangeBounds;
2

3
use crate::{
4
    collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap},
5
    indexed_tx_graph::Indexer,
6
};
7
use bitcoin::{self, OutPoint, Script, ScriptBuf, Transaction, TxOut, Txid};
8

9
/// An index storing [`TxOut`]s that have a script pubkey that matches those in a list.
10
///
11
/// The basic idea is that you insert script pubkeys you care about into the index with
12
/// [`insert_spk`] and then when you call [`Indexer::index_tx`] or [`Indexer::index_txout`], the
13
/// index will look at any txouts you pass in and store and index any txouts matching one of its
14
/// script pubkeys.
15
///
16
/// Each script pubkey is associated with an application-defined index script index `I`, which must be
17
/// [`Ord`]. Usually, this is used to associate the derivation index of the script pubkey or even a
18
/// combination of `(keychain, derivation_index)`.
19
///
20
/// Note there is no harm in scanning transactions that disappear from the blockchain or were never
21
/// in there in the first place. `SpkTxOutIndex` is intentionally *monotone* -- you cannot delete or
22
/// modify txouts that have been indexed. To find out which txouts from the index are actually in the
23
/// chain or unspent, you must use other sources of information like a [`TxGraph`].
24
///
25
/// [`TxOut`]: bitcoin::TxOut
26
/// [`insert_spk`]: Self::insert_spk
27
/// [`Ord`]: core::cmp::Ord
28
/// [`TxGraph`]: crate::tx_graph::TxGraph
29
#[derive(Clone, Debug)]
×
30
pub struct SpkTxOutIndex<I> {
31
    /// script pubkeys ordered by index
32
    spks: BTreeMap<I, ScriptBuf>,
33
    /// A reverse lookup from spk to spk index
34
    spk_indices: HashMap<ScriptBuf, I>,
35
    /// The set of unused indexes.
36
    unused: BTreeSet<I>,
37
    /// Lookup index and txout by outpoint.
38
    txouts: BTreeMap<OutPoint, (I, TxOut)>,
39
    /// Lookup from spk index to outpoints that had that spk
40
    spk_txouts: BTreeSet<(I, OutPoint)>,
41
}
42

43
impl<I> Default for SpkTxOutIndex<I> {
44
    fn default() -> Self {
148✔
45
        Self {
148✔
46
            txouts: Default::default(),
148✔
47
            spks: Default::default(),
148✔
48
            spk_indices: Default::default(),
148✔
49
            spk_txouts: Default::default(),
148✔
50
            unused: Default::default(),
148✔
51
        }
148✔
52
    }
148✔
53
}
54

55
impl<I: Clone + Ord> Indexer for SpkTxOutIndex<I> {
56
    type ChangeSet = BTreeSet<I>;
57

58
    fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet {
228✔
59
        let spk_i = self.spk_indices.get(&txout.script_pubkey);
228✔
60
        let mut scanned_indices = BTreeSet::new();
228✔
61
        if let Some(spk_i) = spk_i {
228✔
62
            self.txouts.insert(outpoint, (spk_i.clone(), txout.clone()));
214✔
63
            self.spk_txouts.insert((spk_i.clone(), outpoint));
214✔
64
            self.unused.remove(spk_i);
214✔
65
            scanned_indices.insert(spk_i.clone());
214✔
66
        }
214✔
67
        scanned_indices
228✔
68
    }
228✔
69

70
    fn index_tx(&mut self, tx: &Transaction) -> Self::ChangeSet {
2✔
71
        let mut scanned_indices = BTreeSet::new();
2✔
72

73
        for (i, txout) in tx.output.iter().enumerate() {
2✔
74
            let op = OutPoint::new(tx.txid(), i as u32);
2✔
75
            let mut txout_indices = self.index_txout(op, txout);
2✔
76
            scanned_indices.append(&mut txout_indices);
2✔
77
        }
2✔
78

79
        scanned_indices
2✔
80
    }
2✔
81

82
    fn initial_changeset(&self) -> Self::ChangeSet {
×
83
        self.spks.keys().cloned().collect()
×
84
    }
×
85

86
    fn apply_changeset(&mut self, _changeset: Self::ChangeSet) {
×
87
        // This applies nothing.
×
88
    }
×
89

90
    fn is_tx_relevant(&self, tx: &Transaction) -> bool {
×
91
        self.is_relevant(tx)
×
92
    }
×
93
}
94

95
impl<I: Clone + Ord> SpkTxOutIndex<I> {
96
    /// Get a reference to the set of indexed outpoints.
97
    pub fn outpoints(&self) -> &BTreeSet<(I, OutPoint)> {
155✔
98
        &self.spk_txouts
155✔
99
    }
155✔
100

101
    /// Iterate over all known txouts that spend to tracked script pubkeys.
102
    pub fn txouts(
×
103
        &self,
×
104
    ) -> impl DoubleEndedIterator<Item = (&I, OutPoint, &TxOut)> + ExactSizeIterator {
×
105
        self.txouts
×
106
            .iter()
×
107
            .map(|(op, (index, txout))| (index, *op, txout))
×
108
    }
×
109

110
    /// Finds all txouts on a transaction that has previously been scanned and indexed.
111
    pub fn txouts_in_tx(
×
112
        &self,
×
113
        txid: Txid,
×
114
    ) -> impl DoubleEndedIterator<Item = (&I, OutPoint, &TxOut)> {
×
115
        self.txouts
×
116
            .range(OutPoint::new(txid, u32::MIN)..=OutPoint::new(txid, u32::MAX))
×
117
            .map(|(op, (index, txout))| (index, *op, txout))
×
118
    }
×
119

120
    /// Iterates over all the outputs with script pubkeys in an index range.
121
    pub fn outputs_in_range(
8✔
122
        &self,
8✔
123
        range: impl RangeBounds<I>,
8✔
124
    ) -> impl DoubleEndedIterator<Item = (&I, OutPoint)> {
8✔
125
        use bitcoin::hashes::Hash;
8✔
126
        use core::ops::Bound::*;
8✔
127
        let min_op = OutPoint {
8✔
128
            txid: Txid::all_zeros(),
8✔
129
            vout: u32::MIN,
8✔
130
        };
8✔
131
        let max_op = OutPoint {
8✔
132
            txid: Txid::from_byte_array([0xff; Txid::LEN]),
8✔
133
            vout: u32::MAX,
8✔
134
        };
8✔
135

136
        let start = match range.start_bound() {
8✔
137
            Included(index) => Included((index.clone(), min_op)),
8✔
138
            Excluded(index) => Excluded((index.clone(), max_op)),
×
139
            Unbounded => Unbounded,
×
140
        };
141

142
        let end = match range.end_bound() {
8✔
143
            Included(index) => Included((index.clone(), max_op)),
4✔
144
            Excluded(index) => Excluded((index.clone(), min_op)),
4✔
145
            Unbounded => Unbounded,
×
146
        };
147

148
        self.spk_txouts.range((start, end)).map(|(i, op)| (i, *op))
14✔
149
    }
8✔
150

151
    /// Returns the txout and script pubkey index of the `TxOut` at `OutPoint`.
152
    ///
153
    /// Returns `None` if the `TxOut` hasn't been scanned or if nothing matching was found there.
154
    pub fn txout(&self, outpoint: OutPoint) -> Option<(&I, &TxOut)> {
74✔
155
        self.txouts
74✔
156
            .get(&outpoint)
74✔
157
            .map(|(spk_i, txout)| (spk_i, txout))
74✔
158
    }
74✔
159

160
    /// Returns the script that has been inserted at the `index`.
161
    ///
162
    /// If that index hasn't been inserted yet, it will return `None`.
163
    pub fn spk_at_index(&self, index: &I) -> Option<&Script> {
577✔
164
        self.spks.get(index).map(|s| s.as_script())
577✔
165
    }
577✔
166

167
    /// The script pubkeys that are being tracked by the index.
168
    pub fn all_spks(&self) -> &BTreeMap<I, ScriptBuf> {
708✔
169
        &self.spks
708✔
170
    }
708✔
171

172
    /// Adds a script pubkey to scan for. Returns `false` and does nothing if spk already exists in the map
173
    ///
174
    /// the index will look for outputs spending to this spk whenever it scans new data.
175
    pub fn insert_spk(&mut self, index: I, spk: ScriptBuf) -> bool {
425✔
176
        match self.spk_indices.entry(spk.clone()) {
425✔
177
            Entry::Vacant(value) => {
425✔
178
                value.insert(index.clone());
425✔
179
                self.spks.insert(index.clone(), spk);
425✔
180
                self.unused.insert(index);
425✔
181
                true
425✔
182
            }
183
            Entry::Occupied(_) => false,
×
184
        }
185
    }
425✔
186

187
    /// Iterates over all unused script pubkeys in an index range.
188
    ///
189
    /// Here, "unused" means that after the script pubkey was stored in the index, the index has
190
    /// never scanned a transaction output with it.
191
    ///
192
    /// # Example
193
    ///
194
    /// ```rust
195
    /// # use bdk_chain::SpkTxOutIndex;
196
    ///
197
    /// // imagine our spks are indexed like (keychain, derivation_index).
198
    /// let txout_index = SpkTxOutIndex::<(u32, u32)>::default();
199
    /// let all_unused_spks = txout_index.unused_spks(..);
200
    /// let change_index = 1;
201
    /// let unused_change_spks =
202
    ///     txout_index.unused_spks((change_index, u32::MIN)..(change_index, u32::MAX));
203
    /// ```
204
    pub fn unused_spks<R>(&self, range: R) -> impl DoubleEndedIterator<Item = (&I, &Script)>
154✔
205
    where
154✔
206
        R: RangeBounds<I>,
154✔
207
    {
154✔
208
        self.unused
154✔
209
            .range(range)
154✔
210
            .map(move |index| (index, self.spk_at_index(index).expect("must exist")))
322✔
211
    }
154✔
212

213
    /// Returns whether the script pubkey at `index` has been used or not.
214
    ///
215
    /// Here, "unused" means that after the script pubkey was stored in the index, the index has
216
    /// never scanned a transaction output with it.
217
    pub fn is_used(&self, index: &I) -> bool {
5✔
218
        self.unused.get(index).is_none()
5✔
219
    }
5✔
220

221
    /// Marks the script pubkey at `index` as used even though it hasn't seen an output spending to it.
222
    /// This only affects when the `index` had already been added to `self` and was unused.
223
    ///
224
    /// Returns whether the `index` was initially present as `unused`.
225
    ///
226
    /// This is useful when you want to reserve a script pubkey for something but don't want to add
227
    /// the transaction output using it to the index yet. Other callers will consider the `index` used
228
    /// until you call [`unmark_used`].
229
    ///
230
    /// [`unmark_used`]: Self::unmark_used
231
    pub fn mark_used(&mut self, index: &I) -> bool {
129✔
232
        self.unused.remove(index)
129✔
233
    }
129✔
234

235
    /// Undoes the effect of [`mark_used`]. Returns whether the `index` is inserted back into
236
    /// `unused`.
237
    ///
238
    /// Note that if `self` has scanned an output with this script pubkey then this will have no
239
    /// effect.
240
    ///
241
    /// [`mark_used`]: Self::mark_used
242
    pub fn unmark_used(&mut self, index: &I) -> bool {
7✔
243
        // we cannot set the index as unused when it does not exist
7✔
244
        if !self.spks.contains_key(index) {
7✔
245
            return false;
3✔
246
        }
4✔
247
        // we cannot set the index as unused when txouts are indexed under it
4✔
248
        if self.outputs_in_range(index..=index).next().is_some() {
4✔
249
            return false;
1✔
250
        }
3✔
251
        self.unused.insert(index.clone())
3✔
252
    }
7✔
253

254
    /// Returns the index associated with the script pubkey.
255
    pub fn index_of_spk(&self, script: &Script) -> Option<&I> {
7,079✔
256
        self.spk_indices.get(script)
7,079✔
257
    }
7,079✔
258

259
    /// Computes total input value going from script pubkeys in the index (sent) and the total output
260
    /// value going to script pubkeys in the index (received) in `tx`. For the `sent` to be computed
261
    /// correctly, the output being spent must have already been scanned by the index. Calculating
262
    /// received just uses the transaction outputs directly, so it will be correct even if it has not
263
    /// been scanned.
264
    pub fn sent_and_received(&self, tx: &Transaction) -> (u64, u64) {
5✔
265
        let mut sent = 0;
5✔
266
        let mut received = 0;
5✔
267

268
        for txin in &tx.input {
7✔
269
            if let Some((_, txout)) = self.txout(txin.previous_output) {
2✔
270
                sent += txout.value;
2✔
271
            }
2✔
272
        }
273
        for txout in &tx.output {
12✔
274
            if self.index_of_spk(&txout.script_pubkey).is_some() {
7✔
275
                received += txout.value;
7✔
276
            }
7✔
277
        }
278

279
        (sent, received)
5✔
280
    }
5✔
281

282
    /// Computes the net value that this transaction gives to the script pubkeys in the index and
283
    /// *takes* from the transaction outputs in the index. Shorthand for calling
284
    /// [`sent_and_received`] and subtracting sent from received.
285
    ///
286
    /// [`sent_and_received`]: Self::sent_and_received
287
    pub fn net_value(&self, tx: &Transaction) -> i64 {
2✔
288
        let (sent, received) = self.sent_and_received(tx);
2✔
289
        received as i64 - sent as i64
2✔
290
    }
2✔
291

292
    /// Whether any of the inputs of this transaction spend a txout tracked or whether any output
293
    /// matches one of our script pubkeys.
294
    ///
295
    /// It is easily possible to misuse this method and get false negatives by calling it before you
296
    /// have scanned the `TxOut`s the transaction is spending. For example, if you want to filter out
297
    /// all the transactions in a block that are irrelevant, you **must first scan all the
298
    /// transactions in the block** and only then use this method.
299
    pub fn is_relevant(&self, tx: &Transaction) -> bool {
9✔
300
        let input_matches = tx
9✔
301
            .input
9✔
302
            .iter()
9✔
303
            .any(|input| self.txouts.contains_key(&input.previous_output));
9✔
304
        let output_matches = tx
9✔
305
            .output
9✔
306
            .iter()
9✔
307
            .any(|output| self.spk_indices.contains_key(&output.script_pubkey));
9✔
308
        input_matches || output_matches
9✔
309
    }
9✔
310
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc