diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs index f62c2f16..64d65c0b 100644 --- a/crates/chain/src/keychain/txout_index.rs +++ b/crates/chain/src/keychain/txout_index.rs @@ -15,87 +15,6 @@ use core::{ use super::*; use crate::Append; -/// 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`]. [`ChangeSet] are -/// monotone in that they will never decrease the revealed derivation index. -/// -/// [`KeychainTxOutIndex`]: crate::keychain::KeychainTxOutIndex -/// [`apply_changeset`]: crate::keychain::KeychainTxOutIndex::apply_changeset -#[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 { - /// Append another [`ChangeSet`] into self. - /// - /// For each keychain in `keychains_added` in the given [`ChangeSet`]: - /// If the keychain already exist with a different descriptor, we overwrite the old descriptor. - /// - /// For each `last_revealed` in the given [`ChangeSet`]: - /// If the keychain already exists, increase the index when the other's index > self's index. - fn append(&mut self, other: Self) { - for (new_keychain, new_descriptor) in other.keychains_added { - 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(), - } - } -} - /// The default lookahead for a [`KeychainTxOutIndex`] pub const DEFAULT_LOOKAHEAD: u32 = 25; @@ -106,6 +25,10 @@ pub const DEFAULT_LOOKAHEAD: u32 = 25; /// 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. +/// /// # Revealed script pubkeys /// /// Tracking how script pubkeys are revealed is useful for collecting chain data. For example, if @@ -177,18 +100,19 @@ pub const DEFAULT_LOOKAHEAD: u32 = 25; /// [`Ord`]: core::cmp::Ord /// [`SpkTxOutIndex`]: crate::spk_txout_index::SpkTxOutIndex /// [`Descriptor`]: crate::miniscript::Descriptor -/// [`reveal_to_target`]: KeychainTxOutIndex::reveal_to_target -/// [`reveal_next_spk`]: KeychainTxOutIndex::reveal_next_spk -/// [`revealed_keychain_spks`]: KeychainTxOutIndex::revealed_keychain_spks -/// [`revealed_spks`]: KeychainTxOutIndex::revealed_spks -/// [`index_tx`]: KeychainTxOutIndex::index_tx -/// [`index_txout`]: KeychainTxOutIndex::index_txout -/// [`new`]: KeychainTxOutIndex::new -/// [`unbounded_spk_iter`]: KeychainTxOutIndex::unbounded_spk_iter -/// [`all_unbounded_spk_iters`]: KeychainTxOutIndex::all_unbounded_spk_iters -/// [`outpoints`]: KeychainTxOutIndex::outpoints -/// [`txouts`]: KeychainTxOutIndex::txouts -/// [`unused_spks`]: KeychainTxOutIndex::unused_spks +/// [`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)>, @@ -879,19 +803,18 @@ impl KeychainTxOutIndex { .collect() } - /// Applies the derivation changeset to the [`KeychainTxOutIndex`], as specified in the - /// [`ChangeSet::append`] documentation: - /// - Extends the number of derived scripts per keychain - /// - Adds new descriptors introduced - /// - If a descriptor is introduced for a keychain that already had a descriptor, overwrites - /// the old descriptor + /// 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 _ = self.insert_descriptor(keychain, descriptor); + let _ignore_invariant_violation = self.insert_descriptor(keychain, descriptor); } for (&desc_id, &index) in &last_revealed { @@ -951,3 +874,88 @@ impl core::fmt::Display for InsertDescriptorError { #[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(), + } + } +}