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}