diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 7ab0ffa8a..f69b227a2 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -1,12 +1,9 @@ -use core::convert::Infallible; - use alloc::vec::Vec; -use bitcoin::{OutPoint, Script, Transaction, TxOut}; +use bitcoin::{OutPoint, Transaction, TxOut}; use crate::{ - keychain::Balance, tx_graph::{Additions, TxGraph}, - Anchor, Append, BlockId, ChainOracle, FullTxOut, ObservedAs, + Anchor, Append, }; /// A struct that combines [`TxGraph`] and an [`Indexer`] implementation. @@ -29,6 +26,14 @@ impl Default for IndexedTxGraph { } impl IndexedTxGraph { + /// Construct a new [`IndexedTxGraph`] with a given `index`. + pub fn new(index: I) -> Self { + Self { + index, + graph: TxGraph::default(), + } + } + /// Get a reference of the internal transaction graph. pub fn graph(&self) -> &TxGraph { &self.graph @@ -157,115 +162,6 @@ where } } -impl IndexedTxGraph { - pub fn try_list_owned_txouts<'a, C: ChainOracle + 'a>( - &'a self, - chain: &'a C, - chain_tip: BlockId, - ) -> impl Iterator>, C::Error>> + 'a { - self.graph() - .try_list_chain_txouts(chain, chain_tip) - .filter(|r| { - if let Ok(full_txout) = r { - if !self.index.is_spk_owned(&full_txout.txout.script_pubkey) { - return false; - } - } - true - }) - } - - pub fn list_owned_txouts<'a, C: ChainOracle + 'a>( - &'a self, - chain: &'a C, - chain_tip: BlockId, - ) -> impl Iterator>> + 'a { - self.try_list_owned_txouts(chain, chain_tip) - .map(|r| r.expect("oracle is infallible")) - } - - pub fn try_list_owned_unspents<'a, C: ChainOracle + 'a>( - &'a self, - chain: &'a C, - chain_tip: BlockId, - ) -> impl Iterator>, C::Error>> + 'a { - self.graph() - .try_list_chain_unspents(chain, chain_tip) - .filter(|r| { - if let Ok(full_txout) = r { - if !self.index.is_spk_owned(&full_txout.txout.script_pubkey) { - return false; - } - } - true - }) - } - - pub fn list_owned_unspents<'a, C: ChainOracle + 'a>( - &'a self, - chain: &'a C, - chain_tip: BlockId, - ) -> impl Iterator>> + 'a { - self.try_list_owned_unspents(chain, chain_tip) - .map(|r| r.expect("oracle is infallible")) - } - - pub fn try_balance( - &self, - chain: &C, - chain_tip: BlockId, - mut should_trust: F, - ) -> Result - where - C: ChainOracle, - F: FnMut(&Script) -> bool, - { - let tip_height = chain_tip.height; - - let mut immature = 0; - let mut trusted_pending = 0; - let mut untrusted_pending = 0; - let mut confirmed = 0; - - for res in self.try_list_owned_unspents(chain, chain_tip) { - let txout = res?; - - match &txout.chain_position { - ObservedAs::Confirmed(_) => { - if txout.is_confirmed_and_spendable(tip_height) { - confirmed += txout.txout.value; - } else if !txout.is_mature(tip_height) { - immature += txout.txout.value; - } - } - ObservedAs::Unconfirmed(_) => { - if should_trust(&txout.txout.script_pubkey) { - trusted_pending += txout.txout.value; - } else { - untrusted_pending += txout.txout.value; - } - } - } - } - - Ok(Balance { - immature, - trusted_pending, - untrusted_pending, - confirmed, - }) - } - - pub fn balance(&self, chain: &C, chain_tip: BlockId, should_trust: F) -> Balance - where - C: ChainOracle, - F: FnMut(&Script) -> bool, - { - self.try_balance(chain, chain_tip, should_trust) - .expect("error is infallible") - } -} - /// A structure that represents changes to an [`IndexedTxGraph`]. #[derive(Clone, Debug, PartialEq)] #[cfg_attr( @@ -324,9 +220,3 @@ pub trait Indexer { /// Determines whether the transaction should be included in the index. fn is_tx_relevant(&self, tx: &Transaction) -> bool; } - -/// A trait that extends [`Indexer`] to also index "owned" script pubkeys. -pub trait OwnedIndexer: Indexer { - /// Determines whether a given script pubkey (`spk`) is owned. - fn is_spk_owned(&self, spk: &Script) -> bool; -} diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs index c7a8dd54b..397c43386 100644 --- a/crates/chain/src/keychain/txout_index.rs +++ b/crates/chain/src/keychain/txout_index.rs @@ -1,6 +1,6 @@ use crate::{ collections::*, - indexed_tx_graph::{Indexer, OwnedIndexer}, + indexed_tx_graph::Indexer, miniscript::{Descriptor, DescriptorPublicKey}, spk_iter::BIP32_MAX_INDEX, ForEachTxOut, SpkIterator, SpkTxOutIndex, @@ -109,12 +109,6 @@ impl Indexer for KeychainTxOutIndex { } } -impl OwnedIndexer for KeychainTxOutIndex { - fn is_spk_owned(&self, spk: &Script) -> bool { - self.index_of_spk(spk).is_some() - } -} - impl KeychainTxOutIndex { /// Scans an object for relevant outpoints, which are stored and indexed internally. /// @@ -153,6 +147,11 @@ impl KeychainTxOutIndex { &self.inner } + /// Get a reference to the set of indexed outpoints. + pub fn outpoints(&self) -> &BTreeSet<((K, u32), OutPoint)> { + self.inner.outpoints() + } + /// Return a reference to the internal map of the keychain to descriptors. pub fn keychains(&self) -> &BTreeMap> { &self.keychains diff --git a/crates/chain/src/persist.rs b/crates/chain/src/persist.rs index 188f88f26..07ff67957 100644 --- a/crates/chain/src/persist.rs +++ b/crates/chain/src/persist.rs @@ -41,11 +41,19 @@ where /// Commit the staged changes to the underlying persistance backend. /// + /// Changes that are committed (if any) are returned. + /// + /// # Error + /// /// Returns a backend-defined error if this fails. - pub fn commit(&mut self) -> Result<(), B::WriteError> { - let mut temp = C::default(); - core::mem::swap(&mut temp, &mut self.stage); - self.backend.write_changes(&temp) + pub fn commit(&mut self) -> Result, B::WriteError> { + if self.stage.is_empty() { + return Ok(None); + } + self.backend + .write_changes(&self.stage) + // if written successfully, take and return `self.stage` + .map(|_| Some(core::mem::take(&mut self.stage))) } } diff --git a/crates/chain/src/spk_txout_index.rs b/crates/chain/src/spk_txout_index.rs index ae9441492..0eaec4bb7 100644 --- a/crates/chain/src/spk_txout_index.rs +++ b/crates/chain/src/spk_txout_index.rs @@ -2,7 +2,7 @@ use core::ops::RangeBounds; use crate::{ collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap}, - indexed_tx_graph::{Indexer, OwnedIndexer}, + indexed_tx_graph::Indexer, ForEachTxOut, }; use bitcoin::{self, OutPoint, Script, Transaction, TxOut, Txid}; @@ -75,12 +75,6 @@ impl Indexer for SpkTxOutIndex { } } -impl OwnedIndexer for SpkTxOutIndex { - fn is_spk_owned(&self, spk: &Script) -> bool { - self.spk_indices.get(spk).is_some() - } -} - /// This macro is used instead of a member function of `SpkTxOutIndex`, which would result in a /// compiler error[E0521]: "borrowed data escapes out of closure" when we attempt to take a /// reference out of the `ForEachTxOut` closure during scanning. @@ -126,6 +120,11 @@ impl SpkTxOutIndex { scan_txout!(self, op, txout) } + /// Get a reference to the set of indexed outpoints. + pub fn outpoints(&self) -> &BTreeSet<(I, OutPoint)> { + &self.spk_txouts + } + /// Iterate over all known txouts that spend to tracked script pubkeys. pub fn txouts( &self, diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index ef3f3847c..335a19197 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -56,10 +56,11 @@ //! ``` use crate::{ - collections::*, Anchor, Append, BlockId, ChainOracle, ForEachTxOut, FullTxOut, ObservedAs, + collections::*, keychain::Balance, Anchor, Append, BlockId, ChainOracle, ForEachTxOut, + FullTxOut, ObservedAs, }; use alloc::vec::Vec; -use bitcoin::{OutPoint, Transaction, TxOut, Txid}; +use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid}; use core::{ convert::Infallible, ops::{Deref, RangeInclusive}, @@ -762,107 +763,201 @@ impl TxGraph { .map(|r| r.expect("oracle is infallible")) } - /// List outputs that are in `chain` with `chain_tip`. + /// Get a filtered list of outputs from the given `outpoints` that are in `chain` with + /// `chain_tip`. /// - /// Floating ouputs are not iterated over. + /// `outpoints` is a list of outpoints we are interested in, coupled with an outpoint identifier + /// (`OI`) for convenience. If `OI` is not necessary, the caller can use `()`, or + /// [`Iterator::enumerate`] over a list of [`OutPoint`]s. /// - /// The `filter_predicate` should return true for outputs that we wish to iterate over. + /// Floating outputs are ignored. /// /// # Error /// - /// A returned item can error if the [`ChainOracle`] implementation (`chain`) fails. + /// An [`Iterator::Item`] can be an [`Err`] if the [`ChainOracle`] implementation (`chain`) + /// fails. /// - /// If the [`ChainOracle`] is infallible, [`list_chain_txouts`] can be used instead. + /// If the [`ChainOracle`] implementation is infallible, [`filter_chain_txouts`] can be used + /// instead. /// - /// [`list_chain_txouts`]: Self::list_chain_txouts - pub fn try_list_chain_txouts<'a, C: ChainOracle + 'a>( + /// [`filter_chain_txouts`]: Self::filter_chain_txouts + pub fn try_filter_chain_txouts<'a, C: ChainOracle + 'a, OI: Clone + 'a>( &'a self, chain: &'a C, chain_tip: BlockId, - ) -> impl Iterator>, C::Error>> + 'a { - self.try_list_chain_txs(chain, chain_tip) - .flat_map(move |tx_res| match tx_res { - Ok(canonical_tx) => canonical_tx - .node - .output - .iter() - .enumerate() - .map(|(vout, txout)| { - let outpoint = OutPoint::new(canonical_tx.node.txid, vout as _); - Ok((outpoint, txout.clone(), canonical_tx.clone())) - }) - .collect::>(), - Err(err) => vec![Err(err)], - }) - .map(move |res| -> Result<_, C::Error> { - let ( - outpoint, - txout, - CanonicalTx { - observed_as, - node: tx_node, - }, - ) = res?; - let chain_position = observed_as.cloned(); - let spent_by = self - .try_get_chain_spend(chain, chain_tip, outpoint)? - .map(|(obs_as, txid)| (obs_as.cloned(), txid)); - let is_on_coinbase = tx_node.tx.is_coin_base(); - Ok(FullTxOut { - outpoint, - txout, - chain_position, - spent_by, - is_on_coinbase, - }) - }) + outpoints: impl IntoIterator + 'a, + ) -> impl Iterator>), C::Error>> + 'a { + outpoints + .into_iter() + .map( + move |(spk_i, op)| -> Result)>, C::Error> { + let tx_node = match self.get_tx_node(op.txid) { + Some(n) => n, + None => return Ok(None), + }; + + let txout = match tx_node.tx.output.get(op.vout as usize) { + Some(txout) => txout.clone(), + None => return Ok(None), + }; + + let chain_position = + match self.try_get_chain_position(chain, chain_tip, op.txid)? { + Some(pos) => pos.cloned(), + None => return Ok(None), + }; + + let spent_by = self + .try_get_chain_spend(chain, chain_tip, op)? + .map(|(a, txid)| (a.cloned(), txid)); + + Ok(Some(( + spk_i, + FullTxOut { + outpoint: op, + txout, + chain_position, + spent_by, + is_on_coinbase: tx_node.tx.is_coin_base(), + }, + ))) + }, + ) + .filter_map(Result::transpose) } - /// List outputs that are in `chain` with `chain_tip`. + /// Get a filtered list of outputs from the given `outpoints` that are in `chain` with + /// `chain_tip`. /// - /// This is the infallible version of [`try_list_chain_txouts`]. + /// This is the infallible version of [`try_filter_chain_txouts`]. /// - /// [`try_list_chain_txouts`]: Self::try_list_chain_txouts - pub fn list_chain_txouts<'a, C: ChainOracle + 'a>( + /// [`try_filter_chain_txouts`]: Self::try_filter_chain_txouts + pub fn filter_chain_txouts<'a, C: ChainOracle + 'a, OI: Clone + 'a>( &'a self, chain: &'a C, chain_tip: BlockId, - ) -> impl Iterator>> + 'a { - self.try_list_chain_txouts(chain, chain_tip) - .map(|r| r.expect("error in infallible")) + outpoints: impl IntoIterator + 'a, + ) -> impl Iterator>)> + 'a { + self.try_filter_chain_txouts(chain, chain_tip, outpoints) + .map(|r| r.expect("oracle is infallible")) } - /// List unspent outputs (UTXOs) that are in `chain` with `chain_tip`. + /// Get a filtered list of unspent outputs (UTXOs) from the given `outpoints` that are in + /// `chain` with `chain_tip`. + /// + /// `outpoints` is a list of outpoints we are interested in, coupled with an outpoint identifier + /// (`OI`) for convenience. If `OI` is not necessary, the caller can use `()`, or + /// [`Iterator::enumerate`] over a list of [`OutPoint`]s. /// - /// Floating outputs are not iterated over. + /// Floating outputs are ignored. /// /// # Error /// - /// An item can be an error if the [`ChainOracle`] implementation fails. If the oracle is - /// infallible, [`list_chain_unspents`] can be used instead. + /// An [`Iterator::Item`] can be an [`Err`] if the [`ChainOracle`] implementation (`chain`) + /// fails. /// - /// [`list_chain_unspents`]: Self::list_chain_unspents - pub fn try_list_chain_unspents<'a, C: ChainOracle + 'a>( + /// If the [`ChainOracle`] implementation is infallible, [`filter_chain_unspents`] can be used + /// instead. + /// + /// [`filter_chain_unspents`]: Self::filter_chain_unspents + pub fn try_filter_chain_unspents<'a, C: ChainOracle + 'a, OI: Clone + 'a>( &'a self, chain: &'a C, chain_tip: BlockId, - ) -> impl Iterator>, C::Error>> + 'a { - self.try_list_chain_txouts(chain, chain_tip) - .filter(|r| matches!(r, Ok(txo) if txo.spent_by.is_none())) + outpoints: impl IntoIterator + 'a, + ) -> impl Iterator>), C::Error>> + 'a { + self.try_filter_chain_txouts(chain, chain_tip, outpoints) + .filter(|r| match r { + // keep unspents, drop spents + Ok((_, full_txo)) => full_txo.spent_by.is_none(), + // keep errors + Err(_) => true, + }) } - /// List unspent outputs (UTXOs) that are in `chain` with `chain_tip`. + /// Get a filtered list of unspent outputs (UTXOs) from the given `outpoints` that are in + /// `chain` with `chain_tip`. /// - /// This is the infallible version of [`try_list_chain_unspents`]. + /// This is the infallible version of [`try_filter_chain_unspents`]. /// - /// [`try_list_chain_unspents`]: Self::try_list_chain_unspents - pub fn list_chain_unspents<'a, C: ChainOracle + 'a>( + /// [`try_filter_chain_unspents`]: Self::try_filter_chain_unspents + pub fn filter_chain_unspents<'a, C: ChainOracle + 'a, OI: Clone + 'a>( &'a self, chain: &'a C, - static_block: BlockId, - ) -> impl Iterator>> + 'a { - self.try_list_chain_unspents(chain, static_block) - .map(|r| r.expect("error is infallible")) + chain_tip: BlockId, + txouts: impl IntoIterator + 'a, + ) -> impl Iterator>)> + 'a { + self.try_filter_chain_unspents(chain, chain_tip, txouts) + .map(|r| r.expect("oracle is infallible")) + } + + /// Get the total balance of `outpoints` that are in `chain` of `chain_tip`. + /// + /// The output of `trust_predicate` should return `true` for scripts that we trust. + /// + /// `outpoints` is a list of outpoints we are interested in, coupled with an outpoint identifier + /// (`OI`) for convenience. If `OI` is not necessary, the caller can use `()`, or + /// [`Iterator::enumerate`] over a list of [`OutPoint`]s. + /// + /// If the provided [`ChainOracle`] implementation (`chain`) is infallible, [`balance`] can be + /// used instead. + /// + /// [`balance`]: Self::balance + pub fn try_balance( + &self, + chain: &C, + chain_tip: BlockId, + outpoints: impl IntoIterator, + mut trust_predicate: impl FnMut(&OI, &Script) -> bool, + ) -> Result { + let mut immature = 0; + let mut trusted_pending = 0; + let mut untrusted_pending = 0; + let mut confirmed = 0; + + for res in self.try_filter_chain_unspents(chain, chain_tip, outpoints) { + let (spk_i, txout) = res?; + + match &txout.chain_position { + ObservedAs::Confirmed(_) => { + if txout.is_confirmed_and_spendable(chain_tip.height) { + confirmed += txout.txout.value; + } else if !txout.is_mature(chain_tip.height) { + immature += txout.txout.value; + } + } + ObservedAs::Unconfirmed(_) => { + if trust_predicate(&spk_i, &txout.txout.script_pubkey) { + trusted_pending += txout.txout.value; + } else { + untrusted_pending += txout.txout.value; + } + } + } + } + + Ok(Balance { + immature, + trusted_pending, + untrusted_pending, + confirmed, + }) + } + + /// Get the total balance of `outpoints` that are in `chain` of `chain_tip`. + /// + /// This is the infallible version of [`try_balance`]. + /// + /// [`try_balance`]: Self::try_balance + pub fn balance, OI: Clone>( + &self, + chain: &C, + chain_tip: BlockId, + outpoints: impl IntoIterator, + trust_predicate: impl FnMut(&OI, &Script) -> bool, + ) -> Balance { + self.try_balance(chain, chain_tip, outpoints, trust_predicate) + .expect("oracle is infallible") } } diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index f32ffe4f0..f231f7683 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -236,23 +236,36 @@ fn test_list_owned_txouts() { .map(|&hash| BlockId { height, hash }) .expect("block must exist"); let txouts = graph - .list_owned_txouts(&local_chain, chain_tip) + .graph() + .filter_chain_txouts( + &local_chain, + chain_tip, + graph.index.outpoints().iter().cloned(), + ) .collect::>(); let utxos = graph - .list_owned_unspents(&local_chain, chain_tip) + .graph() + .filter_chain_unspents( + &local_chain, + chain_tip, + graph.index.outpoints().iter().cloned(), + ) .collect::>(); - let balance = graph.balance(&local_chain, chain_tip, |spk: &Script| { - trusted_spks.contains(spk) - }); + let balance = graph.graph().balance( + &local_chain, + chain_tip, + graph.index.outpoints().iter().cloned(), + |_, spk: &Script| trusted_spks.contains(spk), + ); assert_eq!(txouts.len(), 5); assert_eq!(utxos.len(), 4); let confirmed_txouts_txid = txouts .iter() - .filter_map(|full_txout| { + .filter_map(|(_, full_txout)| { if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) { Some(full_txout.outpoint.txid) } else { @@ -263,7 +276,7 @@ fn test_list_owned_txouts() { let unconfirmed_txouts_txid = txouts .iter() - .filter_map(|full_txout| { + .filter_map(|(_, full_txout)| { if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) { Some(full_txout.outpoint.txid) } else { @@ -274,7 +287,7 @@ fn test_list_owned_txouts() { let confirmed_utxos_txid = utxos .iter() - .filter_map(|full_txout| { + .filter_map(|(_, full_txout)| { if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) { Some(full_txout.outpoint.txid) } else { @@ -285,7 +298,7 @@ fn test_list_owned_txouts() { let unconfirmed_utxos_txid = utxos .iter() - .filter_map(|full_txout| { + .filter_map(|(_, full_txout)| { if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) { Some(full_txout.outpoint.txid) } else {