diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 206566971..a05a4c3ee 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -51,6 +51,7 @@ pub use descriptor_ext::DescriptorExt; mod spk_iter; #[cfg(feature = "miniscript")] pub use spk_iter::*; +pub mod spk_client; #[allow(unused_imports)] #[macro_use] diff --git a/crates/chain/src/spk_client.rs b/crates/chain/src/spk_client.rs new file mode 100644 index 000000000..7873ba227 --- /dev/null +++ b/crates/chain/src/spk_client.rs @@ -0,0 +1,315 @@ +//! Helper types for spk-based blockchain clients. + +use core::{fmt::Debug, ops::RangeBounds}; + +use alloc::{boxed::Box, collections::BTreeMap, vec::Vec}; +use bitcoin::{OutPoint, Script, ScriptBuf, Txid}; + +use crate::{local_chain::CheckPoint, ConfirmationTimeHeightAnchor, TxGraph}; + +/// 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 [`LocalChain::tip`]. + /// The sync process will return a new chain update that extends this tip. + /// + /// [`LocalChain::tip`]: crate::local_chain::LocalChain::tip + pub chain_tip: CheckPoint, + /// Transactions that spend from or to these indexed script pubkeys. + pub spks: Box + Send>, + /// Transactions with these txids. + pub txids: Box + Send>, + /// Transactions with these outpoints or spent from these outpoints. + pub outpoints: Box + Send>, +} + +impl SyncRequest { + /// Construct a new [`SyncRequest`] from a given `cp` tip. + pub fn from_chain_tip(cp: CheckPoint) -> Self { + Self { + chain_tip: cp, + spks: Box::new(core::iter::empty()), + txids: Box::new(core::iter::empty()), + outpoints: Box::new(core::iter::empty()), + } + } + + /// Set the [`Script`]s that will be synced against. + /// + /// This consumes the [`SyncRequest`] and returns the updated one. + #[must_use] + pub fn set_spks( + mut self, + spks: impl IntoIterator + Send + 'static>, + ) -> Self { + self.spks = Box::new(spks.into_iter()); + self + } + + /// Set the [`Txid`]s that will be synced against. + /// + /// This consumes the [`SyncRequest`] and returns the updated one. + #[must_use] + pub fn set_txids( + mut self, + txids: impl IntoIterator + Send + 'static>, + ) -> Self { + self.txids = Box::new(txids.into_iter()); + self + } + + /// Set the [`OutPoint`]s that will be synced against. + /// + /// This consumes the [`SyncRequest`] and returns the updated one. + #[must_use] + pub fn set_outpoints( + mut self, + outpoints: impl IntoIterator + Send + 'static>, + ) -> Self { + self.outpoints = Box::new(outpoints.into_iter()); + self + } + + /// Chain on additional [`Script`]s that will be synced against. + /// + /// This consumes the [`SyncRequest`] and returns the updated one. + #[must_use] + pub fn chain_spks( + mut self, + spks: impl IntoIterator< + IntoIter = impl Iterator + Send + 'static, + Item = ScriptBuf, + >, + ) -> Self { + self.spks = Box::new(self.spks.chain(spks)); + self + } + + /// Chain on additional [`Txid`]s that will be synced against. + /// + /// This consumes the [`SyncRequest`] and returns the updated one. + #[must_use] + pub fn chain_txids( + mut self, + txids: impl IntoIterator + Send + 'static, Item = Txid>, + ) -> Self { + self.txids = Box::new(self.txids.chain(txids)); + self + } + + /// Chain on additional [`OutPoint`]s that will be synced against. + /// + /// This consumes the [`SyncRequest`] and returns the updated one. + #[must_use] + pub fn chain_outpoints( + mut self, + outpoints: impl IntoIterator< + IntoIter = impl Iterator + Send + 'static, + Item = OutPoint, + >, + ) -> Self { + self.outpoints = Box::new(self.outpoints.chain(outpoints)); + self + } + + /// Add a closure that will be called for each [`Script`] synced in this request. + /// + /// This consumes the [`SyncRequest`] and returns the updated one. + #[must_use] + pub fn inspect_spks(mut self, inspect: impl Fn(&Script) + Send + Sync + 'static) -> Self { + self.spks = Box::new(self.spks.inspect(move |spk| inspect(spk))); + self + } + + /// Add a closure that will be called for each [`Txid`] synced in this request. + /// + /// This consumes the [`SyncRequest`] and returns the updated one. + #[must_use] + pub fn inspect_txids(mut self, inspect: impl Fn(&Txid) + Send + Sync + 'static) -> Self { + self.txids = Box::new(self.txids.inspect(move |txid| inspect(txid))); + self + } + + /// Add a closure that will be called for each [`OutPoint`] synced in this request. + /// + /// This consumes the [`SyncRequest`] and returns the updated one. + #[must_use] + pub fn inspect_outpoints( + mut self, + inspect: impl Fn(&OutPoint) + Send + Sync + 'static, + ) -> Self { + self.outpoints = Box::new(self.outpoints.inspect(move |op| inspect(op))); + self + } + + /// Populate the request with revealed script pubkeys from `index` with the given `spk_range`. + /// + /// This consumes the [`SyncRequest`] and returns the updated one. + #[cfg(feature = "miniscript")] + #[must_use] + pub fn populate_with_revealed_spks( + self, + index: &crate::keychain::KeychainTxOutIndex, + spk_range: impl RangeBounds, + ) -> Self { + use alloc::borrow::ToOwned; + self.chain_spks( + index + .revealed_spks(spk_range) + .map(|(_, _, spk)| spk.to_owned()) + .collect::>(), + ) + } +} + +/// Data returned from a spk-based blockchain client sync. +/// +/// See also [`SyncRequest`]. +pub struct SyncResult { + /// The update to apply to the receiving [`TxGraph`]. + pub graph_update: TxGraph, + /// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain). + pub chain_update: CheckPoint, +} + +/// 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`]. +pub struct FullScanRequest { + /// A checkpoint for the current [`LocalChain::tip`]. + /// The full scan process will return a new chain update that extends this tip. + /// + /// [`LocalChain::tip`]: crate::local_chain::LocalChain::tip + pub chain_tip: CheckPoint, + /// Iterators of script pubkeys indexed by the keychain index. + pub spks_by_keychain: BTreeMap + Send>>, +} + +impl FullScanRequest { + /// Construct a new [`FullScanRequest`] from a given `chain_tip`. + #[must_use] + pub fn from_chain_tip(chain_tip: CheckPoint) -> Self { + Self { + chain_tip, + spks_by_keychain: BTreeMap::new(), + } + } + + /// Construct a new [`FullScanRequest`] from a given `chain_tip` and `index`. + /// + /// Unbounded script pubkey iterators for each keychain (`K`) are extracted using + /// [`KeychainTxOutIndex::all_unbounded_spk_iters`] and is used to populate the + /// [`FullScanRequest`]. + /// + /// [`KeychainTxOutIndex::all_unbounded_spk_iters`]: crate::keychain::KeychainTxOutIndex::all_unbounded_spk_iters + #[cfg(feature = "miniscript")] + #[must_use] + pub fn from_keychain_txout_index( + chain_tip: CheckPoint, + index: &crate::keychain::KeychainTxOutIndex, + ) -> Self + where + K: Debug, + { + let mut req = Self::from_chain_tip(chain_tip); + for (keychain, spks) in index.all_unbounded_spk_iters() { + req = req.set_spks_for_keychain(keychain, spks); + } + req + } + + /// Set the [`Script`]s for a given `keychain`. + /// + /// This consumes the [`FullScanRequest`] and returns the updated one. + #[must_use] + pub fn set_spks_for_keychain( + mut self, + keychain: K, + spks: impl IntoIterator + Send + 'static>, + ) -> Self { + self.spks_by_keychain + .insert(keychain, Box::new(spks.into_iter())); + self + } + + /// Chain on additional [`Script`]s that will be synced against. + /// + /// This consumes the [`FullScanRequest`] and returns the updated one. + #[must_use] + pub fn chain_spks_for_keychain( + mut self, + keychain: K, + spks: impl IntoIterator + Send + 'static>, + ) -> Self { + match self.spks_by_keychain.remove(&keychain) { + Some(keychain_spks) => self + .spks_by_keychain + .insert(keychain, Box::new(keychain_spks.chain(spks.into_iter()))), + None => self + .spks_by_keychain + .insert(keychain, Box::new(spks.into_iter())), + }; + self + } + + /// Add a closure that will be called for every [`Script`] previously added to any keychain in + /// this request. + /// + /// This consumes the [`SyncRequest`] and returns the updated one. + #[must_use] + pub fn inspect_spks_for_all_keychains( + mut self, + inspect: impl FnMut(K, u32, &Script) + Send + Sync + Clone + 'static, + ) -> Self + where + K: Send + 'static, + { + for (keychain, spks) in core::mem::take(&mut self.spks_by_keychain) { + let mut inspect = inspect.clone(); + self.spks_by_keychain.insert( + keychain.clone(), + Box::new(spks.inspect(move |(i, spk)| inspect(keychain.clone(), *i, spk))), + ); + } + self + } + + /// Add a closure that will be called for every [`Script`] previously added to a given + /// `keychain` in this request. + /// + /// This consumes the [`SyncRequest`] and returns the updated one. + #[must_use] + pub fn inspect_spks_for_keychain( + mut self, + keychain: K, + mut inspect: impl FnMut(u32, &Script) + Send + Sync + 'static, + ) -> Self + where + K: Send + 'static, + { + if let Some(spks) = self.spks_by_keychain.remove(&keychain) { + self.spks_by_keychain.insert( + keychain, + Box::new(spks.inspect(move |(i, spk)| inspect(*i, spk))), + ); + } + self + } +} + +/// Data returned from a spk-based blockchain client full scan. +/// +/// See also [`FullScanRequest`]. +pub struct FullScanResult { + /// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain). + pub graph_update: TxGraph, + /// The update to apply to the receiving [`TxGraph`]. + pub chain_update: CheckPoint, + /// Last active indices for the corresponding keychains (`K`). + pub last_active_indices: BTreeMap, +}