bdk_chain/indexer/
spk_txout.rs

1//! [`SpkTxOutIndex`] is an index storing [`TxOut`]s that have a script pubkey that matches those in
2//! a list.
3
4use core::ops::RangeBounds;
5
6use crate::{
7    collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap},
8    Indexer,
9};
10use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
11
12/// An index storing [`TxOut`]s that have a script pubkey that matches those in a list.
13///
14/// The basic idea is that you insert script pubkeys you care about into the index with
15/// [`insert_spk`] and then when you call [`Indexer::index_tx`] or [`Indexer::index_txout`], the
16/// index will look at any txouts you pass in and store and index any txouts matching one of its
17/// script pubkeys.
18///
19/// Each script pubkey is associated with an application-defined index script index `I`, which must
20/// be [`Ord`]. Usually, this is used to associate the derivation index of the script pubkey or even
21/// a combination of `(keychain, derivation_index)`.
22///
23/// Note there is no harm in scanning transactions that disappear from the blockchain or were never
24/// in there in the first place. `SpkTxOutIndex` is intentionally *monotone* -- you cannot delete or
25/// modify txouts that have been indexed. To find out which txouts from the index are actually in
26/// the chain or unspent, you must use other sources of information like a [`TxGraph`].
27///
28/// [`TxOut`]: bitcoin::TxOut
29/// [`insert_spk`]: Self::insert_spk
30/// [`Ord`]: core::cmp::Ord
31/// [`TxGraph`]: crate::tx_graph::TxGraph
32#[derive(Clone, Debug)]
33pub struct SpkTxOutIndex<I> {
34    /// script pubkeys ordered by index
35    spks: BTreeMap<I, ScriptBuf>,
36    /// A reverse lookup from spk to spk index
37    spk_indices: HashMap<ScriptBuf, I>,
38    /// The set of unused indexes.
39    unused: BTreeSet<I>,
40    /// Lookup index and txout by outpoint.
41    txouts: BTreeMap<OutPoint, (I, TxOut)>,
42    /// Lookup from spk index to outpoints that had that spk
43    spk_txouts: BTreeSet<(I, OutPoint)>,
44}
45
46impl<I> Default for SpkTxOutIndex<I> {
47    fn default() -> Self {
48        Self {
49            txouts: Default::default(),
50            spks: Default::default(),
51            spk_indices: Default::default(),
52            spk_txouts: Default::default(),
53            unused: Default::default(),
54        }
55    }
56}
57
58impl<I> AsRef<SpkTxOutIndex<I>> for SpkTxOutIndex<I> {
59    fn as_ref(&self) -> &SpkTxOutIndex<I> {
60        self
61    }
62}
63
64impl<I: Clone + Ord + core::fmt::Debug> Indexer for SpkTxOutIndex<I> {
65    type ChangeSet = ();
66
67    fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet {
68        self.scan_txout(outpoint, txout);
69        Default::default()
70    }
71
72    fn index_tx(&mut self, tx: &Transaction) -> Self::ChangeSet {
73        self.scan(tx);
74        Default::default()
75    }
76
77    fn initial_changeset(&self) -> Self::ChangeSet {}
78
79    fn apply_changeset(&mut self, _changeset: Self::ChangeSet) {
80        // This applies nothing.
81    }
82
83    fn is_tx_relevant(&self, tx: &Transaction) -> bool {
84        self.is_relevant(tx)
85    }
86}
87
88impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
89    /// Scans a transaction's outputs for matching script pubkeys.
90    ///
91    /// Typically, this is used in two situations:
92    ///
93    /// 1. After loading transaction data from the disk, you may scan over all the txouts to restore
94    ///    all your txouts.
95    /// 2. When getting new data from the chain, you usually scan it before incorporating it into
96    ///    your chain state.
97    pub fn scan(&mut self, tx: &Transaction) -> BTreeSet<I> {
98        let mut scanned_indices = BTreeSet::new();
99        let txid = tx.compute_txid();
100        for (i, txout) in tx.output.iter().enumerate() {
101            let op = OutPoint::new(txid, i as u32);
102            if let Some(spk_i) = self.scan_txout(op, txout) {
103                scanned_indices.insert(spk_i.clone());
104            }
105        }
106
107        scanned_indices
108    }
109
110    /// Scan a single `TxOut` for a matching script pubkey and returns the index that matches the
111    /// script pubkey (if any).
112    pub fn scan_txout(&mut self, op: OutPoint, txout: &TxOut) -> Option<&I> {
113        let spk_i = self.spk_indices.get(&txout.script_pubkey);
114        if let Some(spk_i) = spk_i {
115            self.txouts.insert(op, (spk_i.clone(), txout.clone()));
116            self.spk_txouts.insert((spk_i.clone(), op));
117            self.unused.remove(spk_i);
118        }
119        spk_i
120    }
121
122    /// Get a reference to the set of indexed outpoints.
123    pub fn outpoints(&self) -> &BTreeSet<(I, OutPoint)> {
124        &self.spk_txouts
125    }
126
127    /// Iterate over all known txouts that spend to tracked script pubkeys.
128    pub fn txouts(
129        &self,
130    ) -> impl DoubleEndedIterator<Item = (&I, OutPoint, &TxOut)> + ExactSizeIterator {
131        self.txouts
132            .iter()
133            .map(|(op, (index, txout))| (index, *op, txout))
134    }
135
136    /// Finds all txouts on a transaction that has previously been scanned and indexed.
137    pub fn txouts_in_tx(
138        &self,
139        txid: Txid,
140    ) -> impl DoubleEndedIterator<Item = (&I, OutPoint, &TxOut)> {
141        self.txouts
142            .range(OutPoint::new(txid, u32::MIN)..=OutPoint::new(txid, u32::MAX))
143            .map(|(op, (index, txout))| (index, *op, txout))
144    }
145
146    /// Iterates over all the outputs with script pubkeys in an index range.
147    pub fn outputs_in_range(
148        &self,
149        range: impl RangeBounds<I>,
150    ) -> impl DoubleEndedIterator<Item = (&I, OutPoint)> {
151        use bitcoin::hashes::Hash;
152        use core::ops::Bound::*;
153        let min_op = OutPoint {
154            txid: Txid::all_zeros(),
155            vout: u32::MIN,
156        };
157        let max_op = OutPoint {
158            txid: Txid::from_byte_array([0xff; Txid::LEN]),
159            vout: u32::MAX,
160        };
161
162        let start = match range.start_bound() {
163            Included(index) => Included((index.clone(), min_op)),
164            Excluded(index) => Excluded((index.clone(), max_op)),
165            Unbounded => Unbounded,
166        };
167
168        let end = match range.end_bound() {
169            Included(index) => Included((index.clone(), max_op)),
170            Excluded(index) => Excluded((index.clone(), min_op)),
171            Unbounded => Unbounded,
172        };
173
174        self.spk_txouts.range((start, end)).map(|(i, op)| (i, *op))
175    }
176
177    /// Returns the txout and script pubkey index of the `TxOut` at `OutPoint`.
178    ///
179    /// Returns `None` if the `TxOut` hasn't been scanned or if nothing matching was found there.
180    pub fn txout(&self, outpoint: OutPoint) -> Option<(&I, &TxOut)> {
181        self.txouts.get(&outpoint).map(|v| (&v.0, &v.1))
182    }
183
184    /// Returns the script that has been inserted at the `index`.
185    ///
186    /// If that index hasn't been inserted yet, it will return `None`.
187    pub fn spk_at_index(&self, index: &I) -> Option<ScriptBuf> {
188        self.spks.get(index).cloned()
189    }
190
191    /// The script pubkeys that are being tracked by the index.
192    pub fn all_spks(&self) -> &BTreeMap<I, ScriptBuf> {
193        &self.spks
194    }
195
196    /// Adds a script pubkey to scan for. Returns `false` and does nothing if spk already exists in
197    /// the map
198    ///
199    /// the index will look for outputs spending to this spk whenever it scans new data.
200    pub fn insert_spk(&mut self, index: I, spk: ScriptBuf) -> bool {
201        match self.spk_indices.entry(spk.clone()) {
202            Entry::Vacant(value) => {
203                value.insert(index.clone());
204                self.spks.insert(index.clone(), spk);
205                self.unused.insert(index);
206                true
207            }
208            Entry::Occupied(_) => false,
209        }
210    }
211
212    /// Iterates over all unused script pubkeys in an index range.
213    ///
214    /// Here, "unused" means that after the script pubkey was stored in the index, the index has
215    /// never scanned a transaction output with it.
216    ///
217    /// # Example
218    ///
219    /// ```rust
220    /// # use bdk_chain::spk_txout::SpkTxOutIndex;
221    ///
222    /// // imagine our spks are indexed like (keychain, derivation_index).
223    /// let txout_index = SpkTxOutIndex::<(u32, u32)>::default();
224    /// let all_unused_spks = txout_index.unused_spks(..);
225    /// let change_index = 1;
226    /// let unused_change_spks =
227    ///     txout_index.unused_spks((change_index, u32::MIN)..(change_index, u32::MAX));
228    /// ```
229    pub fn unused_spks<R>(
230        &self,
231        range: R,
232    ) -> impl DoubleEndedIterator<Item = (&I, ScriptBuf)> + Clone + '_
233    where
234        R: RangeBounds<I>,
235    {
236        self.unused
237            .range(range)
238            .map(move |index| (index, self.spk_at_index(index).expect("must exist")))
239    }
240
241    /// Returns whether the script pubkey at `index` has been used or not.
242    ///
243    /// Here, "unused" means that after the script pubkey was stored in the index, the index has
244    /// never scanned a transaction output with it.
245    pub fn is_used(&self, index: &I) -> bool {
246        !self.unused.contains(index)
247    }
248
249    /// Marks the script pubkey at `index` as used even though it hasn't seen an output spending to
250    /// it. This only affects when the `index` had already been added to `self` and was unused.
251    ///
252    /// Returns whether the `index` was initially present as `unused`.
253    ///
254    /// This is useful when you want to reserve a script pubkey for something but don't want to add
255    /// the transaction output using it to the index yet. Other callers will consider the `index`
256    /// used until you call [`unmark_used`].
257    ///
258    /// [`unmark_used`]: Self::unmark_used
259    pub fn mark_used(&mut self, index: &I) -> bool {
260        self.unused.remove(index)
261    }
262
263    /// Undoes the effect of [`mark_used`]. Returns whether the `index` is inserted back into
264    /// `unused`.
265    ///
266    /// Note that if `self` has scanned an output with this script pubkey then this will have no
267    /// effect.
268    ///
269    /// [`mark_used`]: Self::mark_used
270    pub fn unmark_used(&mut self, index: &I) -> bool {
271        // we cannot set the index as unused when it does not exist
272        if !self.spks.contains_key(index) {
273            return false;
274        }
275        // we cannot set the index as unused when txouts are indexed under it
276        if self.outputs_in_range(index..=index).next().is_some() {
277            return false;
278        }
279        self.unused.insert(index.clone())
280    }
281
282    /// Returns the index associated with the script pubkey.
283    pub fn index_of_spk(&self, script: ScriptBuf) -> Option<&I> {
284        self.spk_indices.get(script.as_script())
285    }
286
287    /// Computes the total value transfer effect `tx` has on the script pubkeys in `range`. Value is
288    /// *sent* when a script pubkey in the `range` is on an input and *received* when it is on an
289    /// output. For `sent` to be computed correctly, the output being spent must have already been
290    /// scanned by the index. Calculating received just uses the [`Transaction`] outputs directly,
291    /// so it will be correct even if it has not been scanned.
292    pub fn sent_and_received(
293        &self,
294        tx: &Transaction,
295        range: impl RangeBounds<I>,
296    ) -> (Amount, Amount) {
297        let mut sent = Amount::ZERO;
298        let mut received = Amount::ZERO;
299
300        for txin in &tx.input {
301            if let Some((index, txout)) = self.txout(txin.previous_output) {
302                if range.contains(index) {
303                    sent += txout.value;
304                }
305            }
306        }
307        for txout in &tx.output {
308            if let Some(index) = self.index_of_spk(txout.script_pubkey.clone()) {
309                if range.contains(index) {
310                    received += txout.value;
311                }
312            }
313        }
314
315        (sent, received)
316    }
317
318    /// Computes the net value transfer effect of `tx` on the script pubkeys in `range`. Shorthand
319    /// for calling [`sent_and_received`] and subtracting sent from received.
320    ///
321    /// [`sent_and_received`]: Self::sent_and_received
322    pub fn net_value(&self, tx: &Transaction, range: impl RangeBounds<I>) -> SignedAmount {
323        let (sent, received) = self.sent_and_received(tx, range);
324        received.to_signed().expect("valid `SignedAmount`")
325            - sent.to_signed().expect("valid `SignedAmount`")
326    }
327
328    /// Whether any of the inputs of this transaction spend a txout tracked or whether any output
329    /// matches one of our script pubkeys.
330    ///
331    /// It is easily possible to misuse this method and get false negatives by calling it before you
332    /// have scanned the `TxOut`s the transaction is spending. For example, if you want to filter
333    /// out all the transactions in a block that are irrelevant, you **must first scan all the
334    /// transactions in the block** and only then use this method.
335    pub fn is_relevant(&self, tx: &Transaction) -> bool {
336        let input_matches = tx
337            .input
338            .iter()
339            .any(|input| self.txouts.contains_key(&input.previous_output));
340        let output_matches = tx
341            .output
342            .iter()
343            .any(|output| self.spk_indices.contains_key(&output.script_pubkey));
344        input_matches || output_matches
345    }
346
347    /// Find relevant script pubkeys associated with a transaction for tracking and validation.
348    ///
349    /// Returns a set of script pubkeys from [`SpkTxOutIndex`] that are relevant to the outputs and
350    /// previous outputs of a given transaction. Inputs are only considered relevant if the parent
351    /// transactions have been scanned.
352    pub fn relevant_spks_of_tx(&self, tx: &Transaction) -> BTreeSet<(I, ScriptBuf)> {
353        let spks_from_inputs = tx.input.iter().filter_map(|txin| {
354            self.txouts
355                .get(&txin.previous_output)
356                .cloned()
357                .map(|(i, prev_txo)| (i, prev_txo.script_pubkey))
358        });
359        let spks_from_outputs = tx
360            .output
361            .iter()
362            .filter_map(|txout| self.spk_indices.get_key_value(&txout.script_pubkey))
363            .map(|(spk, i)| (i.clone(), spk.clone()));
364        spks_from_inputs.chain(spks_from_outputs).collect()
365    }
366}