diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs index da6a1e25ba..d53e14e53b 100644 --- a/crates/chain/src/keychain/txout_index.rs +++ b/crates/chain/src/keychain/txout_index.rs @@ -5,12 +5,15 @@ use crate::{ spk_iter::BIP32_MAX_INDEX, SpkIterator, SpkTxOutIndex, }; -use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid}; +use alloc::vec::Vec; +use bitcoin::{OutPoint, Script, ScriptBuf, Transaction, TxOut, Txid}; use core::{ fmt::Debug, ops::{Bound, RangeBounds}, }; +use crate::local_chain::CheckPoint; +use crate::spk_client::{FullScanRequest, SyncRequest}; use crate::Append; const DEFAULT_LOOKAHEAD: u32 = 25; @@ -100,7 +103,7 @@ const DEFAULT_LOOKAHEAD: u32 = 25; /// [`unbounded_spk_iter`]: KeychainTxOutIndex::unbounded_spk_iter /// [`all_unbounded_spk_iters`]: KeychainTxOutIndex::all_unbounded_spk_iters #[derive(Clone, Debug)] -pub struct KeychainTxOutIndex<K> { +pub struct KeychainTxOutIndex<K: Clone + Ord + Send> { inner: SpkTxOutIndex<(K, u32)>, // descriptors of each keychain keychains: BTreeMap<K, Descriptor<DescriptorPublicKey>>, @@ -110,13 +113,13 @@ pub struct KeychainTxOutIndex<K> { lookahead: u32, } -impl<K> Default for KeychainTxOutIndex<K> { +impl<K: Clone + Ord + Debug + Send> Default for KeychainTxOutIndex<K> { fn default() -> Self { Self::new(DEFAULT_LOOKAHEAD) } } -impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> { +impl<K: Clone + Ord + Debug + Send> Indexer for KeychainTxOutIndex<K> { type ChangeSet = super::ChangeSet<K>; fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet { @@ -134,20 +137,20 @@ impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> { changeset } - fn initial_changeset(&self) -> Self::ChangeSet { - super::ChangeSet(self.last_revealed.clone()) - } - fn apply_changeset(&mut self, changeset: Self::ChangeSet) { self.apply_changeset(changeset) } + fn initial_changeset(&self) -> Self::ChangeSet { + super::ChangeSet(self.last_revealed.clone()) + } + fn is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool { self.inner.is_relevant(tx) } } -impl<K> KeychainTxOutIndex<K> { +impl<K: Clone + Ord + Debug + Send> KeychainTxOutIndex<K> { /// Construct a [`KeychainTxOutIndex`] with the given `lookahead`. /// /// The `lookahead` is the number of script pubkeys to derive and cache from the internal @@ -169,7 +172,7 @@ impl<K> KeychainTxOutIndex<K> { } /// Methods that are *re-exposed* from the internal [`SpkTxOutIndex`]. -impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> { +impl<K: Clone + Ord + Debug + Send> KeychainTxOutIndex<K> { /// Return a reference to the internal [`SpkTxOutIndex`]. /// /// **WARNING:** The internal index will contain lookahead spks. Refer to @@ -291,7 +294,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> { } } -impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> { +impl<K: Clone + Ord + Debug + Send> KeychainTxOutIndex<K> { /// Return a reference to the internal map of keychain to descriptors. pub fn keychains(&self) -> &BTreeMap<K, Descriptor<DescriptorPublicKey>> { &self.keychains @@ -664,6 +667,42 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> { .collect() } + /// Create a [`SyncRequest`] for this [`KeychainTxOutIndex`] for all revealed spks. + /// + /// This is the first step when performing a spk-based wallet sync, the returned [`SyncRequest`] collects + /// all revealed script pub keys needed to start a blockchain sync with a spk based blockchain client. A + /// [`CheckPoint`] representing the current chain tip must be provided. + pub fn sync_revealed_spks_request(&self, chain_tip: CheckPoint) -> SyncRequest { + // Sync all revealed SPKs + let spks = self + .revealed_spks() + .map(|(_keychain, _index, script)| ScriptBuf::from(script)) + .collect::<Vec<ScriptBuf>>(); + + let mut req = SyncRequest::new(chain_tip); + req.add_spks(spks); + req + } + + /// Create a [`FullScanRequest`] for this [`KeychainTxOutIndex`]. + /// + /// This is the first step when performing a spk-based full scan, the returned [`FullScanRequest`] + /// collects iterators for the index's keychain script pub keys to start a blockchain full scan with a + /// spk based blockchain client. A [`CheckPoint`] representing the current chain tip must be provided. + /// + /// This operation is generally only used when importing or restoring previously used keychains + /// in which the list of used scripts is not known. + pub fn full_scan_request( + &self, + chain_tip: CheckPoint, + ) -> FullScanRequest<K, impl Iterator<Item = (u32, ScriptBuf)> + Clone> { + let spks_by_keychain = self.all_unbounded_spk_iters(); + + let mut req = FullScanRequest::new(chain_tip); + req.add_spks_by_keychain(spks_by_keychain); + req + } + /// Applies the derivation changeset to the [`KeychainTxOutIndex`], extending the number of /// derived scripts per keychain, as specified in the `changeset`. pub fn apply_changeset(&mut self, changeset: super::ChangeSet<K>) { diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 2065669714..9d2cb06eba 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -52,6 +52,9 @@ mod spk_iter; #[cfg(feature = "miniscript")] pub use spk_iter::*; +/// Helper types for use with spk-based blockchain clients. +pub mod spk_client; + #[allow(unused_imports)] #[macro_use] extern crate alloc; diff --git a/crates/chain/src/spk_client.rs b/crates/chain/src/spk_client.rs new file mode 100644 index 0000000000..03331a1d37 --- /dev/null +++ b/crates/chain/src/spk_client.rs @@ -0,0 +1,182 @@ +use crate::collections::BTreeMap; +use crate::local_chain::CheckPoint; +use crate::{local_chain, ConfirmationTimeHeightAnchor, TxGraph}; +use alloc::{boxed::Box, vec::Vec}; +use bitcoin::{OutPoint, ScriptBuf, Txid}; +use core::default::Default; + +type InspectSpkFn = Box<dyn FnMut(&ScriptBuf) + Send>; +type InspectTxidFn = Box<dyn FnMut(&Txid) + Send>; +type InspectOutPointFn = Box<dyn FnMut(&OutPoint) + Send>; + +/// Helper types for use with spk-based blockchain clients. + +/// Data required to perform a spk-based blockchain client sync. +/// +/// A client sync fetches relevant chain data for a known list of scripts, transaction ids and +/// outpoints. The sync process also updates the chain from the given [`CheckPoint`]. +pub struct SyncRequest { + /// A checkpoint for the current chain tip. + /// The full scan process will return a new chain update that extends this tip. + pub chain_tip: CheckPoint, + /// Transactions that spend from or to these script pubkeys. + spks: Vec<ScriptBuf>, + /// Transactions with these txids. + txids: Vec<Txid>, + /// Transactions with these outpoints or spend from these outpoints. + outpoints: Vec<OutPoint>, + /// An optional call-back function to inspect sync'd spks + inspect_spks: Option<InspectSpkFn>, + /// An optional call-back function to inspect sync'd txids + inspect_txids: Option<InspectTxidFn>, + /// An optional call-back function to inspect sync'd outpoints + inspect_outpoints: Option<InspectOutPointFn>, +} + +fn null_inspect_spks(_spk: &ScriptBuf) {} +fn null_inspect_txids(_txid: &Txid) {} +fn null_inspect_outpoints(_outpoint: &OutPoint) {} + +impl SyncRequest { + /// Create a new [`SyncRequest`] from the current chain tip [`CheckPoint`]. + pub fn new(chain_tip: CheckPoint) -> Self { + Self { + chain_tip, + spks: Default::default(), + txids: Default::default(), + outpoints: Default::default(), + inspect_spks: Default::default(), + inspect_txids: Default::default(), + inspect_outpoints: Default::default(), + } + } + + /// Add [`ScriptBuf`]s to be sync'd with this request. + pub fn add_spks(&mut self, spks: impl IntoIterator<Item = ScriptBuf>) { + self.spks.extend(spks.into_iter()) + } + + /// Take the [`ScriptBuf`]s to be sync'd with this request. + pub fn take_spks(&mut self) -> impl Iterator<Item = ScriptBuf> { + let spks = core::mem::take(&mut self.spks); + let mut inspect = self + .inspect_spks + .take() + .unwrap_or(Box::new(null_inspect_spks)); + spks.into_iter().inspect(move |s| inspect(s)) + } + + /// Add a function that will be called for each [`ScriptBuf`] sync'd in this request. + pub fn inspect_spks(&mut self, inspect: impl FnMut(&ScriptBuf) + Send + 'static) { + self.inspect_spks = Some(Box::new(inspect)) + } + + /// Add [`Txid`]s to be sync'd with this request. + pub fn add_txids(&mut self, txids: impl IntoIterator<Item = Txid>) { + self.txids.extend(txids.into_iter()) + } + + /// Take the [`Txid`]s to be sync'd with this request. + pub fn take_txids(&mut self) -> impl Iterator<Item = Txid> { + let txids = core::mem::take(&mut self.txids); + let mut inspect = self + .inspect_txids + .take() + .unwrap_or(Box::new(null_inspect_txids)); + txids.into_iter().inspect(move |t| inspect(t)) + } + + /// Add a function that will be called for each [`Txid`] sync'd in this request. + pub fn inspect_txids(&mut self, inspect: impl FnMut(&Txid) + Send + 'static) { + self.inspect_txids = Some(Box::new(inspect)) + } + + /// Add [`OutPoint`]s to be sync'd with this request. + pub fn add_outpoints(&mut self, outpoints: impl IntoIterator<Item = OutPoint>) { + self.outpoints.extend(outpoints.into_iter()) + } + + /// Take the [`OutPoint`]s to be sync'd with this request. + pub fn take_outpoints(&mut self) -> impl Iterator<Item = OutPoint> { + let outpoints = core::mem::take(&mut self.outpoints); + let mut inspect = self + .inspect_outpoints + .take() + .unwrap_or(Box::new(null_inspect_outpoints)); + outpoints.into_iter().inspect(move |o| inspect(o)) + } + + /// Add a function that will be called for each [`OutPoint`] sync'd in this request. + pub fn inspect_outpoints(&mut self, inspect: impl FnMut(&OutPoint) + Send + 'static) { + self.inspect_outpoints = Some(Box::new(inspect)) + } +} + +/// Data returned from a spk-based blockchain client sync. +/// +/// See also [`SyncRequest`]. +pub struct SyncResult { + /// [`TxGraph`] update. + pub graph_update: TxGraph<ConfirmationTimeHeightAnchor>, + /// [`LocalChain`] update. + /// + /// [`LocalChain`]: local_chain::LocalChain + pub chain_update: local_chain::Update, +} + +/// Data required to perform a spk-based blockchain client full scan. +/// +/// A client full scan iterates through all the scripts for the given keychains, fetching relevant +/// data until some stop gap number of scripts is found that have no data. This operation is +/// generally only used when importing or restoring previously used keychains in which the list of +/// used scripts is not known. The full scan process also updates the chain from the given [`CheckPoint`]. +#[derive(Debug, Clone)] +pub struct FullScanRequest<K, I> { + /// A checkpoint for the current chain tip. The full scan process will return a new chain update that extends this tip. + pub chain_tip: CheckPoint, + /// Iterators of script pubkeys indexed by the keychain index. + spks_by_keychain: BTreeMap<K, I>, +} + +/// Create a new [`FullScanRequest`] from the current chain tip [`CheckPoint`]. +impl< + K: Ord + Clone + Send, + I: IntoIterator<IntoIter = impl Iterator<Item = (u32, ScriptBuf)> + Send> + Send, + > FullScanRequest<K, I> +{ + /// Create a new [`FullScanRequest`] from the current chain tip [`CheckPoint`]. + pub fn new(chain_tip: CheckPoint) -> Self { + Self { + chain_tip, + spks_by_keychain: Default::default(), + } + } + + /// Add map of keychain's to tuple of index, [`ScriptBuf`] iterators to be scanned with this + /// request. + /// + /// Adding a map with a keychain that has already been added will overwrite the previously added + /// keychain [`ScriptBuf`] iterator. + pub fn add_spks_by_keychain(&mut self, spks_by_keychain: BTreeMap<K, I>) { + self.spks_by_keychain.extend(spks_by_keychain) + } + + /// Take the map of keychain, [`ScriptBuf`]s to be full scanned with this request. + pub fn take_spks_by_keychain(&mut self) -> BTreeMap<K, I> { + core::mem::take(&mut self.spks_by_keychain) + } +} + +/// Data returned from a spk-based blockchain client full scan. +/// +/// See also [`FullScanRequest`]. +pub struct FullScanResult<K: Ord + Clone + Send> { + /// [`TxGraph`] update. + pub graph_update: TxGraph<ConfirmationTimeHeightAnchor>, + /// [`LocalChain`] update. + /// + /// [`LocalChain`]: local_chain::LocalChain + pub chain_update: local_chain::Update, + /// Map of keychain last active indices. + pub last_active_indices: BTreeMap<K, u32>, +} diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 34cbccf5ce..16e158c41c 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -723,11 +723,6 @@ impl<A: Anchor> TxGraph<A> { } !has_missing_height }); - #[cfg(feature = "std")] - debug_assert!({ - println!("txid={} skip={}", txid, skip); - true - }); !skip }) .filter_map(move |(a, _)| { @@ -743,6 +738,24 @@ impl<A: Anchor> TxGraph<A> { }) } + /// Iterates over the heights of that the transaction anchors in this tx graph greater than a minimum height. + /// + /// This is useful if you want to find which heights you need to fetch data about in order to + /// confirm or exclude these anchors. + /// + /// See also: [`Self::missing_heights`] + pub fn anchor_heights(&self, min_height: u32) -> impl Iterator<Item = u32> + '_ { + let mut dedup = None; + self.anchors + .iter() + .map(|(a, _)| a.anchor_block().height) + .filter(move |height| { + let duplicate = dedup == Some(*height); + dedup = Some(*height); + !duplicate && *height > min_height + }) + } + /// Get the position of the transaction in `chain` with tip `chain_tip`. /// /// Chain data is fetched from `chain`, a [`ChainOracle`] implementation.