Skip to content

Commit

Permalink
[chain] Make KeychainTxOutIndex more range based
Browse files Browse the repository at this point in the history
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 "..".
  • Loading branch information
LLFourn committed Feb 6, 2024
1 parent 7aca884 commit 0317415
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 75 deletions.
114 changes: 63 additions & 51 deletions crates/chain/src/keychain/txout_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,15 +268,14 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
self.inner.unmark_used(&(keychain, index))
}

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

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

Expand Down Expand Up @@ -385,24 +384,35 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
.collect()
}

/// Iterate over revealed spks of all keychains.
pub fn revealed_spks(&self) -> impl DoubleEndedIterator<Item = (K, u32, &Script)> + Clone {
self.keychains.keys().flat_map(|keychain| {
self.revealed_keychain_spks(keychain)
.map(|(i, spk)| (keychain.clone(), i, spk))
})
/// Iterate over revealed spks of keychains in `range`
pub fn revealed_spks(
&self,
range: impl RangeBounds<K>,
) -> impl DoubleEndedIterator<Item = (&K, u32, &Script)> + Clone {
self.keychains
.range(range)
.map(|(keychain, _)| {
let start = Bound::Included((keychain.clone(), u32::MIN));
let end = match self.last_revealed.get(keychain) {
Some(last_revealed) => Bound::Included((keychain.clone(), *last_revealed)),
None => Bound::Excluded((keychain.clone(), u32::MIN)),
};

self.inner
.all_spks()
.range((start, end))
.map(|((keychain, i), spk)| (keychain, *i, spk.as_script()))
})
.flatten()

Check failure on line 406 in crates/chain/src/keychain/txout_index.rs

View workflow job for this annotation

GitHub Actions / clippy

called `map(..).flatten()` on `Iterator`

error: called `map(..).flatten()` on `Iterator` --> crates/chain/src/keychain/txout_index.rs:394:14 | 394 | .map(|(keychain, _)| { | ______________^ 395 | | let start = Bound::Included((keychain.clone(), u32::MIN)); 396 | | let end = match self.last_revealed.get(keychain) { 397 | | Some(last_revealed) => Bound::Included((keychain.clone(), *last_revealed)), ... | 405 | | }) 406 | | .flatten() | |______________________^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten = note: `-D clippy::map-flatten` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(clippy::map_flatten)]` help: try replacing `map` with `flat_map` and remove the `.flatten()` | 394 ~ .flat_map(|(keychain, _)| { 395 + let start = Bound::Included((keychain.clone(), u32::MIN)); 396 + let end = match self.last_revealed.get(keychain) { 397 + Some(last_revealed) => Bound::Included((keychain.clone(), *last_revealed)), 398 + None => Bound::Excluded((keychain.clone(), u32::MIN)), 399 + }; 400 + 401 + self.inner 402 + .all_spks() 403 + .range((start, end)) 404 + .map(|((keychain, i), spk)| (keychain, *i, spk.as_script())) 405 + }) |

Check failure on line 406 in crates/chain/src/keychain/txout_index.rs

View workflow job for this annotation

GitHub Actions / clippy

called `map(..).flatten()` on `Iterator`

error: called `map(..).flatten()` on `Iterator` --> crates/chain/src/keychain/txout_index.rs:394:14 | 394 | .map(|(keychain, _)| { | ______________^ 395 | | let start = Bound::Included((keychain.clone(), u32::MIN)); 396 | | let end = match self.last_revealed.get(keychain) { 397 | | Some(last_revealed) => Bound::Included((keychain.clone(), *last_revealed)), ... | 405 | | }) 406 | | .flatten() | |______________________^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten = note: `-D clippy::map-flatten` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(clippy::map_flatten)]` help: try replacing `map` with `flat_map` and remove the `.flatten()` | 394 ~ .flat_map(|(keychain, _)| { 395 + let start = Bound::Included((keychain.clone(), u32::MIN)); 396 + let end = match self.last_revealed.get(keychain) { 397 + Some(last_revealed) => Bound::Included((keychain.clone(), *last_revealed)), 398 + None => Bound::Excluded((keychain.clone(), u32::MIN)), 399 + }; 400 + 401 + self.inner 402 + .all_spks() 403 + .range((start, end)) 404 + .map(|((keychain, i), spk)| (keychain, *i, spk.as_script())) 405 + }) |
}

/// Iterate over revealed spks of the given `keychain`.
pub fn revealed_keychain_spks(
&self,
keychain: &K,
) -> impl DoubleEndedIterator<Item = (u32, &Script)> + Clone {
let next_i = self.last_revealed.get(keychain).map_or(0, |&i| i + 1);
self.inner
.all_spks()
.range((keychain.clone(), u32::MIN)..(keychain.clone(), next_i))
.map(|((_, i), spk)| (*i, spk.as_script()))
pub fn revealed_keychain_spks<'a>(
&'a self,
keychain: &'a K,
) -> impl DoubleEndedIterator<Item = (u32, &Script)> + 'a {
self.revealed_spks(keychain..=keychain)
.map(|(_, i, spk)| (i, spk))
}

