Skip to content

Commit

Permalink
feat(chain): add SyncRequest and FullScanRequest structures
Browse files Browse the repository at this point in the history
  • Loading branch information
notmandatory committed Feb 25, 2024
1 parent 2c324d3 commit 9385dcf
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 11 deletions.
52 changes: 46 additions & 6 deletions crates/chain/src/keychain/txout_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -147,7 +150,7 @@ impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
}
}

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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -664,6 +667,43 @@ 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, SpkIterator<Descriptor<DescriptorPublicKey>>> {
let spks_by_keychain: BTreeMap<K, SpkIterator<Descriptor<DescriptorPublicKey>>> =
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>) {
Expand Down
3 changes: 3 additions & 0 deletions crates/chain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
205 changes: 205 additions & 0 deletions crates/chain/src/spk_client.rs
Original file line number Diff line number Diff line change
@@ -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<dyn FnMut(&ScriptBuf) + Send>;
type InspectSpkTupleFn = Box<dyn Fn(&(u32, 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_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<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`].
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>,
/// An optional call-back function to inspect scanned spks
inspect_spks: Option<InspectSpkTupleFn>,
}

/// Create a new [`FullScanRequest`] from the current chain tip [`CheckPoint`].
impl<K: Ord + Clone + Send, I: Iterator<Item = (u32, ScriptBuf)> + 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(),
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<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,
Box<impl IntoIterator<IntoIter = impl Iterator<Item = (u32, ScriptBuf)> + 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<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>,
}
23 changes: 18 additions & 5 deletions crates/chain/src/tx_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, _)| {
Expand All @@ -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.
Expand Down

0 comments on commit 9385dcf

Please sign in to comment.