Skip to content

Commit 52f3955

Browse files
committed
Merge #1324: [chain] Make KeychainTxOutIndex more range based
fac2283 feat(chain)!: make `KeychainTxOutIndex` more range based (LLFourn) Pull request description: KeychainTxOut index should try and avoid "all" kind of queries. There may be subranges of interest. If the user wants "all" they can just query "..". The ideas is that KeychainTxOutIndex should be designed to be able to incorporate many unrelated keychains that can be managed in the same index. We should be able to see the "net_value" of a transaction to a specific subrange. e.g. imagine a collaborative custody service that manages all their user descriptors inside the same `KeychainTxOutIndex`. One user in their service may pay another so when you are analyzing how much a transaction is spending for a particular user you need to do analyze a particular sub-range. ### Notes to the reviewers - I didn't change `unused_spks` to follow this rule because I want to delete that method some time in the future. `unused_spks` is being used in the examples for syncing but it shouldn't be (the discussion as to why will probably surface in #1194). - I haven't applied this reasoning to the methods that return `BTreeMap`s e.g. `all_unbounded_spk_iters`. It probably should be but I haven't made up my mind yet. This probably belongs after #1194 ### Changelog notice - `KeychainTxOutIndex` methods modified to take ranges of keychains instead. ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo fmt` and `cargo clippy` before committing #### New Features: * [x] I've added tests for the new feature * [x] I've added docs for the new feature ACKs for top commit: evanlinjin: ACK fac2283 Tree-SHA512: ec1e75f19d79f71de4b6d7748ef6da076ca92c2f3fd07e0f0dc88e091bf80c61268880ef78be4bed5e0dbab2572e22028f868f33e68a67d47813195d38d78ba5
2 parents ee21ffe + fac2283 commit 52f3955

File tree

6 files changed

+91
-75
lines changed

6 files changed

+91
-75
lines changed

crates/bdk/src/wallet/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1015,7 +1015,7 @@ impl<D> Wallet<D> {
10151015
/// let (sent, received) = wallet.sent_and_received(tx);
10161016
/// ```
10171017
pub fn sent_and_received(&self, tx: &Transaction) -> (u64, u64) {
1018-
self.indexed_graph.index.sent_and_received(tx)
1018+
self.indexed_graph.index.sent_and_received(tx, ..)
10191019
}
10201020

10211021
/// Get a single transaction from the wallet as a [`CanonicalTx`] (if the transaction exists).

crates/chain/src/keychain/txout_index.rs

Lines changed: 59 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -268,15 +268,14 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
268268
self.inner.unmark_used(&(keychain, index))
269269
}
270270

271-
/// Computes total input value going from script pubkeys in the index (sent) and the total output
272-
/// value going to script pubkeys in the index (received) in `tx`. For the `sent` to be computed
273-
/// correctly, the output being spent must have already been scanned by the index. Calculating
274-
/// received just uses the [`Transaction`] outputs directly, so it will be correct even if it has
275-
/// not been scanned.
276-
///
277-
/// This calls [`SpkTxOutIndex::sent_and_received`] internally.
278-
pub fn sent_and_received(&self, tx: &Transaction) -> (u64, u64) {
279-
self.inner.sent_and_received(tx)
271+
/// Computes the total value transfer effect `tx` has on the script pubkeys belonging to the
272+
/// keychains in `range`. Value is *sent* when a script pubkey in the `range` is on an input and
273+
/// *received* when it is on an output. For `sent` to be computed correctly, the output being
274+
/// spent must have already been scanned by the index. Calculating received just uses the
275+
/// [`Transaction`] outputs directly, so it will be correct even if it has not been scanned.
276+
pub fn sent_and_received(&self, tx: &Transaction, range: impl RangeBounds<K>) -> (u64, u64) {
277+
self.inner
278+
.sent_and_received(tx, Self::map_to_inner_bounds(range))
280279
}
281280

282281
/// Computes the net value that this transaction gives to the script pubkeys in the index and
@@ -286,8 +285,8 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
286285
/// This calls [`SpkTxOutIndex::net_value`] internally.
287286
///
288287
/// [`sent_and_received`]: Self::sent_and_received
289-
pub fn net_value(&self, tx: &Transaction) -> i64 {
290-
self.inner.net_value(tx)
288+
pub fn net_value(&self, tx: &Transaction, range: impl RangeBounds<K>) -> i64 {
289+
self.inner.net_value(tx, Self::map_to_inner_bounds(range))
291290
}
292291
}
293292

@@ -390,24 +389,32 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
390389
.collect()
391390
}
392391