/// Iterate over revealed, but unused, spks of all keychains.
Expand Down Expand Up @@ -612,38 +622,40 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
}
}

/// Iterate over all [`OutPoint`]s that point to `TxOut`s with script pubkeys derived from
/// Iterate over all [`OutPoint`]s that have `TxOut`s with script pubkeys derived from
/// `keychain`.
///
/// Use [`keychain_outpoints_in_range`](KeychainTxOutIndex::keychain_outpoints_in_range) to
/// iterate over a specific derivation range.
pub fn keychain_outpoints(
&self,
keychain: &K,
) -> impl DoubleEndedIterator<Item = (u32, OutPoint)> + '_ {
self.keychain_outpoints_in_range(keychain, ..)
pub fn keychain_outpoints<'a>(
&'a self,
keychain: &'a K,
) -> impl DoubleEndedIterator<Item = (u32, OutPoint)> + 'a {
self.keychain_outpoints_in_range(keychain..=keychain)
.map(move |(_, i, op)| (i, op))
}

/// Iterate over [`OutPoint`]s that have script pubkeys derived from keychains in `range`.
pub fn keychain_outpoints_in_range<'a>(
&'a self,
range: impl RangeBounds<K> + 'a,
) -> impl DoubleEndedIterator<Item = (&'a K, u32, OutPoint)> + 'a {
let bounds = Self::map_to_inner_bounds(range);
self.inner
.outputs_in_range(bounds)
.map(move |((keychain, i), op)| (keychain, *i, op))
}

/// Iterate over [`OutPoint`]s that point to `TxOut`s with script pubkeys derived from
/// `keychain` in a given derivation `range`.
pub fn keychain_outpoints_in_range(
&self,
keychain: &K,
range: impl RangeBounds<u32>,
) -> impl DoubleEndedIterator<Item = (u32, OutPoint)> + '_ {
let start = match range.start_bound() {
Bound::Included(i) => Bound::Included((keychain.clone(), *i)),
Bound::Excluded(i) => Bound::Excluded((keychain.clone(), *i)),
fn map_to_inner_bounds(bound: impl RangeBounds<K>) -> impl RangeBounds<(K, u32)> {
let start = match bound.start_bound() {
Bound::Included(keychain) => Bound::Included((keychain.clone(), u32::MIN)),
Bound::Excluded(keychain) => Bound::Excluded((keychain.clone(), u32::MAX)),
Bound::Unbounded => Bound::Unbounded,
};
let end = match range.end_bound() {
Bound::Included(i) => Bound::Included((keychain.clone(), *i)),
Bound::Excluded(i) => Bound::Excluded((keychain.clone(), *i)),
let end = match bound.end_bound() {
Bound::Included(keychain) => Bound::Included((keychain.clone(), u32::MAX)),
Bound::Excluded(keychain) => Bound::Excluded((keychain.clone(), u32::MIN)),
Bound::Unbounded => Bound::Unbounded,
};
self.inner
.outputs_in_range((start, end))
.map(|((_, i), op)| (*i, op))

(start, end)
}

/// Returns the highest derivation index of the `keychain` where [`KeychainTxOutIndex`] has
Expand Down
33 changes: 18 additions & 15 deletions crates/chain/src/spk_txout_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,36 +270,39 @@ impl<I: Clone + Ord> SpkTxOutIndex<I> {
self.spk_indices.get(script)
}

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

for txin in &tx.input {
if let Some((_, txout)) = self.txout(txin.previous_output) {
sent += txout.value;
if let Some((index, txout)) = self.txout(txin.previous_output) {
if range.contains(&index) {

Check failure on line 284 in crates/chain/src/spk_txout_index.rs

View workflow job for this annotation

GitHub Actions / clippy

this expression creates a reference which is immediately dereferenced by the compiler

error: this expression creates a reference which is immediately dereferenced by the compiler --> crates/chain/src/spk_txout_index.rs:284:35 | 284 | if range.contains(&index) { | ^^^^^^ help: change this to: `index` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow = note: `-D clippy::needless-borrow` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(clippy::needless_borrow)]`

Check failure on line 284 in crates/chain/src/spk_txout_index.rs

View workflow job for this annotation

GitHub Actions / clippy

this expression creates a reference which is immediately dereferenced by the compiler

error: this expression creates a reference which is immediately dereferenced by the compiler --> crates/chain/src/spk_txout_index.rs:284:35 | 284 | if range.contains(&index) { | ^^^^^^ help: change this to: `index` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow = note: `-D clippy::needless-borrow` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(clippy::needless_borrow)]`
sent += txout.value;
}
}
}
for txout in &tx.output {
if self.index_of_spk(&txout.script_pubkey).is_some() {
received += txout.value;
if let Some(index) = self.index_of_spk(&txout.script_pubkey) {
if range.contains(index) {
received += txout.value;
}
}
}

(sent, received)
}

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

Expand Down
14 changes: 9 additions & 5 deletions crates/chain/tests/test_spk_txout_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ fn spk_txout_sent_and_received() {
}],
};

assert_eq!(index.sent_and_received(&tx1), (0, 42_000));
assert_eq!(index.net_value(&tx1), 42_000);
assert_eq!(index.sent_and_received(&tx1, ..), (0, 42_000));
assert_eq!(index.sent_and_received(&tx1, ..1), (0, 42_000));
assert_eq!(index.sent_and_received(&tx1, 1..), (0, 0));
assert_eq!(index.net_value(&tx1, ..), 42_000);
index.index_tx(&tx1);
assert_eq!(
index.sent_and_received(&tx1),
index.sent_and_received(&tx1, ..),
(0, 42_000),
"shouldn't change after scanning"
);
Expand All @@ -51,8 +53,10 @@ fn spk_txout_sent_and_received() {
],
};

assert_eq!(index.sent_and_received(&tx2), (42_000, 50_000));
assert_eq!(index.net_value(&tx2), 8_000);
assert_eq!(index.sent_and_received(&tx2, ..), (42_000, 50_000));
assert_eq!(index.sent_and_received(&tx2, ..1), (42_000, 30_000));
assert_eq!(index.sent_and_received(&tx2, 1..), (0, 20_000));
assert_eq!(index.net_value(&tx2, ..), 8_000);
}

#[test]
Expand Down
4 changes: 2 additions & 2 deletions example-crates/example_electrum/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,8 @@ fn main() -> anyhow::Result<()> {
if all_spks {
let all_spks = graph
.index
.revealed_spks()
.map(|(k, i, spk)| (k, i, spk.to_owned()))
.revealed_spks(..)
.map(|(k, i, spk)| (k.to_owned(), i, spk.to_owned()))
.collect::<Vec<_>>();
spks = Box::new(spks.chain(all_spks.into_iter().map(|(k, i, spk)| {
eprintln!("scanning {}:{}", k, i);
Expand Down
4 changes: 2 additions & 2 deletions example-crates/example_esplora/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,8 @@ fn main() -> anyhow::Result<()> {
if *all_spks {
let all_spks = graph
.index
.revealed_spks()
.map(|(k, i, spk)| (k, i, spk.to_owned()))
.revealed_spks(..)
.map(|(k, i, spk)| (k.to_owned(), i, spk.to_owned()))
.collect::<Vec<_>>();
spks = Box::new(spks.chain(all_spks.into_iter().map(|(k, i, spk)| {
eprintln!("scanning {}:{}", k, i);
Expand Down

0 comments on commit 0317415

Please sign in to comment.