From: valued mammal Date: Fri, 28 Jun 2024 13:07:36 +0000 (-0400) Subject: ref(chain)!: create module `indexer` X-Git-Tag: v1.0.0-beta.1~7^2 X-Git-Url: http://internal-gitweb-vhost/script/%22https:/database/enum.EncodeError.html?a=commitdiff_plain;h=c3fc1dd12337bf9bdb99323a51110b7ad6a00221;p=bdk ref(chain)!: create module `indexer` and replace keychain module with `balance.rs` --- diff --git a/crates/bitcoind_rpc/tests/test_emitter.rs b/crates/bitcoind_rpc/tests/test_emitter.rs index 2c8b1e11..2c2c863d 100644 --- a/crates/bitcoind_rpc/tests/test_emitter.rs +++ b/crates/bitcoind_rpc/tests/test_emitter.rs @@ -3,9 +3,8 @@ use std::collections::{BTreeMap, BTreeSet}; use bdk_bitcoind_rpc::Emitter; use bdk_chain::{ bitcoin::{Address, Amount, Txid}, - keychain::Balance, local_chain::{CheckPoint, LocalChain}, - Append, BlockId, IndexedTxGraph, SpkTxOutIndex, + Append, Balance, BlockId, IndexedTxGraph, SpkTxOutIndex, }; use bdk_testenv::{anyhow, TestEnv}; use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash}; diff --git a/crates/chain/src/balance.rs b/crates/chain/src/balance.rs new file mode 100644 index 00000000..569755a9 --- /dev/null +++ b/crates/chain/src/balance.rs @@ -0,0 +1,57 @@ +use bitcoin::Amount; + +/// Balance, differentiated into various categories. +#[derive(Debug, PartialEq, Eq, Clone, Default)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(crate = "serde_crate",) +)] +pub struct Balance { + /// All coinbase outputs not yet matured + pub immature: Amount, + /// Unconfirmed UTXOs generated by a wallet tx + pub trusted_pending: Amount, + /// Unconfirmed UTXOs received from an external wallet + pub untrusted_pending: Amount, + /// Confirmed and immediately spendable balance + pub confirmed: Amount, +} + +impl Balance { + /// Get sum of trusted_pending and confirmed coins. + /// + /// This is the balance you can spend right now that shouldn't get cancelled via another party + /// double spending it. + pub fn trusted_spendable(&self) -> Amount { + self.confirmed + self.trusted_pending + } + + /// Get the whole balance visible to the wallet. + pub fn total(&self) -> Amount { + self.confirmed + self.trusted_pending + self.untrusted_pending + self.immature + } +} + +impl core::fmt::Display for Balance { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "{{ immature: {}, trusted_pending: {}, untrusted_pending: {}, confirmed: {} }}", + self.immature, self.trusted_pending, self.untrusted_pending, self.confirmed + ) + } +} + +impl core::ops::Add for Balance { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self { + immature: self.immature + other.immature, + trusted_pending: self.trusted_pending + other.trusted_pending, + untrusted_pending: self.untrusted_pending + other.untrusted_pending, + confirmed: self.confirmed + other.confirmed, + } + } +} diff --git a/crates/chain/src/changeset.rs b/crates/chain/src/changeset.rs index d8422840..3dabc5a4 100644 --- a/crates/chain/src/changeset.rs +++ b/crates/chain/src/changeset.rs @@ -16,7 +16,8 @@ pub struct CombinedChangeSet { /// Changes to the [`LocalChain`](crate::local_chain::LocalChain). pub chain: crate::local_chain::ChangeSet, /// Changes to [`IndexedTxGraph`](crate::indexed_tx_graph::IndexedTxGraph). - pub indexed_tx_graph: crate::indexed_tx_graph::ChangeSet>, + pub indexed_tx_graph: + crate::indexed_tx_graph::ChangeSet>, /// Stores the network type of the transaction data. pub network: Option, } @@ -62,11 +63,14 @@ impl From for CombinedChangeSet { } #[cfg(feature = "miniscript")] -impl From>> +impl From>> for CombinedChangeSet { fn from( - indexed_tx_graph: crate::indexed_tx_graph::ChangeSet>, + indexed_tx_graph: crate::indexed_tx_graph::ChangeSet< + A, + crate::indexer::keychain_txout::ChangeSet, + >, ) -> Self { Self { indexed_tx_graph, @@ -76,8 +80,8 @@ impl From From> for CombinedChangeSet { - fn from(indexer: crate::keychain::ChangeSet) -> Self { +impl From> for CombinedChangeSet { + fn from(indexer: crate::indexer::keychain_txout::ChangeSet) -> Self { Self { indexed_tx_graph: crate::indexed_tx_graph::ChangeSet { indexer, diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 3848ba64..41392c02 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -5,7 +5,7 @@ use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid}; use crate::{ tx_graph::{self, TxGraph}, - Anchor, AnchorFromBlockPosition, Append, BlockId, + Anchor, AnchorFromBlockPosition, Append, BlockId, Indexer, }; /// The [`IndexedTxGraph`] combines a [`TxGraph`] and an [`Indexer`] implementation. @@ -320,8 +320,10 @@ impl From> for ChangeSet { } #[cfg(feature = "miniscript")] -impl From> for ChangeSet> { - fn from(indexer: crate::keychain::ChangeSet) -> Self { +impl From> + for ChangeSet> +{ + fn from(indexer: crate::indexer::keychain_txout::ChangeSet) -> Self { Self { graph: Default::default(), indexer, @@ -329,30 +331,6 @@ impl From> for ChangeSet Self::ChangeSet; - - /// Scans a transaction for relevant outpoints, which are stored and indexed internally. - fn index_tx(&mut self, tx: &Transaction) -> Self::ChangeSet; - - /// Apply changeset to itself. - fn apply_changeset(&mut self, changeset: Self::ChangeSet); - - /// Determines the [`ChangeSet`] between `self` and an empty [`Indexer`]. - fn initial_changeset(&self) -> Self::ChangeSet; - - /// Determines whether the transaction should be included in the index. - fn is_tx_relevant(&self, tx: &Transaction) -> bool; -} - impl AsRef> for IndexedTxGraph { fn as_ref(&self) -> &TxGraph { &self.graph diff --git a/crates/chain/src/indexer.rs b/crates/chain/src/indexer.rs new file mode 100644 index 00000000..22e83981 --- /dev/null +++ b/crates/chain/src/indexer.rs @@ -0,0 +1,33 @@ +//! [`Indexer`] provides utilities for indexing transaction data. + +use bitcoin::{OutPoint, Transaction, TxOut}; + +#[cfg(feature = "miniscript")] +pub mod keychain_txout; +pub mod spk_txout; + +/// Utilities for indexing transaction data. +/// +/// Types which implement this trait can be used to construct an [`IndexedTxGraph`]. +/// This trait's methods should rarely be called directly. +/// +/// [`IndexedTxGraph`]: crate::IndexedTxGraph +pub trait Indexer { + /// The resultant "changeset" when new transaction data is indexed. + type ChangeSet; + + /// Scan and index the given `outpoint` and `txout`. + fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet; + + /// Scans a transaction for relevant outpoints, which are stored and indexed internally. + fn index_tx(&mut self, tx: &Transaction) -> Self::ChangeSet; + + /// Apply changeset to itself. + fn apply_changeset(&mut self, changeset: Self::ChangeSet); + + /// Determines the [`ChangeSet`](Indexer::ChangeSet) between `self` and an empty [`Indexer`]. + fn initial_changeset(&self) -> Self::ChangeSet; + + /// Determines whether the transaction should be included in the index. + fn is_tx_relevant(&self, tx: &Transaction) -> bool; +} diff --git a/crates/chain/src/indexer/keychain_txout.rs b/crates/chain/src/indexer/keychain_txout.rs new file mode 100644 index 00000000..1d9f41f7 --- /dev/null +++ b/crates/chain/src/indexer/keychain_txout.rs @@ -0,0 +1,946 @@ +//! [`KeychainTxOutIndex`] controls how script pubkeys are revealed for multiple keychains and +//! indexes [`TxOut`]s with them. + +use crate::{ + collections::*, + miniscript::{Descriptor, DescriptorPublicKey}, + spk_iter::BIP32_MAX_INDEX, + DescriptorExt, DescriptorId, Indexed, Indexer, KeychainIndexed, SpkIterator, SpkTxOutIndex, +}; +use alloc::{borrow::ToOwned, vec::Vec}; +use bitcoin::{Amount, OutPoint, Script, ScriptBuf, SignedAmount, Transaction, TxOut, Txid}; +use core::{ + fmt::Debug, + ops::{Bound, RangeBounds}, +}; + +use crate::Append; + +/// The default lookahead for a [`KeychainTxOutIndex`] +pub const DEFAULT_LOOKAHEAD: u32 = 25; + +/// [`KeychainTxOutIndex`] controls how script pubkeys are revealed for multiple keychains, and +/// indexes [`TxOut`]s with them. +/// +/// A single keychain is a chain of script pubkeys derived from a single [`Descriptor`]. Keychains +/// are identified using the `K` generic. Script pubkeys are identified by the keychain that they +/// are derived from `K`, as well as the derivation index `u32`. +/// +/// There is a strict 1-to-1 relationship between descriptors and keychains. Each keychain has one +/// and only one descriptor and each descriptor has one and only one keychain. The +/// [`insert_descriptor`] method will return an error if you try and violate this invariant. This +/// rule is a proxy for a stronger rule: no two descriptors should produce the same script pubkey. +/// Having two descriptors produce the same script pubkey should cause whichever keychain derives +/// the script pubkey first to be the effective owner of it but you should not rely on this +/// behaviour. ⚠ It is up you, the developer, not to violate this invariant. +/// +/// # Revealed script pubkeys +/// +/// Tracking how script pubkeys are revealed is useful for collecting chain data. For example, if +/// the user has requested 5 script pubkeys (to receive money with), we only need to use those +/// script pubkeys to scan for chain data. +/// +/// Call [`reveal_to_target`] or [`reveal_next_spk`] to reveal more script pubkeys. +/// Call [`revealed_keychain_spks`] or [`revealed_spks`] to iterate through revealed script pubkeys. +/// +/// # Lookahead script pubkeys +/// +/// When an user first recovers a wallet (i.e. from a recovery phrase and/or descriptor), we will +/// NOT have knowledge of which script pubkeys are revealed. So when we index a transaction or +/// txout (using [`index_tx`]/[`index_txout`]) we scan the txouts against script pubkeys derived +/// above the last revealed index. These additionally-derived script pubkeys are called the +/// lookahead. +/// +/// The [`KeychainTxOutIndex`] is constructed with the `lookahead` and cannot be altered. See +/// [`DEFAULT_LOOKAHEAD`] for the value used in the `Default` implementation. Use [`new`] to set a +/// custom `lookahead`. +/// +/// # Unbounded script pubkey iterator +/// +/// For script-pubkey-based chain sources (such as Electrum/Esplora), an initial scan is best done +/// by iterating though derived script pubkeys one by one and requesting transaction histories for +/// each script pubkey. We will stop after x-number of script pubkeys have empty histories. An +/// unbounded script pubkey iterator is useful to pass to such a chain source because it doesn't +/// require holding a reference to the index. +/// +/// Call [`unbounded_spk_iter`] to get an unbounded script pubkey iterator for a given keychain. +/// Call [`all_unbounded_spk_iters`] to get unbounded script pubkey iterators for all keychains. +/// +/// # Change sets +/// +/// Methods that can update the last revealed index or add keychains will return [`ChangeSet`] to report +/// these changes. This should be persisted for future recovery. +/// +/// ## Synopsis +/// +/// ``` +/// use bdk_chain::indexer::keychain_txout::KeychainTxOutIndex; +/// # use bdk_chain::{ miniscript::{Descriptor, DescriptorPublicKey} }; +/// # use core::str::FromStr; +/// +/// // imagine our service has internal and external addresses but also addresses for users +/// #[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)] +/// enum MyKeychain { +/// External, +/// Internal, +/// MyAppUser { +/// user_id: u32 +/// } +/// } +/// +/// let mut txout_index = KeychainTxOutIndex::::default(); +/// +/// # let secp = bdk_chain::bitcoin::secp256k1::Secp256k1::signing_only(); +/// # let (external_descriptor,_) = Descriptor::::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap(); +/// # let (internal_descriptor,_) = Descriptor::::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)").unwrap(); +/// # let (descriptor_42, _) = Descriptor::::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/2/*)").unwrap(); +/// let _ = txout_index.insert_descriptor(MyKeychain::External, external_descriptor)?; +/// let _ = txout_index.insert_descriptor(MyKeychain::Internal, internal_descriptor)?; +/// let _ = txout_index.insert_descriptor(MyKeychain::MyAppUser { user_id: 42 }, descriptor_42)?; +/// +/// let new_spk_for_user = txout_index.reveal_next_spk(&MyKeychain::MyAppUser{ user_id: 42 }); +/// # Ok::<_, bdk_chain::indexer::keychain_txout::InsertDescriptorError<_>>(()) +/// ``` +/// +/// [`Ord`]: core::cmp::Ord +/// [`SpkTxOutIndex`]: crate::spk_txout_index::SpkTxOutIndex +/// [`Descriptor`]: crate::miniscript::Descriptor +/// [`reveal_to_target`]: Self::reveal_to_target +/// [`reveal_next_spk`]: Self::reveal_next_spk +/// [`revealed_keychain_spks`]: Self::revealed_keychain_spks +/// [`revealed_spks`]: Self::revealed_spks +/// [`index_tx`]: Self::index_tx +/// [`index_txout`]: Self::index_txout +/// [`new`]: Self::new +/// [`unbounded_spk_iter`]: Self::unbounded_spk_iter +/// [`all_unbounded_spk_iters`]: Self::all_unbounded_spk_iters +/// [`outpoints`]: Self::outpoints +/// [`txouts`]: Self::txouts +/// [`unused_spks`]: Self::unused_spks +/// [`insert_descriptor`]: Self::insert_descriptor +#[derive(Clone, Debug)] +pub struct KeychainTxOutIndex { + inner: SpkTxOutIndex<(K, u32)>, + keychain_to_descriptor_id: BTreeMap, + descriptor_id_to_keychain: HashMap, + descriptors: HashMap>, + last_revealed: HashMap, + lookahead: u32, +} + +impl Default for KeychainTxOutIndex { + fn default() -> Self { + Self::new(DEFAULT_LOOKAHEAD) + } +} + +impl Indexer for KeychainTxOutIndex { + type ChangeSet = ChangeSet; + + fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet { + let mut changeset = ChangeSet::default(); + if let Some((keychain, index)) = self.inner.scan_txout(outpoint, txout).cloned() { + let did = self + .keychain_to_descriptor_id + .get(&keychain) + .expect("invariant"); + if self.last_revealed.get(did) < Some(&index) { + self.last_revealed.insert(*did, index); + changeset.last_revealed.insert(*did, index); + self.replenish_inner_index(*did, &keychain, self.lookahead); + } + } + changeset + } + + fn index_tx(&mut self, tx: &bitcoin::Transaction) -> Self::ChangeSet { + let mut changeset = ChangeSet::::default(); + let txid = tx.compute_txid(); + for (op, txout) in tx.output.iter().enumerate() { + changeset.append(self.index_txout(OutPoint::new(txid, op as u32), txout)); + } + changeset + } + + fn initial_changeset(&self) -> Self::ChangeSet { + ChangeSet { + keychains_added: self + .keychains() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), + last_revealed: self.last_revealed.clone().into_iter().collect(), + } + } + + fn apply_changeset(&mut self, changeset: Self::ChangeSet) { + self.apply_changeset(changeset) + } + + fn is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool { + self.inner.is_relevant(tx) + } +} + +impl KeychainTxOutIndex { + /// Construct a [`KeychainTxOutIndex`] with the given `lookahead`. + /// + /// The `lookahead` is the number of script pubkeys to derive and cache from the internal + /// descriptors over and above the last revealed script index. Without a lookahead the index + /// will miss outputs you own when processing transactions whose output script pubkeys lie + /// beyond the last revealed index. In certain situations, such as when performing an initial + /// scan of the blockchain during wallet import, it may be uncertain or unknown what the index + /// of the last revealed script pubkey actually is. + /// + /// Refer to [struct-level docs](KeychainTxOutIndex) for more about `lookahead`. + pub fn new(lookahead: u32) -> Self { + Self { + inner: SpkTxOutIndex::default(), + keychain_to_descriptor_id: Default::default(), + descriptors: Default::default(), + descriptor_id_to_keychain: Default::default(), + last_revealed: Default::default(), + lookahead, + } + } +} + +/// Methods that are *re-exposed* from the internal [`SpkTxOutIndex`]. +impl KeychainTxOutIndex { + /// Return a reference to the internal [`SpkTxOutIndex`]. + /// + /// **WARNING**: The internal index will contain lookahead spks. Refer to + /// [struct-level docs](KeychainTxOutIndex) for more about `lookahead`. + pub fn inner(&self) -> &SpkTxOutIndex<(K, u32)> { + &self.inner + } + + /// Get the set of indexed outpoints, corresponding to tracked keychains. + pub fn outpoints(&self) -> &BTreeSet> { + self.inner.outpoints() + } + + /// Iterate over known txouts that spend to tracked script pubkeys. + pub fn txouts( + &self, + ) -> impl DoubleEndedIterator> + ExactSizeIterator + { + self.inner + .txouts() + .map(|(index, op, txout)| (index.clone(), (op, txout))) + } + + /// Finds all txouts on a transaction that has previously been scanned and indexed. + pub fn txouts_in_tx( + &self, + txid: Txid, + ) -> impl DoubleEndedIterator> { + self.inner + .txouts_in_tx(txid) + .map(|(index, op, txout)| (index.clone(), (op, txout))) + } + + /// Return the [`TxOut`] of `outpoint` if it has been indexed, and if it corresponds to a + /// tracked keychain. + /// + /// The associated keychain and keychain index of the txout's spk is also returned. + /// + /// This calls [`SpkTxOutIndex::txout`] internally. + pub fn txout(&self, outpoint: OutPoint) -> Option> { + self.inner + .txout(outpoint) + .map(|(index, txout)| (index.clone(), txout)) + } + + /// Return the script that exists under the given `keychain`'s `index`. + /// + /// This calls [`SpkTxOutIndex::spk_at_index`] internally. + pub fn spk_at_index(&self, keychain: K, index: u32) -> Option<&Script> { + self.inner.spk_at_index(&(keychain.clone(), index)) + } + + /// Returns the keychain and keychain index associated with the spk. + /// + /// This calls [`SpkTxOutIndex::index_of_spk`] internally. + pub fn index_of_spk(&self, script: &Script) -> Option<&(K, u32)> { + self.inner.index_of_spk(script) + } + + /// Returns whether the spk under the `keychain`'s `index` has been used. + /// + /// Here, "unused" means that after the script pubkey was stored in the index, the index has + /// never scanned a transaction output with it. + /// + /// This calls [`SpkTxOutIndex::is_used`] internally. + pub fn is_used(&self, keychain: K, index: u32) -> bool { + self.inner.is_used(&(keychain, index)) + } + + /// Marks the script pubkey at `index` as used even though the tracker hasn't seen an output + /// with it. + /// + /// This only has an effect when the `index` had been added to `self` already and was unused. + /// + /// Returns whether the spk under the given `keychain` and `index` is successfully + /// marked as used. Returns false either when there is no descriptor under the given + /// keychain, or when the spk is already marked as used. + /// + /// This is useful when you want to reserve a script pubkey for something but don't want to add + /// the transaction output using it to the index yet. Other callers will consider `index` on + /// `keychain` used until you call [`unmark_used`]. + /// + /// This calls [`SpkTxOutIndex::mark_used`] internally. + /// + /// [`unmark_used`]: Self::unmark_used + pub fn mark_used(&mut self, keychain: K, index: u32) -> bool { + self.inner.mark_used(&(keychain, index)) + } + + /// Undoes the effect of [`mark_used`]. Returns whether the `index` is inserted back into + /// `unused`. + /// + /// Note that if `self` has scanned an output with this script pubkey, then this will have no + /// effect. + /// + /// This calls [`SpkTxOutIndex::unmark_used`] internally. + /// + /// [`mark_used`]: Self::mark_used + pub fn unmark_used(&mut self, keychain: K, index: u32) -> bool { + self.inner.unmark_used(&(keychain, index)) + } + + /// Computes the total value transfer effect `tx` has on the script pubkeys belonging to the + /// keychains in `range`. Value is *sent* when a script pubkey in the `range` is on an input and + /// *received* when it is on an output. For `sent` to be computed correctly, the output being + /// spent must have already been scanned by the index. Calculating received just uses the + /// [`Transaction`] outputs directly, so it will be correct even if it has not been scanned. + pub fn sent_and_received( + &self, + tx: &Transaction, + range: impl RangeBounds, + ) -> (Amount, Amount) { + self.inner + .sent_and_received(tx, self.map_to_inner_bounds(range)) + } + + /// Computes the net value that this transaction gives to the script pubkeys in the index and + /// *takes* from the transaction outputs in the index. Shorthand for calling + /// [`sent_and_received`] and subtracting sent from received. + /// + /// This calls [`SpkTxOutIndex::net_value`] internally. + /// + /// [`sent_and_received`]: Self::sent_and_received + pub fn net_value(&self, tx: &Transaction, range: impl RangeBounds) -> SignedAmount { + self.inner.net_value(tx, self.map_to_inner_bounds(range)) + } +} + +impl KeychainTxOutIndex { + /// Return all keychains and their corresponding descriptors. + pub fn keychains( + &self, + ) -> impl DoubleEndedIterator)> + ExactSizeIterator + '_ + { + self.keychain_to_descriptor_id + .iter() + .map(|(k, did)| (k, self.descriptors.get(did).expect("invariant"))) + } + + /// Insert a descriptor with a keychain associated to it. + /// + /// Adding a descriptor means you will be able to derive new script pubkeys under it and the + /// txout index will discover transaction outputs with those script pubkeys (once they've been + /// derived and added to the index). + /// + /// keychain <-> descriptor is a one-to-one mapping that cannot be changed. Attempting to do so + /// will return a [`InsertDescriptorError`]. + /// + /// `[KeychainTxOutIndex]` will prevent you from inserting two descriptors which derive the same + /// script pubkey at index 0, but it's up to you to ensure that descriptors don't collide at + /// other indices. If they do nothing catastrophic happens at the `KeychainTxOutIndex` level + /// (one keychain just becomes the defacto owner of that spk arbitrarily) but this may have + /// subtle implications up the application stack like one UTXO being missing from one keychain + /// because it has been assigned to another which produces the same script pubkey. + pub fn insert_descriptor( + &mut self, + keychain: K, + descriptor: Descriptor, + ) -> Result, InsertDescriptorError> { + let mut changeset = ChangeSet::::default(); + let did = descriptor.descriptor_id(); + if !self.keychain_to_descriptor_id.contains_key(&keychain) + && !self.descriptor_id_to_keychain.contains_key(&did) + { + self.descriptors.insert(did, descriptor.clone()); + self.keychain_to_descriptor_id.insert(keychain.clone(), did); + self.descriptor_id_to_keychain.insert(did, keychain.clone()); + self.replenish_inner_index(did, &keychain, self.lookahead); + changeset + .keychains_added + .insert(keychain.clone(), descriptor); + } else { + if let Some(existing_desc_id) = self.keychain_to_descriptor_id.get(&keychain) { + let descriptor = self.descriptors.get(existing_desc_id).expect("invariant"); + if *existing_desc_id != did { + return Err(InsertDescriptorError::KeychainAlreadyAssigned { + existing_assignment: descriptor.clone(), + keychain, + }); + } + } + + if let Some(existing_keychain) = self.descriptor_id_to_keychain.get(&did) { + let descriptor = self.descriptors.get(&did).expect("invariant").clone(); + + if *existing_keychain != keychain { + return Err(InsertDescriptorError::DescriptorAlreadyAssigned { + existing_assignment: existing_keychain.clone(), + descriptor, + }); + } + } + } + + Ok(changeset) + } + + /// Gets the descriptor associated with the keychain. Returns `None` if the keychain doesn't + /// have a descriptor associated with it. + pub fn get_descriptor(&self, keychain: &K) -> Option<&Descriptor> { + let did = self.keychain_to_descriptor_id.get(keychain)?; + self.descriptors.get(did) + } + + /// Get the lookahead setting. + /// + /// Refer to [`new`] for more information on the `lookahead`. + /// + /// [`new`]: Self::new + pub fn lookahead(&self) -> u32 { + self.lookahead + } + + /// Store lookahead scripts until `target_index` (inclusive). + /// + /// This does not change the global `lookahead` setting. + pub fn lookahead_to_target(&mut self, keychain: &K, target_index: u32) { + if let Some((next_index, _)) = self.next_index(keychain) { + let temp_lookahead = (target_index + 1) + .checked_sub(next_index) + .filter(|&index| index > 0); + + if let Some(temp_lookahead) = temp_lookahead { + self.replenish_inner_index_keychain(keychain, temp_lookahead); + } + } + } + + fn replenish_inner_index_did(&mut self, did: DescriptorId, lookahead: u32) { + if let Some(keychain) = self.descriptor_id_to_keychain.get(&did).cloned() { + self.replenish_inner_index(did, &keychain, lookahead); + } + } + + fn replenish_inner_index_keychain(&mut self, keychain: &K, lookahead: u32) { + if let Some(did) = self.keychain_to_descriptor_id.get(keychain) { + self.replenish_inner_index(*did, keychain, lookahead); + } + } + + /// Syncs the state of the inner spk index after changes to a keychain + fn replenish_inner_index(&mut self, did: DescriptorId, keychain: &K, lookahead: u32) { + let descriptor = self.descriptors.get(&did).expect("invariant"); + let next_store_index = self + .inner + .all_spks() + .range(&(keychain.clone(), u32::MIN)..=&(keychain.clone(), u32::MAX)) + .last() + .map_or(0, |((_, index), _)| *index + 1); + let next_reveal_index = self.last_revealed.get(&did).map_or(0, |v| *v + 1); + for (new_index, new_spk) in + SpkIterator::new_with_range(descriptor, next_store_index..next_reveal_index + lookahead) + { + let _inserted = self + .inner + .insert_spk((keychain.clone(), new_index), new_spk); + debug_assert!(_inserted, "replenish lookahead: must not have existing spk: keychain={:?}, lookahead={}, next_store_index={}, next_reveal_index={}", keychain, lookahead, next_store_index, next_reveal_index); + } + } + + /// Get an unbounded spk iterator over a given `keychain`. Returns `None` if the provided + /// keychain doesn't exist + pub fn unbounded_spk_iter( + &self, + keychain: &K, + ) -> Option>> { + let descriptor = self.get_descriptor(keychain)?.clone(); + Some(SpkIterator::new(descriptor)) + } + + /// Get unbounded spk iterators for all keychains. + pub fn all_unbounded_spk_iters( + &self, + ) -> BTreeMap>> { + self.keychain_to_descriptor_id + .iter() + .map(|(k, did)| { + ( + k.clone(), + SpkIterator::new(self.descriptors.get(did).expect("invariant").clone()), + ) + }) + .collect() + } + + /// Iterate over revealed spks of keychains in `range` + pub fn revealed_spks( + &self, + range: impl RangeBounds, + ) -> impl Iterator> { + let start = range.start_bound(); + let end = range.end_bound(); + let mut iter_last_revealed = self + .keychain_to_descriptor_id + .range((start, end)) + .map(|(k, did)| (k, self.last_revealed.get(did).cloned())); + let mut iter_spks = self + .inner + .all_spks() + .range(self.map_to_inner_bounds((start, end))); + let mut current_keychain = iter_last_revealed.next(); + // The reason we need a tricky algorithm is because of the "lookahead" feature which means + // that some of the spks in the SpkTxoutIndex will not have been revealed yet. So we need to + // filter out those spks that are above the last_revealed for that keychain. To do this we + // iterate through the last_revealed for each keychain and the spks for each keychain in + // tandem. This minimizes BTreeMap queries. + core::iter::from_fn(move || loop { + let ((keychain, index), spk) = iter_spks.next()?; + // We need to find the last revealed that matches the current spk we are considering so + // we skip ahead. + while current_keychain?.0 < keychain { + current_keychain = iter_last_revealed.next(); + } + let (current_keychain, last_revealed) = current_keychain?; + + if current_keychain == keychain && Some(*index) <= last_revealed { + break Some(((keychain.clone(), *index), spk.as_script())); + } + }) + } + + /// Iterate over revealed spks of the given `keychain` with ascending indices. + /// + /// This is a double ended iterator so you can easily reverse it to get an iterator where + /// the script pubkeys that were most recently revealed are first. + pub fn revealed_keychain_spks<'a>( + &'a self, + keychain: &'a K, + ) -> impl DoubleEndedIterator> + 'a { + let end = self + .last_revealed_index(keychain) + .map(|v| v + 1) + .unwrap_or(0); + self.inner + .all_spks() + .range((keychain.clone(), 0)..(keychain.clone(), end)) + .map(|((_, index), spk)| (*index, spk.as_script())) + } + + /// Iterate over revealed, but unused, spks of all keychains. + pub fn unused_spks( + &self, + ) -> impl DoubleEndedIterator> + Clone { + self.keychain_to_descriptor_id.keys().flat_map(|keychain| { + self.unused_keychain_spks(keychain) + .map(|(i, spk)| ((keychain.clone(), i), spk)) + }) + } + + /// Iterate over revealed, but unused, spks of the given `keychain`. + /// Returns an empty iterator if the provided keychain doesn't exist. + pub fn unused_keychain_spks( + &self, + keychain: &K, + ) -> impl DoubleEndedIterator> + Clone { + let end = match self.keychain_to_descriptor_id.get(keychain) { + Some(did) => self.last_revealed.get(did).map(|v| *v + 1).unwrap_or(0), + None => 0, + }; + + self.inner + .unused_spks((keychain.clone(), 0)..(keychain.clone(), end)) + .map(|((_, i), spk)| (*i, spk)) + } + + /// Get the next derivation index for `keychain`. The next index is the index after the last revealed + /// derivation index. + /// + /// The second field in the returned tuple represents whether the next derivation index is new. + /// There are two scenarios where the next derivation index is reused (not new): + /// + /// 1. The keychain's descriptor has no wildcard, and a script has already been revealed. + /// 2. The number of revealed scripts has already reached 2^31 (refer to BIP-32). + /// + /// Not checking the second field of the tuple may result in address reuse. + /// + /// Returns None if the provided `keychain` doesn't exist. + pub fn next_index(&self, keychain: &K) -> Option<(u32, bool)> { + let did = self.keychain_to_descriptor_id.get(keychain)?; + let last_index = self.last_revealed.get(did).cloned(); + let descriptor = self.descriptors.get(did).expect("invariant"); + + // we can only get the next index if the wildcard exists. + let has_wildcard = descriptor.has_wildcard(); + + Some(match last_index { + // if there is no index, next_index is always 0. + None => (0, true), + // descriptors without wildcards can only have one index. + Some(_) if !has_wildcard => (0, false), + // derivation index must be < 2^31 (BIP-32). + Some(index) if index > BIP32_MAX_INDEX => { + unreachable!("index is out of bounds") + } + Some(index) if index == BIP32_MAX_INDEX => (index, false), + // get the next derivation index. + Some(index) => (index + 1, true), + }) + } + + /// Get the last derivation index that is revealed for each keychain. + /// + /// Keychains with no revealed indices will not be included in the returned [`BTreeMap`]. + pub fn last_revealed_indices(&self) -> BTreeMap { + self.last_revealed + .iter() + .filter_map(|(desc_id, index)| { + let keychain = self.descriptor_id_to_keychain.get(desc_id)?; + Some((keychain.clone(), *index)) + }) + .collect() + } + + /// Get the last derivation index revealed for `keychain`. Returns None if the keychain doesn't + /// exist, or if the keychain doesn't have any revealed scripts. + pub fn last_revealed_index(&self, keychain: &K) -> Option { + let descriptor_id = self.keychain_to_descriptor_id.get(keychain)?; + self.last_revealed.get(descriptor_id).cloned() + } + + /// Convenience method to call [`Self::reveal_to_target`] on multiple keychains. + pub fn reveal_to_target_multi(&mut self, keychains: &BTreeMap) -> ChangeSet { + let mut changeset = ChangeSet::default(); + + for (keychain, &index) in keychains { + if let Some((_, new_changeset)) = self.reveal_to_target(keychain, index) { + changeset.append(new_changeset); + } + } + + changeset + } + + /// Reveals script pubkeys of the `keychain`'s descriptor **up to and including** the + /// `target_index`. + /// + /// If the `target_index` cannot be reached (due to the descriptor having no wildcard and/or + /// the `target_index` is in the hardened index range), this method will make a best-effort and + /// reveal up to the last possible index. + /// + /// This returns list of newly revealed indices (alongside their scripts) and a + /// [`ChangeSet`], which reports updates to the latest revealed index. If no new script + /// pubkeys are revealed, then both of these will be empty. + /// + /// Returns None if the provided `keychain` doesn't exist. + #[must_use] + pub fn reveal_to_target( + &mut self, + keychain: &K, + target_index: u32, + ) -> Option<(Vec>, ChangeSet)> { + let mut changeset = ChangeSet::default(); + let mut spks: Vec> = vec![]; + while let Some((i, new)) = self.next_index(keychain) { + if !new || i > target_index { + break; + } + match self.reveal_next_spk(keychain) { + Some(((i, spk), change)) => { + spks.push((i, spk)); + changeset.append(change); + } + None => break, + } + } + + Some((spks, changeset)) + } + + /// Attempts to reveal the next script pubkey for `keychain`. + /// + /// Returns the derivation index of the revealed script pubkey, the revealed script pubkey and a + /// [`ChangeSet`] which represents changes in the last revealed index (if any). + /// Returns None if the provided keychain doesn't exist. + /// + /// When a new script cannot be revealed, we return the last revealed script and an empty + /// [`ChangeSet`]. There are two scenarios when a new script pubkey cannot be derived: + /// + /// 1. The descriptor has no wildcard and already has one script revealed. + /// 2. The descriptor has already revealed scripts up to the numeric bound. + /// 3. There is no descriptor associated with the given keychain. + pub fn reveal_next_spk(&mut self, keychain: &K) -> Option<(Indexed, ChangeSet)> { + let (next_index, new) = self.next_index(keychain)?; + let mut changeset = ChangeSet::default(); + + if new { + let did = self.keychain_to_descriptor_id.get(keychain)?; + self.last_revealed.insert(*did, next_index); + changeset.last_revealed.insert(*did, next_index); + self.replenish_inner_index(*did, keychain, self.lookahead); + } + let script = self + .inner + .spk_at_index(&(keychain.clone(), next_index)) + .expect("we just inserted it"); + Some(((next_index, script.into()), changeset)) + } + + /// Gets the next unused script pubkey in the keychain. I.e., the script pubkey with the lowest + /// index that has not been used yet. + /// + /// This will derive and reveal a new script pubkey if no more unused script pubkeys exist. + /// + /// If the descriptor has no wildcard and already has a used script pubkey or if a descriptor + /// has used all scripts up to the derivation bounds, then the last derived script pubkey will be + /// returned. + /// + /// Returns `None` if there are no script pubkeys that have been used and no new script pubkey + /// could be revealed (see [`reveal_next_spk`] for when this happens). + /// + /// [`reveal_next_spk`]: Self::reveal_next_spk + pub fn next_unused_spk(&mut self, keychain: &K) -> Option<(Indexed, ChangeSet)> { + let next_unused = self + .unused_keychain_spks(keychain) + .next() + .map(|(i, spk)| ((i, spk.to_owned()), ChangeSet::default())); + + next_unused.or_else(|| self.reveal_next_spk(keychain)) + } + + /// Iterate over all [`OutPoint`]s that have `TxOut`s with script pubkeys derived from + /// `keychain`. + pub fn keychain_outpoints<'a>( + &'a self, + keychain: &'a K, + ) -> impl DoubleEndedIterator> + 'a { + self.keychain_outpoints_in_range(keychain..=keychain) + .map(|((_, i), op)| (i, op)) + } + + /// Iterate over [`OutPoint`]s that have script pubkeys derived from keychains in `range`. + pub fn keychain_outpoints_in_range<'a>( + &'a self, + range: impl RangeBounds + 'a, + ) -> impl DoubleEndedIterator> + 'a { + self.inner + .outputs_in_range(self.map_to_inner_bounds(range)) + .map(|((k, i), op)| ((k.clone(), *i), op)) + } + + fn map_to_inner_bounds(&self, bound: impl RangeBounds) -> impl RangeBounds<(K, u32)> { + let start = match bound.start_bound() { + Bound::Included(keychain) => Bound::Included((keychain.clone(), u32::MIN)), + Bound::Excluded(keychain) => Bound::Excluded((keychain.clone(), u32::MAX)), + Bound::Unbounded => Bound::Unbounded, + }; + let end = match bound.end_bound() { + Bound::Included(keychain) => Bound::Included((keychain.clone(), u32::MAX)), + Bound::Excluded(keychain) => Bound::Excluded((keychain.clone(), u32::MIN)), + Bound::Unbounded => Bound::Unbounded, + }; + + (start, end) + } + + /// Returns the highest derivation index of the `keychain` where [`KeychainTxOutIndex`] has + /// found a [`TxOut`] with it's script pubkey. + pub fn last_used_index(&self, keychain: &K) -> Option { + self.keychain_outpoints(keychain).last().map(|(i, _)| i) + } + + /// Returns the highest derivation index of each keychain that [`KeychainTxOutIndex`] has found + /// a [`TxOut`] with it's script pubkey. + pub fn last_used_indices(&self) -> BTreeMap { + self.keychain_to_descriptor_id + .iter() + .filter_map(|(keychain, _)| { + self.last_used_index(keychain) + .map(|index| (keychain.clone(), index)) + }) + .collect() + } + + /// Applies the `ChangeSet` to the [`KeychainTxOutIndex`] + /// + /// Keychains added by the `keychains_added` field of `ChangeSet` respect the one-to-one + /// keychain <-> descriptor invariant by silently ignoring attempts to violate it (but will + /// panic if `debug_assertions` are enabled). + pub fn apply_changeset(&mut self, changeset: ChangeSet) { + let ChangeSet { + keychains_added, + last_revealed, + } = changeset; + for (keychain, descriptor) in keychains_added { + let _ignore_invariant_violation = self.insert_descriptor(keychain, descriptor); + } + + for (&desc_id, &index) in &last_revealed { + let v = self.last_revealed.entry(desc_id).or_default(); + *v = index.max(*v); + } + + for did in last_revealed.keys() { + self.replenish_inner_index_did(*did, self.lookahead); + } + } +} + +#[derive(Clone, Debug, PartialEq)] +/// Error returned from [`KeychainTxOutIndex::insert_descriptor`] +pub enum InsertDescriptorError { + /// The descriptor has already been assigned to a keychain so you can't assign it to another + DescriptorAlreadyAssigned { + /// The descriptor you have attempted to reassign + descriptor: Descriptor, + /// The keychain that the descriptor is already assigned to + existing_assignment: K, + }, + /// The keychain is already assigned to a descriptor so you can't reassign it + KeychainAlreadyAssigned { + /// The keychain that you have attempted to reassign + keychain: K, + /// The descriptor that the keychain is already assigned to + existing_assignment: Descriptor, + }, +} + +impl core::fmt::Display for InsertDescriptorError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + InsertDescriptorError::DescriptorAlreadyAssigned { + existing_assignment: existing, + descriptor, + } => { + write!( + f, + "attempt to re-assign descriptor {descriptor:?} already assigned to {existing:?}" + ) + } + InsertDescriptorError::KeychainAlreadyAssigned { + existing_assignment: existing, + keychain, + } => { + write!( + f, + "attempt to re-assign keychain {keychain:?} already assigned to {existing:?}" + ) + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for InsertDescriptorError {} + +/// Represents updates to the derivation index of a [`KeychainTxOutIndex`]. +/// It maps each keychain `K` to a descriptor and its last revealed index. +/// +/// It can be applied to [`KeychainTxOutIndex`] with [`apply_changeset`]. +/// +/// The `last_revealed` field is monotone in that [`append`] will never decrease it. +/// `keychains_added` is *not* monotone, once it is set any attempt to change it is subject to the +/// same *one-to-one* keychain <-> descriptor mapping invariant as [`KeychainTxOutIndex`] itself. +/// +/// [`apply_changeset`]: KeychainTxOutIndex::apply_changeset +/// [`append`]: Self::append +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde( + crate = "serde_crate", + bound( + deserialize = "K: Ord + serde::Deserialize<'de>", + serialize = "K: Ord + serde::Serialize" + ) + ) +)] +#[must_use] +pub struct ChangeSet { + /// Contains the keychains that have been added and their respective descriptor + pub keychains_added: BTreeMap>, + /// Contains for each descriptor_id the last revealed index of derivation + pub last_revealed: BTreeMap, +} + +impl Append for ChangeSet { + /// Merge another [`ChangeSet`] into self. + /// + /// For the `keychains_added` field this method respects the invariants of + /// [`insert_descriptor`]. `last_revealed` always becomes the larger of the two. + /// + /// [`insert_descriptor`]: KeychainTxOutIndex::insert_descriptor + fn append(&mut self, other: Self) { + for (new_keychain, new_descriptor) in other.keychains_added { + // enforce 1-to-1 invariance + if !self.keychains_added.contains_key(&new_keychain) + // FIXME: very inefficient + && self + .keychains_added + .values() + .all(|descriptor| descriptor != &new_descriptor) + { + self.keychains_added.insert(new_keychain, new_descriptor); + } + } + + // for `last_revealed`, entries of `other` will take precedence ONLY if it is greater than + // what was originally in `self`. + for (desc_id, index) in other.last_revealed { + use crate::collections::btree_map::Entry; + match self.last_revealed.entry(desc_id) { + Entry::Vacant(entry) => { + entry.insert(index); + } + Entry::Occupied(mut entry) => { + if *entry.get() < index { + entry.insert(index); + } + } + } + } + } + + /// Returns whether the changeset are empty. + fn is_empty(&self) -> bool { + self.last_revealed.is_empty() && self.keychains_added.is_empty() + } +} + +impl Default for ChangeSet { + fn default() -> Self { + Self { + last_revealed: BTreeMap::default(), + keychains_added: BTreeMap::default(), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +/// The keychain doesn't exist. Most likley hasn't been inserted with [`KeychainTxOutIndex::insert_descriptor`]. +pub struct NoSuchKeychain(K); + +impl core::fmt::Display for NoSuchKeychain { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "no such keychain {:?} exists", &self.0) + } +} diff --git a/crates/chain/src/indexer/spk_txout.rs b/crates/chain/src/indexer/spk_txout.rs new file mode 100644 index 00000000..ead446a7 --- /dev/null +++ b/crates/chain/src/indexer/spk_txout.rs @@ -0,0 +1,334 @@ +//! [`SpkTxOutIndex`] is an index storing [`TxOut`]s that have a script pubkey that matches those in a list. + +use core::ops::RangeBounds; + +use crate::{ + collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap}, + Indexer, +}; +use bitcoin::{Amount, OutPoint, Script, ScriptBuf, SignedAmount, Transaction, TxOut, Txid}; + +/// An index storing [`TxOut`]s that have a script pubkey that matches those in a list. +/// +/// The basic idea is that you insert script pubkeys you care about into the index with +/// [`insert_spk`] and then when you call [`Indexer::index_tx`] or [`Indexer::index_txout`], the +/// index will look at any txouts you pass in and store and index any txouts matching one of its +/// script pubkeys. +/// +/// Each script pubkey is associated with an application-defined index script index `I`, which must be +/// [`Ord`]. Usually, this is used to associate the derivation index of the script pubkey or even a +/// combination of `(keychain, derivation_index)`. +/// +/// Note there is no harm in scanning transactions that disappear from the blockchain or were never +/// in there in the first place. `SpkTxOutIndex` is intentionally *monotone* -- you cannot delete or +/// modify txouts that have been indexed. To find out which txouts from the index are actually in the +/// chain or unspent, you must use other sources of information like a [`TxGraph`]. +/// +/// [`TxOut`]: bitcoin::TxOut +/// [`insert_spk`]: Self::insert_spk +/// [`Ord`]: core::cmp::Ord +/// [`TxGraph`]: crate::tx_graph::TxGraph +#[derive(Clone, Debug)] +pub struct SpkTxOutIndex { + /// script pubkeys ordered by index + spks: BTreeMap, + /// A reverse lookup from spk to spk index + spk_indices: HashMap, + /// The set of unused indexes. + unused: BTreeSet, + /// Lookup index and txout by outpoint. + txouts: BTreeMap, + /// Lookup from spk index to outpoints that had that spk + spk_txouts: BTreeSet<(I, OutPoint)>, +} + +impl Default for SpkTxOutIndex { + fn default() -> Self { + Self { + txouts: Default::default(), + spks: Default::default(), + spk_indices: Default::default(), + spk_txouts: Default::default(), + unused: Default::default(), + } + } +} + +impl Indexer for SpkTxOutIndex { + type ChangeSet = (); + + fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet { + self.scan_txout(outpoint, txout); + Default::default() + } + + fn index_tx(&mut self, tx: &Transaction) -> Self::ChangeSet { + self.scan(tx); + Default::default() + } + + fn initial_changeset(&self) -> Self::ChangeSet {} + + fn apply_changeset(&mut self, _changeset: Self::ChangeSet) { + // This applies nothing. + } + + fn is_tx_relevant(&self, tx: &Transaction) -> bool { + self.is_relevant(tx) + } +} + +impl SpkTxOutIndex { + /// Scans a transaction's outputs for matching script pubkeys. + /// + /// Typically, this is used in two situations: + /// + /// 1. After loading transaction data from the disk, you may scan over all the txouts to restore all + /// your txouts. + /// 2. When getting new data from the chain, you usually scan it before incorporating it into your chain state. + pub fn scan(&mut self, tx: &Transaction) -> BTreeSet { + let mut scanned_indices = BTreeSet::new(); + let txid = tx.compute_txid(); + for (i, txout) in tx.output.iter().enumerate() { + let op = OutPoint::new(txid, i as u32); + if let Some(spk_i) = self.scan_txout(op, txout) { + scanned_indices.insert(spk_i.clone()); + } + } + + scanned_indices + } + + /// Scan a single `TxOut` for a matching script pubkey and returns the index that matches the + /// script pubkey (if any). + pub fn scan_txout(&mut self, op: OutPoint, txout: &TxOut) -> Option<&I> { + let spk_i = self.spk_indices.get(&txout.script_pubkey); + if let Some(spk_i) = spk_i { + self.txouts.insert(op, (spk_i.clone(), txout.clone())); + self.spk_txouts.insert((spk_i.clone(), op)); + self.unused.remove(spk_i); + } + spk_i + } + + /// Get a reference to the set of indexed outpoints. + pub fn outpoints(&self) -> &BTreeSet<(I, OutPoint)> { + &self.spk_txouts + } + + /// Iterate over all known txouts that spend to tracked script pubkeys. + pub fn txouts( + &self, + ) -> impl DoubleEndedIterator + ExactSizeIterator { + self.txouts + .iter() + .map(|(op, (index, txout))| (index, *op, txout)) + } + + /// Finds all txouts on a transaction that has previously been scanned and indexed. + pub fn txouts_in_tx( + &self, + txid: Txid, + ) -> impl DoubleEndedIterator { + self.txouts + .range(OutPoint::new(txid, u32::MIN)..=OutPoint::new(txid, u32::MAX)) + .map(|(op, (index, txout))| (index, *op, txout)) + } + + /// Iterates over all the outputs with script pubkeys in an index range. + pub fn outputs_in_range( + &self, + range: impl RangeBounds, + ) -> impl DoubleEndedIterator { + use bitcoin::hashes::Hash; + use core::ops::Bound::*; + let min_op = OutPoint { + txid: Txid::all_zeros(), + vout: u32::MIN, + }; + let max_op = OutPoint { + txid: Txid::from_byte_array([0xff; Txid::LEN]), + vout: u32::MAX, + }; + + let start = match range.start_bound() { + Included(index) => Included((index.clone(), min_op)), + Excluded(index) => Excluded((index.clone(), max_op)), + Unbounded => Unbounded, + }; + + let end = match range.end_bound() { + Included(index) => Included((index.clone(), max_op)), + Excluded(index) => Excluded((index.clone(), min_op)), + Unbounded => Unbounded, + }; + + self.spk_txouts.range((start, end)).map(|(i, op)| (i, *op)) + } + + /// Returns the txout and script pubkey index of the `TxOut` at `OutPoint`. + /// + /// Returns `None` if the `TxOut` hasn't been scanned or if nothing matching was found there. + pub fn txout(&self, outpoint: OutPoint) -> Option<(&I, &TxOut)> { + self.txouts.get(&outpoint).map(|v| (&v.0, &v.1)) + } + + /// Returns the script that has been inserted at the `index`. + /// + /// If that index hasn't been inserted yet, it will return `None`. + pub fn spk_at_index(&self, index: &I) -> Option<&Script> { + self.spks.get(index).map(|s| s.as_script()) + } + + /// The script pubkeys that are being tracked by the index. + pub fn all_spks(&self) -> &BTreeMap { + &self.spks + } + + /// Adds a script pubkey to scan for. Returns `false` and does nothing if spk already exists in the map + /// + /// the index will look for outputs spending to this spk whenever it scans new data. + pub fn insert_spk(&mut self, index: I, spk: ScriptBuf) -> bool { + match self.spk_indices.entry(spk.clone()) { + Entry::Vacant(value) => { + value.insert(index.clone()); + self.spks.insert(index.clone(), spk); + self.unused.insert(index); + true + } + Entry::Occupied(_) => false, + } + } + + /// Iterates over all unused script pubkeys in an index range. + /// + /// Here, "unused" means that after the script pubkey was stored in the index, the index has + /// never scanned a transaction output with it. + /// + /// # Example + /// + /// ```rust + /// # use bdk_chain::SpkTxOutIndex; + /// + /// // imagine our spks are indexed like (keychain, derivation_index). + /// let txout_index = SpkTxOutIndex::<(u32, u32)>::default(); + /// let all_unused_spks = txout_index.unused_spks(..); + /// let change_index = 1; + /// let unused_change_spks = + /// txout_index.unused_spks((change_index, u32::MIN)..(change_index, u32::MAX)); + /// ``` + pub fn unused_spks(&self, range: R) -> impl DoubleEndedIterator + Clone + where + R: RangeBounds, + { + self.unused + .range(range) + .map(move |index| (index, self.spk_at_index(index).expect("must exist"))) + } + + /// Returns whether the script pubkey at `index` has been used or not. + /// + /// Here, "unused" means that after the script pubkey was stored in the index, the index has + /// never scanned a transaction output with it. + pub fn is_used(&self, index: &I) -> bool { + !self.unused.contains(index) + } + + /// Marks the script pubkey at `index` as used even though it hasn't seen an output spending to it. + /// This only affects when the `index` had already been added to `self` and was unused. + /// + /// Returns whether the `index` was initially present as `unused`. + /// + /// This is useful when you want to reserve a script pubkey for something but don't want to add + /// the transaction output using it to the index yet. Other callers will consider the `index` used + /// until you call [`unmark_used`]. + /// + /// [`unmark_used`]: Self::unmark_used + pub fn mark_used(&mut self, index: &I) -> bool { + self.unused.remove(index) + } + + /// Undoes the effect of [`mark_used`]. Returns whether the `index` is inserted back into + /// `unused`. + /// + /// Note that if `self` has scanned an output with this script pubkey then this will have no + /// effect. + /// + /// [`mark_used`]: Self::mark_used + pub fn unmark_used(&mut self, index: &I) -> bool { + // we cannot set the index as unused when it does not exist + if !self.spks.contains_key(index) { + return false; + } + // we cannot set the index as unused when txouts are indexed under it + if self.outputs_in_range(index..=index).next().is_some() { + return false; + } + self.unused.insert(index.clone()) + } + + /// Returns the index associated with the script pubkey. + pub fn index_of_spk(&self, script: &Script) -> Option<&I> { + self.spk_indices.get(script) + } + + /// Computes the total value transfer effect `tx` has on the script pubkeys in `range`. Value is + /// *sent* when a script pubkey in the `range` is on an input and *received* when it is on an + /// output. For `sent` to be computed correctly, the output being spent must have already been + /// scanned by the index. Calculating received just uses the [`Transaction`] outputs directly, + /// so it will be correct even if it has not been scanned. + pub fn sent_and_received( + &self, + tx: &Transaction, + range: impl RangeBounds, + ) -> (Amount, Amount) { + let mut sent = Amount::ZERO; + let mut received = Amount::ZERO; + + for txin in &tx.input { + if let Some((index, txout)) = self.txout(txin.previous_output) { + if range.contains(index) { + sent += txout.value; + } + } + } + for txout in &tx.output { + if let Some(index) = self.index_of_spk(&txout.script_pubkey) { + if range.contains(index) { + received += txout.value; + } + } + } + + (sent, received) + } + + /// Computes the net value transfer effect of `tx` on the script pubkeys in `range`. Shorthand + /// for calling [`sent_and_received`] and subtracting sent from received. + /// + /// [`sent_and_received`]: Self::sent_and_received + pub fn net_value(&self, tx: &Transaction, range: impl RangeBounds) -> SignedAmount { + let (sent, received) = self.sent_and_received(tx, range); + received.to_signed().expect("valid `SignedAmount`") + - sent.to_signed().expect("valid `SignedAmount`") + } + + /// Whether any of the inputs of this transaction spend a txout tracked or whether any output + /// matches one of our script pubkeys. + /// + /// It is easily possible to misuse this method and get false negatives by calling it before you + /// have scanned the `TxOut`s the transaction is spending. For example, if you want to filter out + /// all the transactions in a block that are irrelevant, you **must first scan all the + /// transactions in the block** and only then use this method. + pub fn is_relevant(&self, tx: &Transaction) -> bool { + let input_matches = tx + .input + .iter() + .any(|input| self.txouts.contains_key(&input.previous_output)); + let output_matches = tx + .output + .iter() + .any(|output| self.spk_indices.contains_key(&output.script_pubkey)); + input_matches || output_matches + } +} diff --git a/crates/chain/src/keychain.rs b/crates/chain/src/keychain.rs deleted file mode 100644 index 2a0500a4..00000000 --- a/crates/chain/src/keychain.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Module for keychain related structures. -//! -//! A keychain here is a set of application-defined indexes for a miniscript descriptor where we can -//! derive script pubkeys at a particular derivation index. The application's index is simply -//! anything that implements `Ord`. -//! -//! [`KeychainTxOutIndex`] indexes script pubkeys of keychains and scans in relevant outpoints (that -//! has a `txout` containing an indexed script pubkey). Internally, this uses [`SpkTxOutIndex`], but -//! also maintains "revealed" and "lookahead" index counts per keychain. -//! -//! [`SpkTxOutIndex`]: crate::SpkTxOutIndex - -#[cfg(feature = "miniscript")] -mod txout_index; -use bitcoin::{Amount, ScriptBuf}; -#[cfg(feature = "miniscript")] -pub use txout_index::*; - -/// Balance, differentiated into various categories. -#[derive(Debug, PartialEq, Eq, Clone, Default)] -#[cfg_attr( - feature = "serde", - derive(serde::Deserialize, serde::Serialize), - serde(crate = "serde_crate",) -)] -pub struct Balance { - /// All coinbase outputs not yet matured - pub immature: Amount, - /// Unconfirmed UTXOs generated by a wallet tx - pub trusted_pending: Amount, - /// Unconfirmed UTXOs received from an external wallet - pub untrusted_pending: Amount, - /// Confirmed and immediately spendable balance - pub confirmed: Amount, -} - -impl Balance { - /// Get sum of trusted_pending and confirmed coins. - /// - /// This is the balance you can spend right now that shouldn't get cancelled via another party - /// double spending it. - pub fn trusted_spendable(&self) -> Amount { - self.confirmed + self.trusted_pending - } - - /// Get the whole balance visible to the wallet. - pub fn total(&self) -> Amount { - self.confirmed + self.trusted_pending + self.untrusted_pending + self.immature - } -} - -/// A tuple of keychain index and `T` representing the indexed value. -pub type Indexed = (u32, T); -/// A tuple of keychain `K`, derivation index (`u32`) and a `T` associated with them. -pub type KeychainIndexed = ((K, u32), T); - -impl core::fmt::Display for Balance { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!( - f, - "{{ immature: {}, trusted_pending: {}, untrusted_pending: {}, confirmed: {} }}", - self.immature, self.trusted_pending, self.untrusted_pending, self.confirmed - ) - } -} - -impl core::ops::Add for Balance { - type Output = Self; - - fn add(self, other: Self) -> Self { - Self { - immature: self.immature + other.immature, - trusted_pending: self.trusted_pending + other.trusted_pending, - untrusted_pending: self.untrusted_pending + other.untrusted_pending, - confirmed: self.confirmed + other.confirmed, - } - } -} diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs deleted file mode 100644 index 758f16cb..00000000 --- a/crates/chain/src/keychain/txout_index.rs +++ /dev/null @@ -1,946 +0,0 @@ -use crate::{ - collections::*, - indexed_tx_graph::Indexer, - miniscript::{Descriptor, DescriptorPublicKey}, - spk_iter::BIP32_MAX_INDEX, - DescriptorExt, DescriptorId, SpkIterator, SpkTxOutIndex, -}; -use alloc::{borrow::ToOwned, vec::Vec}; -use bitcoin::{Amount, OutPoint, Script, SignedAmount, Transaction, TxOut, Txid}; -use core::{ - fmt::Debug, - ops::{Bound, RangeBounds}, -}; - -use super::*; -use crate::Append; - -/// The default lookahead for a [`KeychainTxOutIndex`] -pub const DEFAULT_LOOKAHEAD: u32 = 25; - -/// [`KeychainTxOutIndex`] controls how script pubkeys are revealed for multiple keychains, and -/// indexes [`TxOut`]s with them. -/// -/// A single keychain is a chain of script pubkeys derived from a single [`Descriptor`]. Keychains -/// are identified using the `K` generic. Script pubkeys are identified by the keychain that they -/// are derived from `K`, as well as the derivation index `u32`. -/// -/// There is a strict 1-to-1 relationship between descriptors and keychains. Each keychain has one -/// and only one descriptor and each descriptor has one and only one keychain. The -/// [`insert_descriptor`] method will return an error if you try and violate this invariant. This -/// rule is a proxy for a stronger rule: no two descriptors should produce the same script pubkey. -/// Having two descriptors produce the same script pubkey should cause whichever keychain derives -/// the script pubkey first to be the effective owner of it but you should not rely on this -/// behaviour. ⚠ It is up you, the developer, not to violate this invariant. -/// -/// # Revealed script pubkeys -/// -/// Tracking how script pubkeys are revealed is useful for collecting chain data. For example, if -/// the user has requested 5 script pubkeys (to receive money with), we only need to use those -/// script pubkeys to scan for chain data. -/// -/// Call [`reveal_to_target`] or [`reveal_next_spk`] to reveal more script pubkeys. -/// Call [`revealed_keychain_spks`] or [`revealed_spks`] to iterate through revealed script pubkeys. -/// -/// # Lookahead script pubkeys -/// -/// When an user first recovers a wallet (i.e. from a recovery phrase and/or descriptor), we will -/// NOT have knowledge of which script pubkeys are revealed. So when we index a transaction or -/// txout (using [`index_tx`]/[`index_txout`]) we scan the txouts against script pubkeys derived -/// above the last revealed index. These additionally-derived script pubkeys are called the -/// lookahead. -/// -/// The [`KeychainTxOutIndex`] is constructed with the `lookahead` and cannot be altered. See -/// [`DEFAULT_LOOKAHEAD`] for the value used in the `Default` implementation. Use [`new`] to set a -/// custom `lookahead`. -/// -/// # Unbounded script pubkey iterator -/// -/// For script-pubkey-based chain sources (such as Electrum/Esplora), an initial scan is best done -/// by iterating though derived script pubkeys one by one and requesting transaction histories for -/// each script pubkey. We will stop after x-number of script pubkeys have empty histories. An -/// unbounded script pubkey iterator is useful to pass to such a chain source because it doesn't -/// require holding a reference to the index. -/// -/// Call [`unbounded_spk_iter`] to get an unbounded script pubkey iterator for a given keychain. -/// Call [`all_unbounded_spk_iters`] to get unbounded script pubkey iterators for all keychains. -/// -/// # Change sets -/// -/// Methods that can update the last revealed index or add keychains will return [`ChangeSet`] to report -/// these changes. This should be persisted for future recovery. -/// -/// ## Synopsis -/// -/// ``` -/// use bdk_chain::keychain::KeychainTxOutIndex; -/// # use bdk_chain::{ miniscript::{Descriptor, DescriptorPublicKey} }; -/// # use core::str::FromStr; -/// -/// // imagine our service has internal and external addresses but also addresses for users -/// #[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)] -/// enum MyKeychain { -/// External, -/// Internal, -/// MyAppUser { -/// user_id: u32 -/// } -/// } -/// -/// let mut txout_index = KeychainTxOutIndex::::default(); -/// -/// # let secp = bdk_chain::bitcoin::secp256k1::Secp256k1::signing_only(); -/// # let (external_descriptor,_) = Descriptor::::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap(); -/// # let (internal_descriptor,_) = Descriptor::::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)").unwrap(); -/// # let (descriptor_42, _) = Descriptor::::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/2/*)").unwrap(); -/// let _ = txout_index.insert_descriptor(MyKeychain::External, external_descriptor)?; -/// let _ = txout_index.insert_descriptor(MyKeychain::Internal, internal_descriptor)?; -/// let _ = txout_index.insert_descriptor(MyKeychain::MyAppUser { user_id: 42 }, descriptor_42)?; -/// -/// let new_spk_for_user = txout_index.reveal_next_spk(&MyKeychain::MyAppUser{ user_id: 42 }); -/// # Ok::<_, bdk_chain::keychain::InsertDescriptorError<_>>(()) -/// ``` -/// -/// [`Ord`]: core::cmp::Ord -/// [`SpkTxOutIndex`]: crate::spk_txout_index::SpkTxOutIndex -/// [`Descriptor`]: crate::miniscript::Descriptor -/// [`reveal_to_target`]: Self::reveal_to_target -/// [`reveal_next_spk`]: Self::reveal_next_spk -/// [`revealed_keychain_spks`]: Self::revealed_keychain_spks -/// [`revealed_spks`]: Self::revealed_spks -/// [`index_tx`]: Self::index_tx -/// [`index_txout`]: Self::index_txout -/// [`new`]: Self::new -/// [`unbounded_spk_iter`]: Self::unbounded_spk_iter -/// [`all_unbounded_spk_iters`]: Self::all_unbounded_spk_iters -/// [`outpoints`]: Self::outpoints -/// [`txouts`]: Self::txouts -/// [`unused_spks`]: Self::unused_spks -/// [`insert_descriptor`]: Self::insert_descriptor -#[derive(Clone, Debug)] -pub struct KeychainTxOutIndex { - inner: SpkTxOutIndex<(K, u32)>, - keychain_to_descriptor_id: BTreeMap, - descriptor_id_to_keychain: HashMap, - descriptors: HashMap>, - last_revealed: HashMap, - lookahead: u32, -} - -impl Default for KeychainTxOutIndex { - fn default() -> Self { - Self::new(DEFAULT_LOOKAHEAD) - } -} - -impl Indexer for KeychainTxOutIndex { - type ChangeSet = ChangeSet; - - fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet { - let mut changeset = ChangeSet::default(); - if let Some((keychain, index)) = self.inner.scan_txout(outpoint, txout).cloned() { - let did = self - .keychain_to_descriptor_id - .get(&keychain) - .expect("invariant"); - if self.last_revealed.get(did) < Some(&index) { - self.last_revealed.insert(*did, index); - changeset.last_revealed.insert(*did, index); - self.replenish_inner_index(*did, &keychain, self.lookahead); - } - } - changeset - } - - fn index_tx(&mut self, tx: &bitcoin::Transaction) -> Self::ChangeSet { - let mut changeset = ChangeSet::::default(); - let txid = tx.compute_txid(); - for (op, txout) in tx.output.iter().enumerate() { - changeset.append(self.index_txout(OutPoint::new(txid, op as u32), txout)); - } - changeset - } - - fn initial_changeset(&self) -> Self::ChangeSet { - ChangeSet { - keychains_added: self - .keychains() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(), - last_revealed: self.last_revealed.clone().into_iter().collect(), - } - } - - fn apply_changeset(&mut self, changeset: Self::ChangeSet) { - self.apply_changeset(changeset) - } - - fn is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool { - self.inner.is_relevant(tx) - } -} - -impl KeychainTxOutIndex { - /// Construct a [`KeychainTxOutIndex`] with the given `lookahead`. - /// - /// The `lookahead` is the number of script pubkeys to derive and cache from the internal - /// descriptors over and above the last revealed script index. Without a lookahead the index - /// will miss outputs you own when processing transactions whose output script pubkeys lie - /// beyond the last revealed index. In certain situations, such as when performing an initial - /// scan of the blockchain during wallet import, it may be uncertain or unknown what the index - /// of the last revealed script pubkey actually is. - /// - /// Refer to [struct-level docs](KeychainTxOutIndex) for more about `lookahead`. - pub fn new(lookahead: u32) -> Self { - Self { - inner: SpkTxOutIndex::default(), - keychain_to_descriptor_id: Default::default(), - descriptors: Default::default(), - descriptor_id_to_keychain: Default::default(), - last_revealed: Default::default(), - lookahead, - } - } -} - -/// Methods that are *re-exposed* from the internal [`SpkTxOutIndex`]. -impl KeychainTxOutIndex { - /// Return a reference to the internal [`SpkTxOutIndex`]. - /// - /// **WARNING**: The internal index will contain lookahead spks. Refer to - /// [struct-level docs](KeychainTxOutIndex) for more about `lookahead`. - pub fn inner(&self) -> &SpkTxOutIndex<(K, u32)> { - &self.inner - } - - /// Get the set of indexed outpoints, corresponding to tracked keychains. - pub fn outpoints(&self) -> &BTreeSet> { - self.inner.outpoints() - } - - /// Iterate over known txouts that spend to tracked script pubkeys. - pub fn txouts( - &self, - ) -> impl DoubleEndedIterator> + ExactSizeIterator - { - self.inner - .txouts() - .map(|(index, op, txout)| (index.clone(), (op, txout))) - } - - /// Finds all txouts on a transaction that has previously been scanned and indexed. - pub fn txouts_in_tx( - &self, - txid: Txid, - ) -> impl DoubleEndedIterator> { - self.inner - .txouts_in_tx(txid) - .map(|(index, op, txout)| (index.clone(), (op, txout))) - } - - /// Return the [`TxOut`] of `outpoint` if it has been indexed, and if it corresponds to a - /// tracked keychain. - /// - /// The associated keychain and keychain index of the txout's spk is also returned. - /// - /// This calls [`SpkTxOutIndex::txout`] internally. - pub fn txout(&self, outpoint: OutPoint) -> Option> { - self.inner - .txout(outpoint) - .map(|(index, txout)| (index.clone(), txout)) - } - - /// Return the script that exists under the given `keychain`'s `index`. - /// - /// This calls [`SpkTxOutIndex::spk_at_index`] internally. - pub fn spk_at_index(&self, keychain: K, index: u32) -> Option<&Script> { - self.inner.spk_at_index(&(keychain.clone(), index)) - } - - /// Returns the keychain and keychain index associated with the spk. - /// - /// This calls [`SpkTxOutIndex::index_of_spk`] internally. - pub fn index_of_spk(&self, script: &Script) -> Option<&(K, u32)> { - self.inner.index_of_spk(script) - } - - /// Returns whether the spk under the `keychain`'s `index` has been used. - /// - /// Here, "unused" means that after the script pubkey was stored in the index, the index has - /// never scanned a transaction output with it. - /// - /// This calls [`SpkTxOutIndex::is_used`] internally. - pub fn is_used(&self, keychain: K, index: u32) -> bool { - self.inner.is_used(&(keychain, index)) - } - - /// Marks the script pubkey at `index` as used even though the tracker hasn't seen an output - /// with it. - /// - /// This only has an effect when the `index` had been added to `self` already and was unused. - /// - /// Returns whether the spk under the given `keychain` and `index` is successfully - /// marked as used. Returns false either when there is no descriptor under the given - /// keychain, or when the spk is already marked as used. - /// - /// This is useful when you want to reserve a script pubkey for something but don't want to add - /// the transaction output using it to the index yet. Other callers will consider `index` on - /// `keychain` used until you call [`unmark_used`]. - /// - /// This calls [`SpkTxOutIndex::mark_used`] internally. - /// - /// [`unmark_used`]: Self::unmark_used - pub fn mark_used(&mut self, keychain: K, index: u32) -> bool { - self.inner.mark_used(&(keychain, index)) - } - - /// Undoes the effect of [`mark_used`]. Returns whether the `index` is inserted back into - /// `unused`. - /// - /// Note that if `self` has scanned an output with this script pubkey, then this will have no - /// effect. - /// - /// This calls [`SpkTxOutIndex::unmark_used`] internally. - /// - /// [`mark_used`]: Self::mark_used - pub fn unmark_used(&mut self, keychain: K, index: u32) -> bool { - self.inner.unmark_used(&(keychain, index)) - } - - /// Computes the total value transfer effect `tx` has on the script pubkeys belonging to the - /// keychains in `range`. Value is *sent* when a script pubkey in the `range` is on an input and - /// *received* when it is on an output. For `sent` to be computed correctly, the output being - /// spent must have already been scanned by the index. Calculating received just uses the - /// [`Transaction`] outputs directly, so it will be correct even if it has not been scanned. - pub fn sent_and_received( - &self, - tx: &Transaction, - range: impl RangeBounds, - ) -> (Amount, Amount) { - self.inner - .sent_and_received(tx, self.map_to_inner_bounds(range)) - } - - /// Computes the net value that this transaction gives to the script pubkeys in the index and - /// *takes* from the transaction outputs in the index. Shorthand for calling - /// [`sent_and_received`] and subtracting sent from received. - /// - /// This calls [`SpkTxOutIndex::net_value`] internally. - /// - /// [`sent_and_received`]: Self::sent_and_received - pub fn net_value(&self, tx: &Transaction, range: impl RangeBounds) -> SignedAmount { - self.inner.net_value(tx, self.map_to_inner_bounds(range)) - } -} - -impl KeychainTxOutIndex { - /// Return all keychains and their corresponding descriptors. - pub fn keychains( - &self, - ) -> impl DoubleEndedIterator)> + ExactSizeIterator + '_ - { - self.keychain_to_descriptor_id - .iter() - .map(|(k, did)| (k, self.descriptors.get(did).expect("invariant"))) - } - - /// Insert a descriptor with a keychain associated to it. - /// - /// Adding a descriptor means you will be able to derive new script pubkeys under it and the - /// txout index will discover transaction outputs with those script pubkeys (once they've been - /// derived and added to the index). - /// - /// keychain <-> descriptor is a one-to-one mapping that cannot be changed. Attempting to do so - /// will return a [`InsertDescriptorError`]. - /// - /// `[KeychainTxOutIndex]` will prevent you from inserting two descriptors which derive the same - /// script pubkey at index 0, but it's up to you to ensure that descriptors don't collide at - /// other indices. If they do nothing catastrophic happens at the `KeychainTxOutIndex` level - /// (one keychain just becomes the defacto owner of that spk arbitrarily) but this may have - /// subtle implications up the application stack like one UTXO being missing from one keychain - /// because it has been assigned to another which produces the same script pubkey. - pub fn insert_descriptor( - &mut self, - keychain: K, - descriptor: Descriptor, - ) -> Result, InsertDescriptorError> { - let mut changeset = ChangeSet::::default(); - let did = descriptor.descriptor_id(); - if !self.keychain_to_descriptor_id.contains_key(&keychain) - && !self.descriptor_id_to_keychain.contains_key(&did) - { - self.descriptors.insert(did, descriptor.clone()); - self.keychain_to_descriptor_id.insert(keychain.clone(), did); - self.descriptor_id_to_keychain.insert(did, keychain.clone()); - self.replenish_inner_index(did, &keychain, self.lookahead); - changeset - .keychains_added - .insert(keychain.clone(), descriptor); - } else { - if let Some(existing_desc_id) = self.keychain_to_descriptor_id.get(&keychain) { - let descriptor = self.descriptors.get(existing_desc_id).expect("invariant"); - if *existing_desc_id != did { - return Err(InsertDescriptorError::KeychainAlreadyAssigned { - existing_assignment: descriptor.clone(), - keychain, - }); - } - } - - if let Some(existing_keychain) = self.descriptor_id_to_keychain.get(&did) { - let descriptor = self.descriptors.get(&did).expect("invariant").clone(); - - if *existing_keychain != keychain { - return Err(InsertDescriptorError::DescriptorAlreadyAssigned { - existing_assignment: existing_keychain.clone(), - descriptor, - }); - } - } - } - - Ok(changeset) - } - - /// Gets the descriptor associated with the keychain. Returns `None` if the keychain doesn't - /// have a descriptor associated with it. - pub fn get_descriptor(&self, keychain: &K) -> Option<&Descriptor> { - let did = self.keychain_to_descriptor_id.get(keychain)?; - self.descriptors.get(did) - } - - /// Get the lookahead setting. - /// - /// Refer to [`new`] for more information on the `lookahead`. - /// - /// [`new`]: Self::new - pub fn lookahead(&self) -> u32 { - self.lookahead - } - - /// Store lookahead scripts until `target_index` (inclusive). - /// - /// This does not change the global `lookahead` setting. - pub fn lookahead_to_target(&mut self, keychain: &K, target_index: u32) { - if let Some((next_index, _)) = self.next_index(keychain) { - let temp_lookahead = (target_index + 1) - .checked_sub(next_index) - .filter(|&index| index > 0); - - if let Some(temp_lookahead) = temp_lookahead { - self.replenish_inner_index_keychain(keychain, temp_lookahead); - } - } - } - - fn replenish_inner_index_did(&mut self, did: DescriptorId, lookahead: u32) { - if let Some(keychain) = self.descriptor_id_to_keychain.get(&did).cloned() { - self.replenish_inner_index(did, &keychain, lookahead); - } - } - - fn replenish_inner_index_keychain(&mut self, keychain: &K, lookahead: u32) { - if let Some(did) = self.keychain_to_descriptor_id.get(keychain) { - self.replenish_inner_index(*did, keychain, lookahead); - } - } - - /// Syncs the state of the inner spk index after changes to a keychain - fn replenish_inner_index(&mut self, did: DescriptorId, keychain: &K, lookahead: u32) { - let descriptor = self.descriptors.get(&did).expect("invariant"); - let next_store_index = self - .inner - .all_spks() - .range(&(keychain.clone(), u32::MIN)..=&(keychain.clone(), u32::MAX)) - .last() - .map_or(0, |((_, index), _)| *index + 1); - let next_reveal_index = self.last_revealed.get(&did).map_or(0, |v| *v + 1); - for (new_index, new_spk) in - SpkIterator::new_with_range(descriptor, next_store_index..next_reveal_index + lookahead) - { - let _inserted = self - .inner - .insert_spk((keychain.clone(), new_index), new_spk); - debug_assert!(_inserted, "replenish lookahead: must not have existing spk: keychain={:?}, lookahead={}, next_store_index={}, next_reveal_index={}", keychain, lookahead, next_store_index, next_reveal_index); - } - } - - /// Get an unbounded spk iterator over a given `keychain`. Returns `None` if the provided - /// keychain doesn't exist - pub fn unbounded_spk_iter( - &self, - keychain: &K, - ) -> Option>> { - let descriptor = self.get_descriptor(keychain)?.clone(); - Some(SpkIterator::new(descriptor)) - } - - /// Get unbounded spk iterators for all keychains. - pub fn all_unbounded_spk_iters( - &self, - ) -> BTreeMap>> { - self.keychain_to_descriptor_id - .iter() - .map(|(k, did)| { - ( - k.clone(), - SpkIterator::new(self.descriptors.get(did).expect("invariant").clone()), - ) - }) - .collect() - } - - /// Iterate over revealed spks of keychains in `range` - pub fn revealed_spks( - &self, - range: impl RangeBounds, - ) -> impl Iterator> { - let start = range.start_bound(); - let end = range.end_bound(); - let mut iter_last_revealed = self - .keychain_to_descriptor_id - .range((start, end)) - .map(|(k, did)| (k, self.last_revealed.get(did).cloned())); - let mut iter_spks = self - .inner - .all_spks() - .range(self.map_to_inner_bounds((start, end))); - let mut current_keychain = iter_last_revealed.next(); - // The reason we need a tricky algorithm is because of the "lookahead" feature which means - // that some of the spks in the SpkTxoutIndex will not have been revealed yet. So we need to - // filter out those spks that are above the last_revealed for that keychain. To do this we - // iterate through the last_revealed for each keychain and the spks for each keychain in - // tandem. This minimizes BTreeMap queries. - core::iter::from_fn(move || loop { - let ((keychain, index), spk) = iter_spks.next()?; - // We need to find the last revealed that matches the current spk we are considering so - // we skip ahead. - while current_keychain?.0 < keychain { - current_keychain = iter_last_revealed.next(); - } - let (current_keychain, last_revealed) = current_keychain?; - - if current_keychain == keychain && Some(*index) <= last_revealed { - break Some(((keychain.clone(), *index), spk.as_script())); - } - }) - } - - /// Iterate over revealed spks of the given `keychain` with ascending indices. - /// - /// This is a double ended iterator so you can easily reverse it to get an iterator where - /// the script pubkeys that were most recently revealed are first. - pub fn revealed_keychain_spks<'a>( - &'a self, - keychain: &'a K, - ) -> impl DoubleEndedIterator> + 'a { - let end = self - .last_revealed_index(keychain) - .map(|v| v + 1) - .unwrap_or(0); - self.inner - .all_spks() - .range((keychain.clone(), 0)..(keychain.clone(), end)) - .map(|((_, index), spk)| (*index, spk.as_script())) - } - - /// Iterate over revealed, but unused, spks of all keychains. - pub fn unused_spks( - &self, - ) -> impl DoubleEndedIterator> + Clone { - self.keychain_to_descriptor_id.keys().flat_map(|keychain| { - self.unused_keychain_spks(keychain) - .map(|(i, spk)| ((keychain.clone(), i), spk)) - }) - } - - /// Iterate over revealed, but unused, spks of the given `keychain`. - /// Returns an empty iterator if the provided keychain doesn't exist. - pub fn unused_keychain_spks( - &self, - keychain: &K, - ) -> impl DoubleEndedIterator> + Clone { - let end = match self.keychain_to_descriptor_id.get(keychain) { - Some(did) => self.last_revealed.get(did).map(|v| *v + 1).unwrap_or(0), - None => 0, - }; - - self.inner - .unused_spks((keychain.clone(), 0)..(keychain.clone(), end)) - .map(|((_, i), spk)| (*i, spk)) - } - - /// Get the next derivation index for `keychain`. The next index is the index after the last revealed - /// derivation index. - /// - /// The second field in the returned tuple represents whether the next derivation index is new. - /// There are two scenarios where the next derivation index is reused (not new): - /// - /// 1. The keychain's descriptor has no wildcard, and a script has already been revealed. - /// 2. The number of revealed scripts has already reached 2^31 (refer to BIP-32). - /// - /// Not checking the second field of the tuple may result in address reuse. - /// - /// Returns None if the provided `keychain` doesn't exist. - pub fn next_index(&self, keychain: &K) -> Option<(u32, bool)> { - let did = self.keychain_to_descriptor_id.get(keychain)?; - let last_index = self.last_revealed.get(did).cloned(); - let descriptor = self.descriptors.get(did).expect("invariant"); - - // we can only get the next index if the wildcard exists. - let has_wildcard = descriptor.has_wildcard(); - - Some(match last_index { - // if there is no index, next_index is always 0. - None => (0, true), - // descriptors without wildcards can only have one index. - Some(_) if !has_wildcard => (0, false), - // derivation index must be < 2^31 (BIP-32). - Some(index) if index > BIP32_MAX_INDEX => { - unreachable!("index is out of bounds") - } - Some(index) if index == BIP32_MAX_INDEX => (index, false), - // get the next derivation index. - Some(index) => (index + 1, true), - }) - } - - /// Get the last derivation index that is revealed for each keychain. - /// - /// Keychains with no revealed indices will not be included in the returned [`BTreeMap`]. - pub fn last_revealed_indices(&self) -> BTreeMap { - self.last_revealed - .iter() - .filter_map(|(desc_id, index)| { - let keychain = self.descriptor_id_to_keychain.get(desc_id)?; - Some((keychain.clone(), *index)) - }) - .collect() - } - - /// Get the last derivation index revealed for `keychain`. Returns None if the keychain doesn't - /// exist, or if the keychain doesn't have any revealed scripts. - pub fn last_revealed_index(&self, keychain: &K) -> Option { - let descriptor_id = self.keychain_to_descriptor_id.get(keychain)?; - self.last_revealed.get(descriptor_id).cloned() - } - - /// Convenience method to call [`Self::reveal_to_target`] on multiple keychains. - pub fn reveal_to_target_multi(&mut self, keychains: &BTreeMap) -> ChangeSet { - let mut changeset = ChangeSet::default(); - - for (keychain, &index) in keychains { - if let Some((_, new_changeset)) = self.reveal_to_target(keychain, index) { - changeset.append(new_changeset); - } - } - - changeset - } - - /// Reveals script pubkeys of the `keychain`'s descriptor **up to and including** the - /// `target_index`. - /// - /// If the `target_index` cannot be reached (due to the descriptor having no wildcard and/or - /// the `target_index` is in the hardened index range), this method will make a best-effort and - /// reveal up to the last possible index. - /// - /// This returns list of newly revealed indices (alongside their scripts) and a - /// [`ChangeSet`], which reports updates to the latest revealed index. If no new script - /// pubkeys are revealed, then both of these will be empty. - /// - /// Returns None if the provided `keychain` doesn't exist. - #[must_use] - pub fn reveal_to_target( - &mut self, - keychain: &K, - target_index: u32, - ) -> Option<(Vec>, ChangeSet)> { - let mut changeset = ChangeSet::default(); - let mut spks: Vec> = vec![]; - while let Some((i, new)) = self.next_index(keychain) { - if !new || i > target_index { - break; - } - match self.reveal_next_spk(keychain) { - Some(((i, spk), change)) => { - spks.push((i, spk)); - changeset.append(change); - } - None => break, - } - } - - Some((spks, changeset)) - } - - /// Attempts to reveal the next script pubkey for `keychain`. - /// - /// Returns the derivation index of the revealed script pubkey, the revealed script pubkey and a - /// [`ChangeSet`] which represents changes in the last revealed index (if any). - /// Returns None if the provided keychain doesn't exist. - /// - /// When a new script cannot be revealed, we return the last revealed script and an empty - /// [`ChangeSet`]. There are two scenarios when a new script pubkey cannot be derived: - /// - /// 1. The descriptor has no wildcard and already has one script revealed. - /// 2. The descriptor has already revealed scripts up to the numeric bound. - /// 3. There is no descriptor associated with the given keychain. - pub fn reveal_next_spk(&mut self, keychain: &K) -> Option<(Indexed, ChangeSet)> { - let (next_index, new) = self.next_index(keychain)?; - let mut changeset = ChangeSet::default(); - - if new { - let did = self.keychain_to_descriptor_id.get(keychain)?; - self.last_revealed.insert(*did, next_index); - changeset.last_revealed.insert(*did, next_index); - self.replenish_inner_index(*did, keychain, self.lookahead); - } - let script = self - .inner - .spk_at_index(&(keychain.clone(), next_index)) - .expect("we just inserted it"); - Some(((next_index, script.into()), changeset)) - } - - /// Gets the next unused script pubkey in the keychain. I.e., the script pubkey with the lowest - /// index that has not been used yet. - /// - /// This will derive and reveal a new script pubkey if no more unused script pubkeys exist. - /// - /// If the descriptor has no wildcard and already has a used script pubkey or if a descriptor - /// has used all scripts up to the derivation bounds, then the last derived script pubkey will be - /// returned. - /// - /// Returns `None` if there are no script pubkeys that have been used and no new script pubkey - /// could be revealed (see [`reveal_next_spk`] for when this happens). - /// - /// [`reveal_next_spk`]: Self::reveal_next_spk - pub fn next_unused_spk(&mut self, keychain: &K) -> Option<(Indexed, ChangeSet)> { - let next_unused = self - .unused_keychain_spks(keychain) - .next() - .map(|(i, spk)| ((i, spk.to_owned()), ChangeSet::default())); - - next_unused.or_else(|| self.reveal_next_spk(keychain)) - } - - /// Iterate over all [`OutPoint`]s that have `TxOut`s with script pubkeys derived from - /// `keychain`. - pub fn keychain_outpoints<'a>( - &'a self, - keychain: &'a K, - ) -> impl DoubleEndedIterator> + 'a { - self.keychain_outpoints_in_range(keychain..=keychain) - .map(|((_, i), op)| (i, op)) - } - - /// Iterate over [`OutPoint`]s that have script pubkeys derived from keychains in `range`. - pub fn keychain_outpoints_in_range<'a>( - &'a self, - range: impl RangeBounds + 'a, - ) -> impl DoubleEndedIterator> + 'a { - self.inner - .outputs_in_range(self.map_to_inner_bounds(range)) - .map(|((k, i), op)| ((k.clone(), *i), op)) - } - - fn map_to_inner_bounds(&self, bound: impl RangeBounds) -> impl RangeBounds<(K, u32)> { - let start = match bound.start_bound() { - Bound::Included(keychain) => Bound::Included((keychain.clone(), u32::MIN)), - Bound::Excluded(keychain) => Bound::Excluded((keychain.clone(), u32::MAX)), - Bound::Unbounded => Bound::Unbounded, - }; - let end = match bound.end_bound() { - Bound::Included(keychain) => Bound::Included((keychain.clone(), u32::MAX)), - Bound::Excluded(keychain) => Bound::Excluded((keychain.clone(), u32::MIN)), - Bound::Unbounded => Bound::Unbounded, - }; - - (start, end) - } - - /// Returns the highest derivation index of the `keychain` where [`KeychainTxOutIndex`] has - /// found a [`TxOut`] with it's script pubkey. - pub fn last_used_index(&self, keychain: &K) -> Option { - self.keychain_outpoints(keychain).last().map(|(i, _)| i) - } - - /// Returns the highest derivation index of each keychain that [`KeychainTxOutIndex`] has found - /// a [`TxOut`] with it's script pubkey. - pub fn last_used_indices(&self) -> BTreeMap { - self.keychain_to_descriptor_id - .iter() - .filter_map(|(keychain, _)| { - self.last_used_index(keychain) - .map(|index| (keychain.clone(), index)) - }) - .collect() - } - - /// Applies the `ChangeSet` to the [`KeychainTxOutIndex`] - /// - /// Keychains added by the `keychains_added` field of `ChangeSet` respect the one-to-one - /// keychain <-> descriptor invariant by silently ignoring attempts to violate it (but will - /// panic if `debug_assertions` are enabled). - pub fn apply_changeset(&mut self, changeset: ChangeSet) { - let ChangeSet { - keychains_added, - last_revealed, - } = changeset; - for (keychain, descriptor) in keychains_added { - let _ignore_invariant_violation = self.insert_descriptor(keychain, descriptor); - } - - for (&desc_id, &index) in &last_revealed { - let v = self.last_revealed.entry(desc_id).or_default(); - *v = index.max(*v); - } - - for did in last_revealed.keys() { - self.replenish_inner_index_did(*did, self.lookahead); - } - } -} - -#[derive(Clone, Debug, PartialEq)] -/// Error returned from [`KeychainTxOutIndex::insert_descriptor`] -pub enum InsertDescriptorError { - /// The descriptor has already been assigned to a keychain so you can't assign it to another - DescriptorAlreadyAssigned { - /// The descriptor you have attempted to reassign - descriptor: Descriptor, - /// The keychain that the descriptor is already assigned to - existing_assignment: K, - }, - /// The keychain is already assigned to a descriptor so you can't reassign it - KeychainAlreadyAssigned { - /// The keychain that you have attempted to reassign - keychain: K, - /// The descriptor that the keychain is already assigned to - existing_assignment: Descriptor, - }, -} - -impl core::fmt::Display for InsertDescriptorError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - InsertDescriptorError::DescriptorAlreadyAssigned { - existing_assignment: existing, - descriptor, - } => { - write!( - f, - "attempt to re-assign descriptor {descriptor:?} already assigned to {existing:?}" - ) - } - InsertDescriptorError::KeychainAlreadyAssigned { - existing_assignment: existing, - keychain, - } => { - write!( - f, - "attempt to re-assign keychain {keychain:?} already assigned to {existing:?}" - ) - } - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for InsertDescriptorError {} - -/// Represents updates to the derivation index of a [`KeychainTxOutIndex`]. -/// It maps each keychain `K` to a descriptor and its last revealed index. -/// -/// It can be applied to [`KeychainTxOutIndex`] with [`apply_changeset`]. -/// -/// The `last_revealed` field is monotone in that [`append`] will never decrease it. -/// `keychains_added` is *not* monotone, once it is set any attempt to change it is subject to the -/// same *one-to-one* keychain <-> descriptor mapping invariant as [`KeychainTxOutIndex`] itself. -/// -/// [`KeychainTxOutIndex`]: crate::keychain::KeychainTxOutIndex -/// [`apply_changeset`]: crate::keychain::KeychainTxOutIndex::apply_changeset -/// [`append`]: Self::append -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr( - feature = "serde", - derive(serde::Deserialize, serde::Serialize), - serde( - crate = "serde_crate", - bound( - deserialize = "K: Ord + serde::Deserialize<'de>", - serialize = "K: Ord + serde::Serialize" - ) - ) -)] -#[must_use] -pub struct ChangeSet { - /// Contains the keychains that have been added and their respective descriptor - pub keychains_added: BTreeMap>, - /// Contains for each descriptor_id the last revealed index of derivation - pub last_revealed: BTreeMap, -} - -impl Append for ChangeSet { - /// Merge another [`ChangeSet`] into self. - /// - /// For the `keychains_added` field this method respects the invariants of - /// [`insert_descriptor`]. `last_revealed` always becomes the larger of the two. - /// - /// [`insert_descriptor`]: KeychainTxOutIndex::insert_descriptor - fn append(&mut self, other: Self) { - for (new_keychain, new_descriptor) in other.keychains_added { - // enforce 1-to-1 invariance - if !self.keychains_added.contains_key(&new_keychain) - // FIXME: very inefficient - && self - .keychains_added - .values() - .all(|descriptor| descriptor != &new_descriptor) - { - self.keychains_added.insert(new_keychain, new_descriptor); - } - } - - // for `last_revealed`, entries of `other` will take precedence ONLY if it is greater than - // what was originally in `self`. - for (desc_id, index) in other.last_revealed { - use crate::collections::btree_map::Entry; - match self.last_revealed.entry(desc_id) { - Entry::Vacant(entry) => { - entry.insert(index); - } - Entry::Occupied(mut entry) => { - if *entry.get() < index { - entry.insert(index); - } - } - } - } - } - - /// Returns whether the changeset are empty. - fn is_empty(&self) -> bool { - self.last_revealed.is_empty() && self.keychains_added.is_empty() - } -} - -impl Default for ChangeSet { - fn default() -> Self { - Self { - last_revealed: BTreeMap::default(), - keychains_added: BTreeMap::default(), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -/// The keychain doesn't exist. Most likley hasn't been inserted with [`KeychainTxOutIndex::insert_descriptor`]. -pub struct NoSuchKeychain(K); - -impl core::fmt::Display for NoSuchKeychain { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "no such keychain {:?} exists", &self.0) - } -} diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 94f81441..ec0c61a3 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -21,14 +21,15 @@ #![warn(missing_docs)] pub use bitcoin; -mod spk_txout_index; -pub use spk_txout_index::*; +mod balance; +pub use balance::*; mod chain_data; pub use chain_data::*; pub mod indexed_tx_graph; pub use indexed_tx_graph::IndexedTxGraph; -pub mod keychain; -pub use keychain::{Indexed, KeychainIndexed}; +pub mod indexer; +pub use indexer::spk_txout::*; +pub use indexer::Indexer; pub mod local_chain; mod tx_data_traits; pub mod tx_graph; @@ -98,3 +99,8 @@ pub mod collections { /// How many confirmations are needed f or a coinbase output to be spent. pub const COINBASE_MATURITY: u32 = 100; + +/// A tuple of keychain index and `T` representing the indexed value. +pub type Indexed = (u32, T); +/// A tuple of keychain `K`, derivation index (`u32`) and a `T` associated with them. +pub type KeychainIndexed = ((K, u32), T); diff --git a/crates/chain/src/spk_client.rs b/crates/chain/src/spk_client.rs index 24bd9b4a..1ddf7a6d 100644 --- a/crates/chain/src/spk_client.rs +++ b/crates/chain/src/spk_client.rs @@ -1,8 +1,7 @@ //! Helper types for spk-based blockchain clients. use crate::{ - collections::BTreeMap, keychain::Indexed, local_chain::CheckPoint, - ConfirmationTimeHeightAnchor, TxGraph, + collections::BTreeMap, local_chain::CheckPoint, ConfirmationTimeHeightAnchor, Indexed, TxGraph, }; use alloc::boxed::Box; use bitcoin::{OutPoint, Script, ScriptBuf, Txid}; @@ -160,7 +159,7 @@ impl SyncRequest { #[must_use] pub fn populate_with_revealed_spks( self, - index: &crate::keychain::KeychainTxOutIndex, + index: &crate::indexer::keychain_txout::KeychainTxOutIndex, spk_range: impl core::ops::RangeBounds, ) -> Self { use alloc::borrow::ToOwned; @@ -216,12 +215,12 @@ impl FullScanRequest { /// [`KeychainTxOutIndex::all_unbounded_spk_iters`] and is used to populate the /// [`FullScanRequest`]. /// - /// [`KeychainTxOutIndex::all_unbounded_spk_iters`]: crate::keychain::KeychainTxOutIndex::all_unbounded_spk_iters + /// [`KeychainTxOutIndex::all_unbounded_spk_iters`]: crate::indexer::keychain_txout::KeychainTxOutIndex::all_unbounded_spk_iters #[cfg(feature = "miniscript")] #[must_use] pub fn from_keychain_txout_index( chain_tip: CheckPoint, - index: &crate::keychain::KeychainTxOutIndex, + index: &crate::indexer::keychain_txout::KeychainTxOutIndex, ) -> Self where K: core::fmt::Debug, diff --git a/crates/chain/src/spk_iter.rs b/crates/chain/src/spk_iter.rs index dd4c0a8f..7228b719 100644 --- a/crates/chain/src/spk_iter.rs +++ b/crates/chain/src/spk_iter.rs @@ -1,7 +1,7 @@ use crate::{ bitcoin::{secp256k1::Secp256k1, ScriptBuf}, - keychain::Indexed, miniscript::{Descriptor, DescriptorPublicKey}, + Indexed, }; use core::{borrow::Borrow, ops::Bound, ops::RangeBounds}; @@ -137,7 +137,7 @@ where mod test { use crate::{ bitcoin::secp256k1::Secp256k1, - keychain::KeychainTxOutIndex, + indexer::keychain_txout::KeychainTxOutIndex, miniscript::{Descriptor, DescriptorPublicKey}, spk_iter::{SpkIterator, BIP32_MAX_INDEX}, }; diff --git a/crates/chain/src/spk_txout_index.rs b/crates/chain/src/spk_txout_index.rs deleted file mode 100644 index f664af89..00000000 --- a/crates/chain/src/spk_txout_index.rs +++ /dev/null @@ -1,332 +0,0 @@ -use core::ops::RangeBounds; - -use crate::{ - collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap}, - indexed_tx_graph::Indexer, -}; -use bitcoin::{Amount, OutPoint, Script, ScriptBuf, SignedAmount, Transaction, TxOut, Txid}; - -/// An index storing [`TxOut`]s that have a script pubkey that matches those in a list. -/// -/// The basic idea is that you insert script pubkeys you care about into the index with -/// [`insert_spk`] and then when you call [`Indexer::index_tx`] or [`Indexer::index_txout`], the -/// index will look at any txouts you pass in and store and index any txouts matching one of its -/// script pubkeys. -/// -/// Each script pubkey is associated with an application-defined index script index `I`, which must be -/// [`Ord`]. Usually, this is used to associate the derivation index of the script pubkey or even a -/// combination of `(keychain, derivation_index)`. -/// -/// Note there is no harm in scanning transactions that disappear from the blockchain or were never -/// in there in the first place. `SpkTxOutIndex` is intentionally *monotone* -- you cannot delete or -/// modify txouts that have been indexed. To find out which txouts from the index are actually in the -/// chain or unspent, you must use other sources of information like a [`TxGraph`]. -/// -/// [`TxOut`]: bitcoin::TxOut -/// [`insert_spk`]: Self::insert_spk -/// [`Ord`]: core::cmp::Ord -/// [`TxGraph`]: crate::tx_graph::TxGraph -#[derive(Clone, Debug)] -pub struct SpkTxOutIndex { - /// script pubkeys ordered by index - spks: BTreeMap, - /// A reverse lookup from spk to spk index - spk_indices: HashMap, - /// The set of unused indexes. - unused: BTreeSet, - /// Lookup index and txout by outpoint. - txouts: BTreeMap, - /// Lookup from spk index to outpoints that had that spk - spk_txouts: BTreeSet<(I, OutPoint)>, -} - -impl Default for SpkTxOutIndex { - fn default() -> Self { - Self { - txouts: Default::default(), - spks: Default::default(), - spk_indices: Default::default(), - spk_txouts: Default::default(), - unused: Default::default(), - } - } -} - -impl Indexer for SpkTxOutIndex { - type ChangeSet = (); - - fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet { - self.scan_txout(outpoint, txout); - Default::default() - } - - fn index_tx(&mut self, tx: &Transaction) -> Self::ChangeSet { - self.scan(tx); - Default::default() - } - - fn initial_changeset(&self) -> Self::ChangeSet {} - - fn apply_changeset(&mut self, _changeset: Self::ChangeSet) { - // This applies nothing. - } - - fn is_tx_relevant(&self, tx: &Transaction) -> bool { - self.is_relevant(tx) - } -} - -impl SpkTxOutIndex { - /// Scans a transaction's outputs for matching script pubkeys. - /// - /// Typically, this is used in two situations: - /// - /// 1. After loading transaction data from the disk, you may scan over all the txouts to restore all - /// your txouts. - /// 2. When getting new data from the chain, you usually scan it before incorporating it into your chain state. - pub fn scan(&mut self, tx: &Transaction) -> BTreeSet { - let mut scanned_indices = BTreeSet::new(); - let txid = tx.compute_txid(); - for (i, txout) in tx.output.iter().enumerate() { - let op = OutPoint::new(txid, i as u32); - if let Some(spk_i) = self.scan_txout(op, txout) { - scanned_indices.insert(spk_i.clone()); - } - } - - scanned_indices - } - - /// Scan a single `TxOut` for a matching script pubkey and returns the index that matches the - /// script pubkey (if any). - pub fn scan_txout(&mut self, op: OutPoint, txout: &TxOut) -> Option<&I> { - let spk_i = self.spk_indices.get(&txout.script_pubkey); - if let Some(spk_i) = spk_i { - self.txouts.insert(op, (spk_i.clone(), txout.clone())); - self.spk_txouts.insert((spk_i.clone(), op)); - self.unused.remove(spk_i); - } - spk_i - } - - /// Get a reference to the set of indexed outpoints. - pub fn outpoints(&self) -> &BTreeSet<(I, OutPoint)> { - &self.spk_txouts - } - - /// Iterate over all known txouts that spend to tracked script pubkeys. - pub fn txouts( - &self, - ) -> impl DoubleEndedIterator + ExactSizeIterator { - self.txouts - .iter() - .map(|(op, (index, txout))| (index, *op, txout)) - } - - /// Finds all txouts on a transaction that has previously been scanned and indexed. - pub fn txouts_in_tx( - &self, - txid: Txid, - ) -> impl DoubleEndedIterator { - self.txouts - .range(OutPoint::new(txid, u32::MIN)..=OutPoint::new(txid, u32::MAX)) - .map(|(op, (index, txout))| (index, *op, txout)) - } - - /// Iterates over all the outputs with script pubkeys in an index range. - pub fn outputs_in_range( - &self, - range: impl RangeBounds, - ) -> impl DoubleEndedIterator { - use bitcoin::hashes::Hash; - use core::ops::Bound::*; - let min_op = OutPoint { - txid: Txid::all_zeros(), - vout: u32::MIN, - }; - let max_op = OutPoint { - txid: Txid::from_byte_array([0xff; Txid::LEN]), - vout: u32::MAX, - }; - - let start = match range.start_bound() { - Included(index) => Included((index.clone(), min_op)), - Excluded(index) => Excluded((index.clone(), max_op)), - Unbounded => Unbounded, - }; - - let end = match range.end_bound() { - Included(index) => Included((index.clone(), max_op)), - Excluded(index) => Excluded((index.clone(), min_op)), - Unbounded => Unbounded, - }; - - self.spk_txouts.range((start, end)).map(|(i, op)| (i, *op)) - } - - /// Returns the txout and script pubkey index of the `TxOut` at `OutPoint`. - /// - /// Returns `None` if the `TxOut` hasn't been scanned or if nothing matching was found there. - pub fn txout(&self, outpoint: OutPoint) -> Option<(&I, &TxOut)> { - self.txouts.get(&outpoint).map(|v| (&v.0, &v.1)) - } - - /// Returns the script that has been inserted at the `index`. - /// - /// If that index hasn't been inserted yet, it will return `None`. - pub fn spk_at_index(&self, index: &I) -> Option<&Script> { - self.spks.get(index).map(|s| s.as_script()) - } - - /// The script pubkeys that are being tracked by the index. - pub fn all_spks(&self) -> &BTreeMap { - &self.spks - } - - /// Adds a script pubkey to scan for. Returns `false` and does nothing if spk already exists in the map - /// - /// the index will look for outputs spending to this spk whenever it scans new data. - pub fn insert_spk(&mut self, index: I, spk: ScriptBuf) -> bool { - match self.spk_indices.entry(spk.clone()) { - Entry::Vacant(value) => { - value.insert(index.clone()); - self.spks.insert(index.clone(), spk); - self.unused.insert(index); - true - } - Entry::Occupied(_) => false, - } - } - - /// Iterates over all unused script pubkeys in an index range. - /// - /// Here, "unused" means that after the script pubkey was stored in the index, the index has - /// never scanned a transaction output with it. - /// - /// # Example - /// - /// ```rust - /// # use bdk_chain::SpkTxOutIndex; - /// - /// // imagine our spks are indexed like (keychain, derivation_index). - /// let txout_index = SpkTxOutIndex::<(u32, u32)>::default(); - /// let all_unused_spks = txout_index.unused_spks(..); - /// let change_index = 1; - /// let unused_change_spks = - /// txout_index.unused_spks((change_index, u32::MIN)..(change_index, u32::MAX)); - /// ``` - pub fn unused_spks(&self, range: R) -> impl DoubleEndedIterator + Clone - where - R: RangeBounds, - { - self.unused - .range(range) - .map(move |index| (index, self.spk_at_index(index).expect("must exist"))) - } - - /// Returns whether the script pubkey at `index` has been used or not. - /// - /// Here, "unused" means that after the script pubkey was stored in the index, the index has - /// never scanned a transaction output with it. - pub fn is_used(&self, index: &I) -> bool { - !self.unused.contains(index) - } - - /// Marks the script pubkey at `index` as used even though it hasn't seen an output spending to it. - /// This only affects when the `index` had already been added to `self` and was unused. - /// - /// Returns whether the `index` was initially present as `unused`. - /// - /// This is useful when you want to reserve a script pubkey for something but don't want to add - /// the transaction output using it to the index yet. Other callers will consider the `index` used - /// until you call [`unmark_used`]. - /// - /// [`unmark_used`]: Self::unmark_used - pub fn mark_used(&mut self, index: &I) -> bool { - self.unused.remove(index) - } - - /// Undoes the effect of [`mark_used`]. Returns whether the `index` is inserted back into - /// `unused`. - /// - /// Note that if `self` has scanned an output with this script pubkey then this will have no - /// effect. - /// - /// [`mark_used`]: Self::mark_used - pub fn unmark_used(&mut self, index: &I) -> bool { - // we cannot set the index as unused when it does not exist - if !self.spks.contains_key(index) { - return false; - } - // we cannot set the index as unused when txouts are indexed under it - if self.outputs_in_range(index..=index).next().is_some() { - return false; - } - self.unused.insert(index.clone()) - } - - /// Returns the index associated with the script pubkey. - pub fn index_of_spk(&self, script: &Script) -> Option<&I> { - self.spk_indices.get(script) - } - - /// Computes the total value transfer effect `tx` has on the script pubkeys in `range`. Value is - /// *sent* when a script pubkey in the `range` is on an input and *received* when it is on an - /// output. For `sent` to be computed correctly, the output being spent must have already been - /// scanned by the index. Calculating received just uses the [`Transaction`] outputs directly, - /// so it will be correct even if it has not been scanned. - pub fn sent_and_received( - &self, - tx: &Transaction, - range: impl RangeBounds, - ) -> (Amount, Amount) { - let mut sent = Amount::ZERO; - let mut received = Amount::ZERO; - - for txin in &tx.input { - if let Some((index, txout)) = self.txout(txin.previous_output) { - if range.contains(index) { - sent += txout.value; - } - } - } - for txout in &tx.output { - if let Some(index) = self.index_of_spk(&txout.script_pubkey) { - if range.contains(index) { - received += txout.value; - } - } - } - - (sent, received) - } - - /// Computes the net value transfer effect of `tx` on the script pubkeys in `range`. Shorthand - /// for calling [`sent_and_received`] and subtracting sent from received. - /// - /// [`sent_and_received`]: Self::sent_and_received - pub fn net_value(&self, tx: &Transaction, range: impl RangeBounds) -> SignedAmount { - let (sent, received) = self.sent_and_received(tx, range); - received.to_signed().expect("valid `SignedAmount`") - - sent.to_signed().expect("valid `SignedAmount`") - } - - /// Whether any of the inputs of this transaction spend a txout tracked or whether any output - /// matches one of our script pubkeys. - /// - /// It is easily possible to misuse this method and get false negatives by calling it before you - /// have scanned the `TxOut`s the transaction is spending. For example, if you want to filter out - /// all the transactions in a block that are irrelevant, you **must first scan all the - /// transactions in the block** and only then use this method. - pub fn is_relevant(&self, tx: &Transaction) -> bool { - let input_matches = tx - .input - .iter() - .any(|input| self.txouts.contains_key(&input.previous_output)); - let output_matches = tx - .output - .iter() - .any(|output| self.spk_indices.contains_key(&output.script_pubkey)); - input_matches || output_matches - } -} diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index b79aa434..1532d13a 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -89,8 +89,7 @@ //! [`insert_txout`]: TxGraph::insert_txout use crate::{ - collections::*, keychain::Balance, Anchor, Append, BlockId, ChainOracle, ChainPosition, - FullTxOut, + collections::*, Anchor, Append, Balance, BlockId, ChainOracle, ChainPosition, FullTxOut, }; use alloc::collections::vec_deque::VecDeque; use alloc::sync::Arc; diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index dbd7fd30..57f98374 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -8,9 +8,9 @@ use std::{collections::BTreeSet, sync::Arc}; use crate::common::DESCRIPTORS; use bdk_chain::{ indexed_tx_graph::{self, IndexedTxGraph}, - keychain::{self, Balance, KeychainTxOutIndex}, + indexer::keychain_txout::KeychainTxOutIndex, local_chain::LocalChain, - tx_graph, Append, ChainPosition, ConfirmationHeightAnchor, DescriptorExt, + tx_graph, Append, Balance, ChainPosition, ConfirmationHeightAnchor, DescriptorExt, }; use bitcoin::{ secp256k1::Secp256k1, Amount, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut, @@ -26,6 +26,7 @@ use miniscript::Descriptor; /// agnostic. #[test] fn insert_relevant_txs() { + use bdk_chain::indexer::keychain_txout; let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTORS[0]) .expect("must be valid"); let spk_0 = descriptor.at_derivation_index(0).unwrap().script_pubkey(); @@ -76,7 +77,7 @@ fn insert_relevant_txs() { txs: txs.iter().cloned().map(Arc::new).collect(), ..Default::default() }, - indexer: keychain::ChangeSet { + indexer: keychain_txout::ChangeSet { last_revealed: [(descriptor.descriptor_id(), 9_u32)].into(), keychains_added: [].into(), }, @@ -90,7 +91,7 @@ fn insert_relevant_txs() { // The initial changeset will also contain info about the keychain we added let initial_changeset = indexed_tx_graph::ChangeSet { graph: changeset.graph, - indexer: keychain::ChangeSet { + indexer: keychain_txout::ChangeSet { last_revealed: changeset.indexer.last_revealed, keychains_added: [((), descriptor)].into(), }, diff --git a/crates/chain/tests/test_keychain_txout_index.rs b/crates/chain/tests/test_keychain_txout_index.rs index 2c1c34e7..751786dd 100644 --- a/crates/chain/tests/test_keychain_txout_index.rs +++ b/crates/chain/tests/test_keychain_txout_index.rs @@ -4,9 +4,8 @@ mod common; use bdk_chain::{ collections::BTreeMap, - indexed_tx_graph::Indexer, - keychain::{self, ChangeSet, KeychainTxOutIndex}, - Append, DescriptorExt, DescriptorId, + indexer::keychain_txout::{ChangeSet, KeychainTxOutIndex}, + Append, DescriptorExt, DescriptorId, Indexer, }; use bitcoin::{secp256k1::Secp256k1, Amount, OutPoint, ScriptBuf, Transaction, TxOut}; @@ -31,8 +30,8 @@ fn init_txout_index( external_descriptor: Descriptor, internal_descriptor: Descriptor, lookahead: u32, -) -> bdk_chain::keychain::KeychainTxOutIndex { - let mut txout_index = bdk_chain::keychain::KeychainTxOutIndex::::new(lookahead); +) -> KeychainTxOutIndex { + let mut txout_index = KeychainTxOutIndex::::new(lookahead); let _ = txout_index .insert_descriptor(TestKeychain::External, external_descriptor) @@ -146,8 +145,6 @@ fn when_apply_contradictory_changesets_they_are_ignored() { #[test] fn test_set_all_derivation_indices() { - use bdk_chain::indexed_tx_graph::Indexer; - let external_descriptor = parse_descriptor(DESCRIPTORS[0]); let internal_descriptor = parse_descriptor(DESCRIPTORS[1]); let mut txout_index = @@ -169,7 +166,7 @@ fn test_set_all_derivation_indices() { assert_eq!(txout_index.last_revealed_indices(), derive_to); assert_eq!( txout_index.reveal_to_target_multi(&derive_to), - keychain::ChangeSet::default(), + ChangeSet::default(), "no changes if we set to the same thing" ); assert_eq!(txout_index.initial_changeset().last_revealed, last_revealed); @@ -304,7 +301,7 @@ fn test_lookahead() { ], ..common::new_tx(external_index) }; - assert_eq!(txout_index.index_tx(&tx), keychain::ChangeSet::default()); + assert_eq!(txout_index.index_tx(&tx), ChangeSet::default()); assert_eq!( txout_index.last_revealed_index(&TestKeychain::External), Some(last_external_index) @@ -643,14 +640,14 @@ fn insert_descriptor_no_change() { let mut txout_index = KeychainTxOutIndex::<()>::default(); assert_eq!( txout_index.insert_descriptor((), desc.clone()), - Ok(keychain::ChangeSet { + Ok(ChangeSet { keychains_added: [((), desc.clone())].into(), last_revealed: Default::default() }), ); assert_eq!( txout_index.insert_descriptor((), desc.clone()), - Ok(keychain::ChangeSet::default()), + Ok(ChangeSet::default()), "inserting the same descriptor for keychain should return an empty changeset", ); } diff --git a/crates/chain/tests/test_spk_txout_index.rs b/crates/chain/tests/test_spk_txout_index.rs index 8be25270..ccad2af0 100644 --- a/crates/chain/tests/test_spk_txout_index.rs +++ b/crates/chain/tests/test_spk_txout_index.rs @@ -1,4 +1,4 @@ -use bdk_chain::{indexed_tx_graph::Indexer, SpkTxOutIndex}; +use bdk_chain::{Indexer, SpkTxOutIndex}; use bitcoin::{ absolute, transaction, Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxIn, TxOut, }; diff --git a/crates/chain/tests/test_tx_graph_conflicts.rs b/crates/chain/tests/test_tx_graph_conflicts.rs index 512b076d..802ba5c7 100644 --- a/crates/chain/tests/test_tx_graph_conflicts.rs +++ b/crates/chain/tests/test_tx_graph_conflicts.rs @@ -5,7 +5,7 @@ mod common; use std::collections::{BTreeSet, HashSet}; -use bdk_chain::{keychain::Balance, BlockId}; +use bdk_chain::{Balance, BlockId}; use bitcoin::{Amount, OutPoint, Script}; use common::*; diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index 9788b251..1befb326 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -1,9 +1,8 @@ use bdk_chain::{ bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash}, - keychain::Balance, local_chain::LocalChain, spk_client::SyncRequest, - ConfirmationTimeHeightAnchor, IndexedTxGraph, SpkTxOutIndex, + Balance, ConfirmationTimeHeightAnchor, IndexedTxGraph, SpkTxOutIndex, }; use bdk_electrum::BdkElectrumClient; use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; diff --git a/crates/esplora/src/async_ext.rs b/crates/esplora/src/async_ext.rs index dff52fdd..939c43a2 100644 --- a/crates/esplora/src/async_ext.rs +++ b/crates/esplora/src/async_ext.rs @@ -231,7 +231,7 @@ async fn chain_update( } /// This performs a full scan to get an update for the [`TxGraph`] and -/// [`KeychainTxOutIndex`](bdk_chain::keychain::KeychainTxOutIndex). +/// [`KeychainTxOutIndex`](bdk_chain::indexer::keychain_txout::KeychainTxOutIndex). async fn full_scan_for_index_and_graph( client: &esplora_client::AsyncClient, keychain_spks: BTreeMap< diff --git a/crates/esplora/src/blocking_ext.rs b/crates/esplora/src/blocking_ext.rs index 5093b4ef..adad25c2 100644 --- a/crates/esplora/src/blocking_ext.rs +++ b/crates/esplora/src/blocking_ext.rs @@ -213,7 +213,7 @@ fn chain_update( } /// This performs a full scan to get an update for the [`TxGraph`] and -/// [`KeychainTxOutIndex`](bdk_chain::keychain::KeychainTxOutIndex). +/// [`KeychainTxOutIndex`](bdk_chain::indexer::keychain_txout::KeychainTxOutIndex). fn full_scan_for_index_and_graph_blocking( client: &esplora_client::BlockingClient, keychain_spks: BTreeMap>>, diff --git a/crates/sqlite/src/store.rs b/crates/sqlite/src/store.rs index 4cd81f50..82b25b5d 100644 --- a/crates/sqlite/src/store.rs +++ b/crates/sqlite/src/store.rs @@ -14,7 +14,8 @@ use std::sync::{Arc, Mutex}; use crate::Error; use bdk_chain::CombinedChangeSet; use bdk_chain::{ - indexed_tx_graph, keychain, local_chain, tx_graph, Anchor, Append, DescriptorExt, DescriptorId, + indexed_tx_graph, indexer::keychain_txout, local_chain, tx_graph, Anchor, Append, + DescriptorExt, DescriptorId, }; /// Persists data in to a relational schema based [SQLite] database file. @@ -187,7 +188,7 @@ where /// If keychain exists only update last active index. fn insert_keychains( db_transaction: &rusqlite::Transaction, - tx_graph_changeset: &indexed_tx_graph::ChangeSet>, + tx_graph_changeset: &indexed_tx_graph::ChangeSet>, ) -> Result<(), Error> { let keychain_changeset = &tx_graph_changeset.indexer; for (keychain, descriptor) in keychain_changeset.keychains_added.iter() { @@ -206,7 +207,7 @@ where /// Update descriptor last revealed index. fn update_last_revealed( db_transaction: &rusqlite::Transaction, - tx_graph_changeset: &indexed_tx_graph::ChangeSet>, + tx_graph_changeset: &indexed_tx_graph::ChangeSet>, ) -> Result<(), Error> { let keychain_changeset = &tx_graph_changeset.indexer; for (descriptor_id, last_revealed) in keychain_changeset.last_revealed.iter() { @@ -279,7 +280,7 @@ impl Store { /// Error if trying to insert existing txid. fn insert_txs( db_transaction: &rusqlite::Transaction, - tx_graph_changeset: &indexed_tx_graph::ChangeSet>, + tx_graph_changeset: &indexed_tx_graph::ChangeSet>, ) -> Result<(), Error> { for tx in tx_graph_changeset.graph.txs.iter() { let insert_tx_stmt = &mut db_transaction @@ -343,7 +344,7 @@ impl Store { /// Error if trying to insert existing outpoint. fn insert_txouts( db_transaction: &rusqlite::Transaction, - tx_graph_changeset: &indexed_tx_graph::ChangeSet>, + tx_graph_changeset: &indexed_tx_graph::ChangeSet>, ) -> Result<(), Error> { for txout in tx_graph_changeset.graph.txouts.iter() { let insert_txout_stmt = &mut db_transaction @@ -393,7 +394,7 @@ impl Store { /// Update transaction last seen times. fn update_last_seen( db_transaction: &rusqlite::Transaction, - tx_graph_changeset: &indexed_tx_graph::ChangeSet>, + tx_graph_changeset: &indexed_tx_graph::ChangeSet>, ) -> Result<(), Error> { for tx_last_seen in tx_graph_changeset.graph.last_seen.iter() { let insert_or_update_tx_stmt = &mut db_transaction @@ -418,7 +419,7 @@ where /// Insert anchors. fn insert_anchors( db_transaction: &rusqlite::Transaction, - tx_graph_changeset: &indexed_tx_graph::ChangeSet>, + tx_graph_changeset: &indexed_tx_graph::ChangeSet>, ) -> Result<(), Error> { // serde_json::to_string for anchor in tx_graph_changeset.graph.anchors.iter() { @@ -514,12 +515,12 @@ where last_seen, }; - let indexer: keychain::ChangeSet = keychain::ChangeSet { + let indexer = keychain_txout::ChangeSet { keychains_added, last_revealed, }; - let indexed_tx_graph: indexed_tx_graph::ChangeSet> = + let indexed_tx_graph: indexed_tx_graph::ChangeSet> = indexed_tx_graph::ChangeSet { graph, indexer }; if network.is_none() && chain.is_empty() && indexed_tx_graph.is_empty() { @@ -547,7 +548,7 @@ mod test { use bdk_chain::miniscript::Descriptor; use bdk_chain::CombinedChangeSet; use bdk_chain::{ - indexed_tx_graph, keychain, tx_graph, BlockId, ConfirmationHeightAnchor, + indexed_tx_graph, tx_graph, BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor, DescriptorExt, }; use std::str::FromStr; @@ -684,12 +685,12 @@ mod test { .into(), }; - let keychain_changeset = keychain::ChangeSet { + let keychain_changeset = keychain_txout::ChangeSet { keychains_added: [(ext_keychain, ext_desc), (int_keychain, int_desc)].into(), last_revealed: [(ext_desc_id, 124), (int_desc_id, 421)].into(), }; - let graph_changeset: indexed_tx_graph::ChangeSet> = + let graph_changeset: indexed_tx_graph::ChangeSet> = indexed_tx_graph::ChangeSet { graph: tx_graph_changeset, indexer: keychain_changeset, @@ -712,10 +713,10 @@ mod test { last_seen: [(tx2.compute_txid(), 1708919121)].into(), }; - let graph_changeset2: indexed_tx_graph::ChangeSet> = + let graph_changeset2: indexed_tx_graph::ChangeSet> = indexed_tx_graph::ChangeSet { graph: tx_graph_changeset2, - indexer: keychain::ChangeSet::default(), + indexer: keychain_txout::ChangeSet::default(), }; changesets.push(CombinedChangeSet { @@ -732,10 +733,10 @@ mod test { last_seen: BTreeMap::default(), }; - let graph_changeset3: indexed_tx_graph::ChangeSet> = + let graph_changeset3: indexed_tx_graph::ChangeSet> = indexed_tx_graph::ChangeSet { graph: tx_graph_changeset3, - indexer: keychain::ChangeSet::default(), + indexer: keychain_txout::ChangeSet::default(), }; changesets.push(CombinedChangeSet { diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 23d7eb11..be158a06 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -19,10 +19,10 @@ use alloc::{ sync::Arc, vec::Vec, }; -pub use bdk_chain::keychain::Balance; +pub use bdk_chain::Balance; use bdk_chain::{ indexed_tx_graph, - keychain::KeychainTxOutIndex, + indexer::keychain_txout::KeychainTxOutIndex, local_chain::{ self, ApplyHeaderError, CannotConnectError, CheckPoint, CheckPointIter, LocalChain, }, @@ -112,7 +112,7 @@ pub struct Wallet { /// An update to [`Wallet`]. /// -/// It updates [`bdk_chain::keychain::KeychainTxOutIndex`], [`bdk_chain::TxGraph`] and [`local_chain::LocalChain`] atomically. +/// It updates [`KeychainTxOutIndex`], [`bdk_chain::TxGraph`] and [`local_chain::LocalChain`] atomically. #[derive(Debug, Clone, Default)] pub struct Update { /// Contains the last active derivation indices per keychain (`K`), which is used to update the @@ -2451,7 +2451,7 @@ fn create_signers( let _ = index .insert_descriptor(KeychainKind::Internal, descriptor) .map_err(|e| { - use bdk_chain::keychain::InsertDescriptorError; + use bdk_chain::indexer::keychain_txout::InsertDescriptorError; match e { InsertDescriptorError::DescriptorAlreadyAssigned { .. } => { crate::descriptor::error::Error::ExternalAndInternalAreTheSame diff --git a/crates/wallet/tests/common.rs b/crates/wallet/tests/common.rs index 0a3e479b..c124ba21 100644 --- a/crates/wallet/tests/common.rs +++ b/crates/wallet/tests/common.rs @@ -1,13 +1,12 @@ #![allow(unused)] - -use bdk_chain::indexed_tx_graph::Indexer; use bdk_chain::{BlockId, ConfirmationTime, ConfirmationTimeHeightAnchor, TxGraph}; -use bdk_wallet::wallet::Update; -use bdk_wallet::{KeychainKind, LocalOutput, Wallet}; -use bitcoin::hashes::Hash; +use bdk_wallet::{ + wallet::{Update, Wallet}, + KeychainKind, LocalOutput, +}; use bitcoin::{ - transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction, TxIn, TxOut, - Txid, + hashes::Hash, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction, + TxIn, TxOut, Txid, }; use std::str::FromStr; diff --git a/example-crates/example_bitcoind_rpc_polling/src/main.rs b/example-crates/example_bitcoind_rpc_polling/src/main.rs index 38e79647..084b9381 100644 --- a/example-crates/example_bitcoind_rpc_polling/src/main.rs +++ b/example-crates/example_bitcoind_rpc_polling/src/main.rs @@ -13,7 +13,8 @@ use bdk_bitcoind_rpc::{ }; use bdk_chain::{ bitcoin::{constants::genesis_block, Block, Transaction}, - indexed_tx_graph, keychain, + indexed_tx_graph, + indexer::keychain_txout, local_chain::{self, LocalChain}, Append, ConfirmationTimeHeightAnchor, IndexedTxGraph, }; @@ -37,7 +38,7 @@ const DB_COMMIT_DELAY: Duration = Duration::from_secs(60); type ChangeSet = ( local_chain::ChangeSet, - indexed_tx_graph::ChangeSet>, + indexed_tx_graph::ChangeSet>, ); #[derive(Debug)] diff --git a/example-crates/example_cli/src/lib.rs b/example-crates/example_cli/src/lib.rs index b1862fb8..bd2e3b8c 100644 --- a/example-crates/example_cli/src/lib.rs +++ b/example-crates/example_cli/src/lib.rs @@ -14,7 +14,7 @@ use bdk_chain::{ transaction, Address, Amount, Network, Sequence, Transaction, TxIn, TxOut, }, indexed_tx_graph::{self, IndexedTxGraph}, - keychain::{self, KeychainTxOutIndex}, + indexer::keychain_txout::{self, KeychainTxOutIndex}, local_chain, miniscript::{ descriptor::{DescriptorSecretKey, KeyMap}, @@ -30,7 +30,7 @@ use clap::{Parser, Subcommand}; pub type KeychainTxGraph = IndexedTxGraph>; pub type KeychainChangeSet = ( local_chain::ChangeSet, - indexed_tx_graph::ChangeSet>, + indexed_tx_graph::ChangeSet>, ); #[derive(Parser)] @@ -191,7 +191,7 @@ impl core::fmt::Display for Keychain { } pub struct CreateTxChange { - pub index_changeset: keychain::ChangeSet, + pub index_changeset: keychain_txout::ChangeSet, pub change_keychain: Keychain, pub index: u32, } @@ -207,7 +207,7 @@ pub fn create_tx( where O::Error: std::error::Error + Send + Sync + 'static, { - let mut changeset = keychain::ChangeSet::default(); + let mut changeset = keychain_txout::ChangeSet::default(); let assets = bdk_tmp_plan::Assets { keys: keymap.iter().map(|(pk, _)| pk.clone()).collect(), diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index 79c6f79e..2ea4d86f 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -7,7 +7,7 @@ use bdk_chain::{ bitcoin::{constants::genesis_block, Address, Network, Txid}, collections::BTreeSet, indexed_tx_graph::{self, IndexedTxGraph}, - keychain, + indexer::keychain_txout, local_chain::{self, LocalChain}, spk_client::{FullScanRequest, SyncRequest}, Append, ConfirmationHeightAnchor, @@ -100,7 +100,7 @@ pub struct ScanOptions { type ChangeSet = ( local_chain::ChangeSet, - indexed_tx_graph::ChangeSet>, + indexed_tx_graph::ChangeSet>, ); fn main() -> anyhow::Result<()> { diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index 60cf1ef3..49b3167d 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -7,7 +7,7 @@ use std::{ use bdk_chain::{ bitcoin::{constants::genesis_block, Address, Network, Txid}, indexed_tx_graph::{self, IndexedTxGraph}, - keychain, + indexer::keychain_txout, local_chain::{self, LocalChain}, spk_client::{FullScanRequest, SyncRequest}, Append, ConfirmationTimeHeightAnchor, @@ -26,7 +26,7 @@ const DB_PATH: &str = ".bdk_esplora_example.db"; type ChangeSet = ( local_chain::ChangeSet, - indexed_tx_graph::ChangeSet>, + indexed_tx_graph::ChangeSet>, ); #[derive(Subcommand, Debug, Clone)]