Skip to content

Commit

Permalink
feat(chain): add sync/full-scan structures for spk-based syncing
Browse files Browse the repository at this point in the history
These structures allows spk-based chain-sources to have a universal API.

Co-authored-by: Steve Myers <[email protected]>
  • Loading branch information
evanlinjin and notmandatory committed Apr 26, 2024
1 parent 8e73998 commit cdfec5f
Show file tree
Hide file tree
Showing 2 changed files with 316 additions and 0 deletions.
1 change: 1 addition & 0 deletions crates/chain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
315 changes: 315 additions & 0 deletions crates/chain/src/spk_client.rs
Original file line number Diff line number Diff line change
@@ -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<dyn Iterator<Item = ScriptBuf> + Send>,
/// Transactions with these txids.
pub txids: Box<dyn Iterator<Item = Txid> + Send>,
/// Transactions with these outpoints or spent from these outpoints.
pub outpoints: Box<dyn Iterator<Item = OutPoint> + 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<IntoIter = impl Iterator<Item = ScriptBuf> + 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<IntoIter = impl Iterator<Item = Txid> + 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<IntoIter = impl Iterator<Item = OutPoint> + 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<Item = ScriptBuf> + 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<IntoIter = impl Iterator<Item = Txid> + 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<Item = OutPoint> + 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<K: Clone + Ord + Debug + Send + Sync>(
self,
index: &crate::keychain::KeychainTxOutIndex<K>,
spk_range: impl RangeBounds<K>,
) -> Self {
use alloc::borrow::ToOwned;
self.chain_spks(
index
.revealed_spks(spk_range)
.map(|(_, _, spk)| spk.to_owned())
.collect::<Vec<_>>(),
)
}
}

/// Data returned from a spk-based blockchain client sync.
///
/// See also [`SyncRequest`].
pub struct SyncResult<A = ConfirmationTimeHeightAnchor> {
/// The update to apply to the receiving [`TxGraph`].
pub graph_update: TxGraph<A>,
/// 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<K> {
/// 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<K, Box<dyn Iterator<Item = (u32, ScriptBuf)> + Send>>,
}

impl<K: Ord + Clone> FullScanRequest<K> {
/// 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<K>,
) -> 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<IntoIter = impl Iterator<Item = (u32, ScriptBuf)> + 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<IntoIter = impl Iterator<Item = (u32, ScriptBuf)> + 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<K> {
/// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
pub graph_update: TxGraph<ConfirmationTimeHeightAnchor>,
/// 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<K, u32>,
}

0 comments on commit cdfec5f

Please sign in to comment.