feat(chain): add sync/full-scan structures for spk-based syncing
These structures allows spk-based chain-sources to have a universal API. Co-authored-by: Steve Myers <steve@notmandatory.org>
This commit is contained in:
parent
8e73998cfa
commit
cdfec5f907
@ -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]
|
||||
|
315
crates/chain/src/spk_client.rs
Normal file
315
crates/chain/src/spk_client.rs
Normal file
@ -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>,
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user