393-
/// Iterate over revealed spks of all keychains.
394-
pub fn revealed_spks(&self) -> impl DoubleEndedIterator<Item = (K, u32, &Script)> + Clone {
395-
self.keychains.keys().flat_map(|keychain| {
396-
self.revealed_keychain_spks(keychain)
397-
.map(|(i, spk)| (keychain.clone(), i, spk))
392+
/// Iterate over revealed spks of keychains in `range`
393+
pub fn revealed_spks(
394+
&self,
395+
range: impl RangeBounds<K>,
396+
) -> impl DoubleEndedIterator<Item = (&K, u32, &Script)> + Clone {
397+
self.keychains.range(range).flat_map(|(keychain, _)| {
398+
let start = Bound::Included((keychain.clone(), u32::MIN));
399+
let end = match self.last_revealed.get(keychain) {
400+
Some(last_revealed) => Bound::Included((keychain.clone(), *last_revealed)),
401+
None => Bound::Excluded((keychain.clone(), u32::MIN)),
402+
};
403+
404+
self.inner
405+
.all_spks()
406+
.range((start, end))
407+
.map(|((keychain, i), spk)| (keychain, *i, spk.as_script()))
398408
})
399409
}
400410

401411
/// Iterate over revealed spks of the given `keychain`.
402-
pub fn revealed_keychain_spks(
403-
&self,
404-
keychain: &K,
405-
) -> impl DoubleEndedIterator<Item = (u32, &Script)> + Clone {
406-
let next_i = self.last_revealed.get(keychain).map_or(0, |&i| i + 1);
407-
self.inner
408-
.all_spks()
409-
.range((keychain.clone(), u32::MIN)..(keychain.clone(), next_i))
410-
.map(|((_, i), spk)| (*i, spk.as_script()))
412+
pub fn revealed_keychain_spks<'a>(
413+
&'a self,
414+
keychain: &'a K,
415+
) -> impl DoubleEndedIterator<Item = (u32, &Script)> + 'a {
416+
self.revealed_spks(keychain..=keychain)
417+
.map(|(_, i, spk)| (i, spk))
411418
}
412419

413420
/// Iterate over revealed, but unused, spks of all keychains.
@@ -617,38 +624,40 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
617624
}
618625
}
619626

