From 9385dcf3acb15114c9a0bb47932011faff46dc30 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Thu, 21 Dec 2023 11:22:15 -0600 Subject: [PATCH] feat(chain): add SyncRequest and FullScanRequest structures --- crates/chain/src/keychain/txout_index.rs | 52 +++++- crates/chain/src/lib.rs | 3 + crates/chain/src/spk_client.rs | 205 +++++++++++++++++++++++ crates/chain/src/tx_graph.rs | 23 ++- 4 files changed, 272 insertions(+), 11 deletions(-) create mode 100644 crates/chain/src/spk_client.rs diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs index da6a1e25ba..e06346e14e 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; @@ -110,13 +113,13 @@ pub struct KeychainTxOutIndex { lookahead: u32, } -impl Default for KeychainTxOutIndex { +impl Default for KeychainTxOutIndex { fn default() -> Self { Self::new(DEFAULT_LOOKAHEAD) } } -impl Indexer for KeychainTxOutIndex { +impl Indexer for KeychainTxOutIndex { type ChangeSet = super::ChangeSet; fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet { @@ -147,7 +150,7 @@ impl Indexer for KeychainTxOutIndex { } } -impl KeychainTxOutIndex { +impl KeychainTxOutIndex { /// 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 KeychainTxOutIndex { } /// Methods that are *re-exposed* from the internal [`SpkTxOutIndex`]. -impl KeychainTxOutIndex { +impl KeychainTxOutIndex { /// Return a reference to the internal [`SpkTxOutIndex`]. /// /// **WARNING:** The internal index will contain lookahead spks. Refer to @@ -291,7 +294,7 @@ impl KeychainTxOutIndex { } } -impl KeychainTxOutIndex { +impl KeychainTxOutIndex { /// Return a reference to the internal map of keychain to descriptors. pub fn keychains(&self) -> &BTreeMap> { &self.keychains @@ -664,6 +667,43 @@ impl KeychainTxOutIndex { .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::>(); + + 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>> { + let spks_by_keychain: BTreeMap>> = + 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) { 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..fe5f719e34 --- /dev/null +++ b/crates/chain/src/spk_client.rs @@ -0,0 +1,205 @@ +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; +type InspectSpkTupleFn = Box; +type InspectTxidFn = Box; +type InspectOutPointFn = Box; + +/// 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, + /// Transactions with these txids. + txids: Vec, + /// Transactions with these outpoints or spend from these outpoints. + outpoints: Vec, + /// An optional call-back function to inspect sync'd spks + inspect_spks: Option, + /// An optional call-back function to inspect sync'd txids + inspect_txids: Option, + /// An optional call-back function to inspect sync'd outpoints + inspect_outpoints: Option, +} + +fn null_inspect_spks(_spk: &ScriptBuf) {} +fn null_inspect_spks_tuple(_spk_tuple: &(u32, 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) { + 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 { + 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) { + 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 { + 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) { + 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 { + 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, + /// [`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`]. +pub struct FullScanRequest { + /// 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, + /// An optional call-back function to inspect scanned spks + inspect_spks: Option, +} + +/// Create a new [`FullScanRequest`] from the current chain tip [`CheckPoint`]. +impl + Send> FullScanRequest { + /// 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(), + inspect_spks: 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) { + 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, + Box + Send> + Send>, + > { + let spks = core::mem::take(&mut self.spks_by_keychain); + + spks.into_iter() + .map(move |(k, spk_iter)| { + let inspect = self + .inspect_spks + .take() + .unwrap_or(Box::new(null_inspect_spks_tuple)); + + let spk_iter_inspected = Box::new(spk_iter.inspect(inspect)); + (k, spk_iter_inspected) + }) + .collect() + // spks + } + + /// Add a function that will be called for each [`ScriptBuf`] sync'd in this request. + pub fn inspect_spks(&mut self, inspect: impl Fn(&(u32, ScriptBuf)) + Send + 'static) { + self.inspect_spks = Some(Box::new(inspect)) + } +} + +/// Data returned from a spk-based blockchain client full scan. +/// +/// See also [`FullScanRequest`]. +pub struct FullScanResult { + /// [`TxGraph`] update. + pub graph_update: TxGraph, + /// [`LocalChain`] update. + /// + /// [`LocalChain`]: local_chain::LocalChain + pub chain_update: local_chain::Update, + /// Map of keychain last active indices. + pub last_active_indices: BTreeMap, +} 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 TxGraph { } !has_missing_height }); - #[cfg(feature = "std")] - debug_assert!({ - println!("txid={} skip={}", txid, skip); - true - }); !skip }) .filter_map(move |(a, _)| { @@ -743,6 +738,24 @@ impl TxGraph { }) } + /// 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 + '_ { + 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.