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

bitcoindevkit / bdk / 5246490478

pending completion
5246490478

Pull #1002

github

web-flow
Merge 1bbe18d16 into 8641847e6
Pull Request #1002: Implement linked-list `LocalChain`

618 of 618 new or added lines in 12 files covered. (100.0%)

6547 of 8387 relevant lines covered (78.06%)

6018.74 hits per line

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

78.21
/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
    ForEachTxOut,
7
};
8
use bitcoin::{self, OutPoint, Script, Transaction, TxOut, Txid};
9

10
/// An index storing [`TxOut`]s that have a script pubkey that matches those in a list.
11
///
12
/// The basic idea is that you insert script pubkeys you care about into the index with
13
/// [`insert_spk`] and then when you call [`scan`], the index will look at any txouts you pass in and
14
/// store and index any txouts matching one of its 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
/// [`scan`]: Self::scan
29
/// [`TxGraph`]: crate::tx_graph::TxGraph
30
#[derive(Clone, Debug)]
×
31
pub struct SpkTxOutIndex<I> {
32
    /// script pubkeys ordered by index
33
    spks: BTreeMap<I, Script>,
34
    /// A reverse lookup from spk to spk index
35
    spk_indices: HashMap<Script, I>,
36
    /// The set of unused indexes.
37
    unused: BTreeSet<I>,
38
    /// Lookup index and txout by outpoint.
39
    txouts: BTreeMap<OutPoint, (I, TxOut)>,
40
    /// Lookup from spk index to outpoints that had that spk
41
    spk_txouts: BTreeSet<(I, OutPoint)>,
42
}
43

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

56
impl<I: Clone + Ord> Indexer for SpkTxOutIndex<I> {
57
    type Additions = ();
58

59
    fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions {
×
60
        self.scan_txout(outpoint, txout);
×
61
        Default::default()
×
62
    }
×
63

64
    fn index_tx(&mut self, tx: &Transaction) -> Self::Additions {
×
65
        self.scan(tx);
×
66
        Default::default()
×
67
    }
×
68

69
    fn apply_additions(&mut self, _additions: Self::Additions) {
×
70
        // This applies nothing.
×
71
    }
×
72

73
    fn is_tx_relevant(&self, tx: &Transaction) -> bool {
×
74
        self.is_relevant(tx)
×
75
    }
×
76
}
77

78
/// This macro is used instead of a member function of `SpkTxOutIndex`, which would result in a
79
/// compiler error[E0521]: "borrowed data escapes out of closure" when we attempt to take a
80
/// reference out of the `ForEachTxOut` closure during scanning.
81
macro_rules! scan_txout {
82
    ($self:ident, $op:expr, $txout:expr) => {{
83
        let spk_i = $self.spk_indices.get(&$txout.script_pubkey);
84
        if let Some(spk_i) = spk_i {
85
            $self.txouts.insert($op, (spk_i.clone(), $txout.clone()));
86
            $self.spk_txouts.insert((spk_i.clone(), $op));
87
            $self.unused.remove(&spk_i);
88
        }
89
        spk_i
90
    }};
91
}
92