620-
/// Iterate over all [`OutPoint`]s that point to `TxOut`s with script pubkeys derived from
627+
/// Iterate over all [`OutPoint`]s that have `TxOut`s with script pubkeys derived from
621628
/// `keychain`.
622-
///
623-
/// Use [`keychain_outpoints_in_range`](KeychainTxOutIndex::keychain_outpoints_in_range) to
624-
/// iterate over a specific derivation range.
625-
pub fn keychain_outpoints(
626-
&self,
627-
keychain: &K,
628-
) -> impl DoubleEndedIterator<Item = (u32, OutPoint)> + '_ {
629-
self.keychain_outpoints_in_range(keychain, ..)
629+
pub fn keychain_outpoints<'a>(
630+
&'a self,
631+
keychain: &'a K,
632+
) -> impl DoubleEndedIterator<Item = (u32, OutPoint)> + 'a {
633+
self.keychain_outpoints_in_range(keychain..=keychain)
634+
.map(move |(_, i, op)| (i, op))
635+
}
636+
637+
/// Iterate over [`OutPoint`]s that have script pubkeys derived from keychains in `range`.
638+
pub fn keychain_outpoints_in_range<'a>(
639+
&'a self,
640+
range: impl RangeBounds<K> + 'a,
641+
) -> impl DoubleEndedIterator<Item = (&'a K, u32, OutPoint)> + 'a {
642+
let bounds = Self::map_to_inner_bounds(range);
643+
self.inner
644+
.outputs_in_range(bounds)
645+
.map(move |((keychain, i), op)| (keychain, *i, op))
630646
}
631647

632-
/// Iterate over [`OutPoint`]s that point to `TxOut`s with script pubkeys derived from
633-
/// `keychain` in a given derivation `range`.
634-
pub fn keychain_outpoints_in_range(
635-
&self,
636-
keychain: &K,
637-
range: impl RangeBounds<u32>,
638-
) -> impl DoubleEndedIterator<Item = (u32, OutPoint)> + '_ {
639-
let start = match range.start_bound() {
640-
Bound::Included(i) => Bound::Included((keychain.clone(), *i)),
641-
Bound::Excluded(i) => Bound::Excluded((keychain.clone(), *i)),
648+
fn map_to_inner_bounds(bound: impl RangeBounds<K>) -> impl RangeBounds<(K, u32)> {
649+
let start = match bound.start_bound() {
650+
Bound::Included(keychain) => Bound::Included((keychain.clone(), u32::MIN)),
651+
Bound::Excluded(keychain) => Bound::Excluded((keychain.clone(), u32::MAX)),
642652
Bound::Unbounded => Bound::Unbounded,
643653
};
644-
let end = match range.end_bound() {
645-
Bound::Included(i) => Bound::Included((keychain.clone(), *i)),
646-
Bound::Excluded(i) => Bound::Excluded((keychain.clone(), *i)),
654+
let end = match bound.end_bound() {
655+
Bound::Included(keychain) => Bound::Included((keychain.clone(), u32::MAX)),
656+
Bound::Excluded(keychain) => Bound::Excluded((keychain.clone(), u32::MIN)),
647657
Bound::Unbounded => Bound::Unbounded,
648658
};
649-
self.inner
650-
.outputs_in_range((start, end))
651-
.map(|((_, i), op)| (*i, op))
659+
660+
(start, end)
652661
}
653662

654663
/// Returns the highest derivation index of the `keychain` where [`KeychainTxOutIndex`] has

crates/chain/src/spk_txout_index.rs

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -270,36 +270,39 @@ impl<I: Clone + Ord> SpkTxOutIndex<I> {
270270
self.spk_indices.get(script)
271271
}
272272

273-
/// Computes total input value going from script pubkeys in the index (sent) and the total output
274-
/// value going to script pubkeys in the index (received) in `tx`. For the `sent` to be computed
275-
/// correctly, the output being spent must have already been scanned by the index. Calculating
276-
/// received just uses the [`Transaction`] outputs directly, so it will be correct even if it has
277-
/// not been scanned.
278-
pub fn sent_and_received(&self, tx: &Transaction) -> (u64, u64) {
273+
/// Computes the total value transfer effect `tx` has on the script pubkeys in `range`. Value is
274+
/// *sent* when a script pubkey in the `range` is on an input and *received* when it is on an
275+
/// output. For `sent` to be computed correctly, the output being spent must have already been
276+
/// scanned by the index. Calculating received just uses the [`Transaction`] outputs directly,
277+
/// so it will be correct even if it has not been scanned.
278+
pub fn sent_and_received(&self, tx: &Transaction, range: impl RangeBounds<I>) -> (u64, u64) {
279279
let mut sent = 0;
280280
let mut received = 0;
281281

282282
for txin in &tx.input {
283-
if let Some((_, txout)) = self.txout(txin.previous_output) {
284-
sent += txout.value.to_sat();
283+
if let Some((index, txout)) = self.txout(txin.previous_output) {
284+
if range.contains(index) {
285+
sent += txout.value.to_sat();
286+
}
285287
}
286288
}
287289
for txout in &tx.output {
288-
if self.index_of_spk(&txout.script_pubkey).is_some() {
289-
received += txout.value.to_sat();
290+
if let Some(index) = self.index_of_spk(&txout.script_pubkey) {
291+
if range.contains(index) {
292+
received += txout.value.to_sat();
293+
}
290294
}
291295
}
292296

293297
(sent, received)
294298
}
295299

296-
/// Computes the net value that this transaction gives to the script pubkeys in the index and
297-
/// *takes* from the transaction outputs in the index. Shorthand for calling
298-
/// [`sent_and_received`] and subtracting sent from received.
300+
/// Computes the net value transfer effect of `tx` on the script pubkeys in `range`. Shorthand
301+
/// for calling [`sent_and_received`] and subtracting sent from received.
299302
///
300303
/// [`sent_and_received`]: Self::sent_and_received
301-
pub fn net_value(&self, tx: &Transaction) -> i64 {
302-
let (sent, received) = self.sent_and_received(tx);
304+
pub fn net_value(&self, tx: &Transaction, range: impl RangeBounds<I>) -> i64 {
305+
let (sent, received) = self.sent_and_received(tx, range);
303306
received as i64 - sent as i64
304307
}
305308

crates/chain/tests/test_spk_txout_index.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ fn spk_txout_sent_and_received() {
2020
}],
2121
};
2222

23-
assert_eq!(index.sent_and_received(&tx1), (0, 42_000));
24-
assert_eq!(index.net_value(&tx1), 42_000);
23+
assert_eq!(index.sent_and_received(&tx1, ..), (0, 42_000));
24+
assert_eq!(index.sent_and_received(&tx1, ..1), (0, 42_000));
25+
assert_eq!(index.sent_and_received(&tx1, 1..), (0, 0));
26+
assert_eq!(index.net_value(&tx1, ..), 42_000);
2527
index.index_tx(&tx1);
2628
assert_eq!(
27-
index.sent_and_received(&tx1),
29+
index.sent_and_received(&tx1, ..),
2830
(0, 42_000),
2931
"shouldn't change after scanning"
3032
);
@@ -51,8 +53,10 @@ fn spk_txout_sent_and_received() {
5153
],
5254
};
5355

54-
assert_eq!(index.sent_and_received(&tx2), (42_000, 50_000));
55-
assert_eq!(index.net_value(&tx2), 8_000);
56+
assert_eq!(index.sent_and_received(&tx2, ..), (42_000, 50_000));
57+
assert_eq!(index.sent_and_received(&tx2, ..1), (42_000, 30_000));
58+
assert_eq!(index.sent_and_received(&tx2, 1..), (0, 20_000));
59+
assert_eq!(index.net_value(&tx2, ..), 8_000);
5660
}
5761

5862
#[test]

example-crates/example_electrum/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,8 @@ fn main() -> anyhow::Result<()> {
210210
if all_spks {
211211
let all_spks = graph
212212
.index
213-
.revealed_spks()
214-
.map(|(k, i, spk)| (k, i, spk.to_owned()))
213+
.revealed_spks(..)
214+
.map(|(k, i, spk)| (k.to_owned(), i, spk.to_owned()))
215215
.collect::<Vec<_>>();
216216
spks = Box::new(spks.chain(all_spks.into_iter().map(|(k, i, spk)| {
217217
eprintln!("scanning {}:{}", k, i);

example-crates/example_esplora/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,8 @@ fn main() -> anyhow::Result<()> {
241241
if *all_spks {
242242
let all_spks = graph
243243
.index
244-
.revealed_spks()
245-
.map(|(k, i, spk)| (k, i, spk.to_owned()))
244+
.revealed_spks(..)
245+
.map(|(k, i, spk)| (k.to_owned(), i, spk.to_owned()))
246246
.collect::<Vec<_>>();
247247
spks = Box::new(spks.chain(all_spks.into_iter().map(|(k, i, spk)| {
248248
eprintln!("scanning {}:{}", k, i);

0 commit comments

Comments
 (0)