93
impl<I: Clone + Ord> SpkTxOutIndex<I> {
94
    /// Scans an object containing many txouts.
95
    ///
96
    /// Typically, this is used in two situations:
97
    ///
98
    /// 1. After loading transaction data from the disk, you may scan over all the txouts to restore all
99
    /// your txouts.
100
    /// 2. When getting new data from the chain, you usually scan it before incorporating it into your chain state.
101
    ///
102
    /// See [`ForEachTxout`] for the types that support this.
103
    ///
104
    /// [`ForEachTxout`]: crate::ForEachTxOut
105
    pub fn scan(&mut self, txouts: &impl ForEachTxOut) -> BTreeSet<I> {
2✔
106
        let mut scanned_indices = BTreeSet::new();
2✔
107

2✔
108
        txouts.for_each_txout(|(op, txout)| {
2✔
109
            if let Some(spk_i) = scan_txout!(self, op, txout) {
2✔
110
                scanned_indices.insert(spk_i.clone());
2✔
111
            }
2✔
112
        });
2✔
113

2✔
114
        scanned_indices
2✔
115
    }
2✔
116

117
    /// Scan a single `TxOut` for a matching script pubkey and returns the index that matches the
118
    /// script pubkey (if any).
119
    pub fn scan_txout(&mut self, op: OutPoint, txout: &TxOut) -> Option<&I> {
226✔
120
        scan_txout!(self, op, txout)
226✔
121
    }
226✔
122

123
    /// Get a reference to the set of indexed outpoints.
124
    pub fn outpoints(&self) -> &BTreeSet<(I, OutPoint)> {
155✔
125
        &self.spk_txouts
155✔
126
    }
155✔
127

128
    /// Iterate over all known txouts that spend to tracked script pubkeys.
129
    pub fn txouts(
×
130
        &self,
×
131
    ) -> impl DoubleEndedIterator<Item = (&I, OutPoint, &TxOut)> + ExactSizeIterator {
×
132
        self.txouts
×
133
            .iter()
×
134
            .map(|(op, (index, txout))| (index, *op, txout))
×
135
    }
×
136

137
    /// Finds all txouts on a transaction that has previously been scanned and indexed.
138
    pub fn txouts_in_tx(
×
139
        &self,
×
140
        txid: Txid,
×
141
    ) -> impl DoubleEndedIterator<Item = (&I, OutPoint, &TxOut)> {
×
142
        self.txouts
×
143
            .range(OutPoint::new(txid, u32::MIN)..=OutPoint::new(txid, u32::MAX))
×
144
            .map(|(op, (index, txout))| (index, *op, txout))
×
145
    }
×
146

147
    /// Iterates over all the outputs with script pubkeys in an index range.
148
    pub fn outputs_in_range(
8✔
149
        &self,
8✔
150
        range: impl RangeBounds<I>,
8✔
151
    ) -> impl DoubleEndedIterator<Item = (&I, OutPoint)> {
8✔
152
        use bitcoin::hashes::Hash;
8✔
153
        use core::ops::Bound::*;
8✔
154
        let min_op = OutPoint {
8✔
155
            txid: Txid::from_inner([0x00; 32]),
8✔
156
            vout: u32::MIN,
8✔
157
        };
8✔
158
        let max_op = OutPoint {
8✔
159
            txid: Txid::from_inner([0xff; 32]),
8✔
160
            vout: u32::MAX,
8✔
161
        };
8✔
162

163
        let start = match range.start_bound() {
8✔
164
            Included(index) => Included((index.clone(), min_op)),
8✔
165
            Excluded(index) => Excluded((index.clone(), max_op)),
×
166
            Unbounded => Unbounded,
×
167
        };
168

169
        let end = match range.end_bound() {
8✔
170
            Included(index) => Included((index.clone(), max_op)),
4✔
171
            Excluded(index) => Excluded((index.clone(), min_op)),
4✔
172
            Unbounded => Unbounded,
×
173
        };
174

175
        self.spk_txouts.range((start, end)).map(|(i, op)| (i, *op))
14✔
176
    }
8✔
177

178
    /// Returns the txout and script pubkey index of the `TxOut` at `OutPoint`.
179
    ///
180
    /// Returns `None` if the `TxOut` hasn't been scanned or if nothing matching was found there.
181
    pub fn txout(&self, outpoint: OutPoint) -> Option<(&I, &TxOut)> {
74✔
182
        self.txouts
74✔
183
            .get(&outpoint)
74✔
184
            .map(|(spk_i, txout)| (spk_i, txout))
74✔
185
    }
74✔
186

187
    /// Returns the script that has been inserted at the `index`.
188
    ///
189
    /// If that index hasn't been inserted yet, it will return `None`.
190
    pub fn spk_at_index(&self, index: &I) -> Option<&Script> {
577✔
191
        self.spks.get(index)
577✔
192
    }
577✔
193

194
    /// The script pubkeys that are being tracked by the index.
195
    pub fn all_spks(&self) -> &BTreeMap<I, Script> {
707✔
196
        &self.spks
707✔
197
    }
707✔
198

199
    /// Adds a script pubkey to scan for. Returns `false` and does nothing if spk already exists in the map
200
    ///
201
    /// the index will look for outputs spending to this spk whenever it scans new data.
202
    pub fn insert_spk(&mut self, index: I, spk: Script) -> bool {
425✔
203
        match self.spk_indices.entry(spk.clone()) {
425✔
204
            Entry::Vacant(value) => {
425✔
205
                value.insert(index.clone());
425✔
206
                self.spks.insert(index.clone(), spk);
425✔
207
                self.unused.insert(index);
425✔
208
                true
425✔
209
            }
210
            Entry::Occupied(_) => false,
×
211
        }
212
    }
425✔
213

214
    /// Iterates over all unused script pubkeys in an index range.
215
    ///
216
    /// Here, "unused" means that after the script pubkey was stored in the index, the index has
217
    /// never scanned a transaction output with it.
218
    ///
219
    /// # Example
220
    ///
221
    /// ```rust
222
    /// # use bdk_chain::SpkTxOutIndex;
223
    ///
224
    /// // imagine our spks are indexed like (keychain, derivation_index).
225
    /// let txout_index = SpkTxOutIndex::<(u32, u32)>::default();
226
    /// let all_unused_spks = txout_index.unused_spks(..);
227
    /// let change_index = 1;
228
    /// let unused_change_spks =
229
    ///     txout_index.unused_spks((change_index, u32::MIN)..(change_index, u32::MAX));
230
    /// ```
231
    pub fn unused_spks<R>(&self, range: R) -> impl DoubleEndedIterator<Item = (&I, &Script)>
154✔
232
    where
154✔
233
        R: RangeBounds<I>,
154✔
234
    {
154✔
235
        self.unused
154✔
236
            .range(range)
154✔
237
            .map(move |index| (index, self.spk_at_index(index).expect("must exist")))
322✔
238
    }
154✔
239

240
    /// Returns whether the script pubkey at `index` has been used or not.
241
    ///
242
    /// Here, "unused" means that after the script pubkey was stored in the index, the index has
243
    /// never scanned a transaction output with it.
244
    pub fn is_used(&self, index: &I) -> bool {
5✔
245
        self.unused.get(index).is_none()
5✔
246
    }
5✔
247

248
    /// Marks the script pubkey at `index` as used even though it hasn't seen an output spending to it.
249
    /// This only affects when the `index` had already been added to `self` and was unused.
250
    ///
251
    /// Returns whether the `index` was initially present as `unused`.
252
    ///
253
    /// This is useful when you want to reserve a script pubkey for something but don't want to add
254
    /// the transaction output using it to the index yet. Other callers will consider the `index` used
255
    /// until you call [`unmark_used`].
256
    ///
257
    /// [`unmark_used`]: Self::unmark_used
258
    pub fn mark_used(&mut self, index: &I) -> bool {
129✔
259
        self.unused.remove(index)
129✔
260
    }
129✔
261

262
    /// Undoes the effect of [`mark_used`]. Returns whether the `index` is inserted back into
263
    /// `unused`.
264
    ///
265
    /// Note that if `self` has scanned an output with this script pubkey then this will have no
266
    /// effect.
267
    ///
268
    /// [`mark_used`]: Self::mark_used
269
    pub fn unmark_used(&mut self, index: &I) -> bool {
7✔
270
        // we cannot set the index as unused when it does not exist
7✔
271
        if !self.spks.contains_key(index) {
7✔
272
            return false;
3✔
273
        }
4✔
274
        // we cannot set the index as unused when txouts are indexed under it
4✔
275
        if self.outputs_in_range(index..=index).next().is_some() {
4✔
276
            return false;
1✔
277
        }
3✔
278
        self.unused.insert(index.clone())
3✔
279
    }
7✔
280

281
    /// Returns the index associated with the script pubkey.
282
    pub fn index_of_spk(&self, script: &Script) -> Option<&I> {
7,095✔
283
        self.spk_indices.get(script)
7,095✔
284
    }
7,095✔
285

286
    /// Computes total input value going from script pubkeys in the index (sent) and the total output
287
    /// value going to script pubkeys in the index (received) in `tx`. For the `sent` to be computed
288
    /// correctly, the output being spent must have already been scanned by the index. Calculating
289
    /// received just uses the transaction outputs directly, so it will be correct even if it has not
290
    /// been scanned.
291
    pub fn sent_and_received(&self, tx: &Transaction) -> (u64, u64) {
5✔
292
        let mut sent = 0;
5✔
293
        let mut received = 0;
5✔
294

295
        for txin in &tx.input {
7✔
296
            if let Some((_, txout)) = self.txout(txin.previous_output) {
2✔
297
                sent += txout.value;
2✔
298
            }
2✔
299
        }
300
        for txout in &tx.output {
12✔
301
            if self.index_of_spk(&txout.script_pubkey).is_some() {
7✔
302
                received += txout.value;
7✔
303
            }
7✔
304
        }
305

306
        (sent, received)
5✔
307
    }
5✔
308

309
    /// Computes the net value that this transaction gives to the script pubkeys in the index and
310
    /// *takes* from the transaction outputs in the index. Shorthand for calling
311
    /// [`sent_and_received`] and subtracting sent from received.
312
    ///
313
    /// [`sent_and_received`]: Self::sent_and_received
314
    pub fn net_value(&self, tx: &Transaction) -> i64 {
2✔
315
        let (sent, received) = self.sent_and_received(tx);
2✔
316
        received as i64 - sent as i64
2✔
317
    }
2✔
318

319
    /// Whether any of the inputs of this transaction spend a txout tracked or whether any output
320
    /// matches one of our script pubkeys.
321
    ///
322
    /// It is easily possible to misuse this method and get false negatives by calling it before you
323
    /// have scanned the `TxOut`s the transaction is spending. For example, if you want to filter out
324
    /// all the transactions in a block that are irrelevant, you **must first scan all the
325
    /// transactions in the block** and only then use this method.
326
    pub fn is_relevant(&self, tx: &Transaction) -> bool {
9✔
327
        let input_matches = tx
9✔
328
            .input
9✔
329
            .iter()
9✔
330
            .any(|input| self.txouts.contains_key(&input.previous_output));
9✔
331
        let output_matches = tx
9✔
332
            .output
9✔
333
            .iter()
9✔
334
            .any(|output| self.spk_indices.contains_key(&output.script_pubkey));
9✔
335
        input_matches || output_matches
9✔
336
    }
9✔
337
}
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