diff --git a/Cargo.toml b/Cargo.toml index 9549e423..83b199b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "crates/bdk", + "crates/bdk_chain", "example-crates/esplora-wallet", "example-crates/electrum-wallet", ] diff --git a/crates/chain/Cargo.toml b/crates/chain/Cargo.toml new file mode 100644 index 00000000..6877b2b8 --- /dev/null +++ b/crates/chain/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "bdk_chain" +version = "0.3.1" +edition = "2021" +homepage = "https://bitcoindevkit.org" +repository = "https://github.com/LLFourn/bdk_core_staging" +documentation = "https://docs.rs/bdk_chain" +description = "Collection of core structures for Bitcoin Dev Kit." +license = "MIT OR Apache-2.0" +readme = "../README.md" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bitcoin = { version = "0.29" } +serde_crate = { package = "serde", version = "1", optional = true, features = ["derive"] } +# Use hashbrown as a feature flag to have HashSet and HashMap from it. +hashbrown = { version = "0.13.2", optional = true } +miniscript = { version = "9.0.0", optional = true } + +[dev-dependencies] +rand = "0.8" + +[features] +default = ["std", "miniscript"] +std = [] +serde = ["serde_crate", "bitcoin/serde"] diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs new file mode 100644 index 00000000..51b1e3b2 --- /dev/null +++ b/crates/chain/src/chain_data.rs @@ -0,0 +1,218 @@ +use bitcoin::{hashes::Hash, BlockHash, OutPoint, TxOut, Txid}; + +use crate::{ + sparse_chain::{self, ChainPosition}, + COINBASE_MATURITY, +}; + +/// Represents the height in which a transaction is confirmed at. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(crate = "serde_crate") +)] +pub enum TxHeight { + Confirmed(u32), + Unconfirmed, +} + +impl Default for TxHeight { + fn default() -> Self { + Self::Unconfirmed + } +} + +impl core::fmt::Display for TxHeight { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Confirmed(h) => core::write!(f, "confirmed_at({})", h), + Self::Unconfirmed => core::write!(f, "unconfirmed"), + } + } +} + +impl From> for TxHeight { + fn from(opt: Option) -> Self { + match opt { + Some(h) => Self::Confirmed(h), + None => Self::Unconfirmed, + } + } +} + +impl From for Option { + fn from(height: TxHeight) -> Self { + match height { + TxHeight::Confirmed(h) => Some(h), + TxHeight::Unconfirmed => None, + } + } +} + +impl crate::sparse_chain::ChainPosition for TxHeight { + fn height(&self) -> TxHeight { + *self + } + + fn max_ord_of_height(height: TxHeight) -> Self { + height + } + + fn min_ord_of_height(height: TxHeight) -> Self { + height + } +} + +impl TxHeight { + pub fn is_confirmed(&self) -> bool { + matches!(self, Self::Confirmed(_)) + } +} + +/// Block height and timestamp in which a transaction is confirmed in. +#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(crate = "serde_crate") +)] +pub enum ConfirmationTime { + Confirmed { height: u32, time: u64 }, + Unconfirmed, +} + +impl sparse_chain::ChainPosition for ConfirmationTime { + fn height(&self) -> TxHeight { + match self { + ConfirmationTime::Confirmed { height, .. } => TxHeight::Confirmed(*height), + ConfirmationTime::Unconfirmed => TxHeight::Unconfirmed, + } + } + + fn max_ord_of_height(height: TxHeight) -> Self { + match height { + TxHeight::Confirmed(height) => Self::Confirmed { + height, + time: u64::MAX, + }, + TxHeight::Unconfirmed => Self::Unconfirmed, + } + } + + fn min_ord_of_height(height: TxHeight) -> Self { + match height { + TxHeight::Confirmed(height) => Self::Confirmed { + height, + time: u64::MIN, + }, + TxHeight::Unconfirmed => Self::Unconfirmed, + } + } +} + +impl ConfirmationTime { + pub fn is_confirmed(&self) -> bool { + matches!(self, Self::Confirmed { .. }) + } +} + +/// A reference to a block in the cannonical chain. +#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(crate = "serde_crate") +)] +pub struct BlockId { + /// The height the block was confirmed at + pub height: u32, + /// The hash of the block + pub hash: BlockHash, +} + +impl Default for BlockId { + fn default() -> Self { + Self { + height: Default::default(), + hash: BlockHash::from_inner([0u8; 32]), + } + } +} + +impl From<(u32, BlockHash)> for BlockId { + fn from((height, hash): (u32, BlockHash)) -> Self { + Self { height, hash } + } +} + +impl From for (u32, BlockHash) { + fn from(block_id: BlockId) -> Self { + (block_id.height, block_id.hash) + } +} + +impl From<(&u32, &BlockHash)> for BlockId { + fn from((height, hash): (&u32, &BlockHash)) -> Self { + Self { + height: *height, + hash: *hash, + } + } +} + +/// A `TxOut` with as much data as we can retreive about it +#[derive(Debug, Clone, PartialEq)] +pub struct FullTxOut { + /// The location of the `TxOut` + pub outpoint: OutPoint, + /// The `TxOut` + pub txout: TxOut, + /// The position of the transaction in `outpoint` in the overall chain. + pub chain_position: I, + /// The txid and chain position of the transaction (if any) that has spent this output. + pub spent_by: Option<(I, Txid)>, + /// Whether this output is on a coinbase transaction + pub is_on_coinbase: bool, +} + +impl FullTxOut { + /// Whether the utxo is/was/will be spendable at `height`. + /// + /// It is spendable if it is not an immature coinbase output and no spending tx has been + /// confirmed by that heigt. + pub fn is_spendable_at(&self, height: u32) -> bool { + if !self.is_mature(height) { + return false; + } + + if self.chain_position.height() > TxHeight::Confirmed(height) { + return false; + } + + match &self.spent_by { + Some((spending_height, _)) => spending_height.height() > TxHeight::Confirmed(height), + None => true, + } + } + + pub fn is_mature(&self, height: u32) -> bool { + if self.is_on_coinbase { + let tx_height = match self.chain_position.height() { + TxHeight::Confirmed(tx_height) => tx_height, + TxHeight::Unconfirmed => { + debug_assert!(false, "coinbase tx can never be unconfirmed"); + return false; + } + }; + let age = height.saturating_sub(tx_height); + if age + 1 < COINBASE_MATURITY { + return false; + } + } + + true + } +} + +// TOOD: make test diff --git a/crates/chain/src/chain_graph.rs b/crates/chain/src/chain_graph.rs new file mode 100644 index 00000000..78a56a90 --- /dev/null +++ b/crates/chain/src/chain_graph.rs @@ -0,0 +1,620 @@ +//! Module for structures that combine the features of [`sparse_chain`] and [`tx_graph`]. +use crate::{ + collections::HashSet, + sparse_chain::{self, ChainPosition, SparseChain}, + tx_graph::{self, TxGraph}, + AsTransaction, BlockId, ForEachTxOut, FullTxOut, IntoOwned, TxHeight, +}; +use alloc::{borrow::Cow, string::ToString, vec::Vec}; +use bitcoin::{OutPoint, Transaction, TxOut, Txid}; +use core::fmt::Debug; + +/// A consistent combination of a [`SparseChain

`] and a [`TxGraph`]. +/// +/// `SparseChain` only keeps track of transaction ids and their position in the chain but you often +/// want to store the full transactions as well. Additionally you want to make sure that everything +/// in the chain is consistent with the full transaction data. `ChainGraph` enforces these two +/// invariants: +/// +/// 1. Every transaction that is in the chain is also in the graph (you always have the full +/// transaction). +/// 2. No transactions in the chain conflict with each other i.e. they don't double spend each +/// other or have ancestors that double spend each other. +/// +/// Note that the `ChainGraph` guarantees a 1:1 mapping between transactions in the `chain` and +/// `graph` but not the other way around. Transactions may fall out of the *chain* (via re-org or +/// mempool eviction) but will remain in the *graph*. +#[derive(Clone, Debug, PartialEq)] +pub struct ChainGraph

{ + chain: SparseChain

, + graph: TxGraph, +} + +impl Default for ChainGraph { + fn default() -> Self { + Self { + chain: Default::default(), + graph: Default::default(), + } + } +} + +impl AsRef> for ChainGraph { + fn as_ref(&self) -> &SparseChain

{ + &self.chain + } +} + +impl AsRef> for ChainGraph { + fn as_ref(&self) -> &TxGraph { + &self.graph + } +} + +impl AsRef> for ChainGraph { + fn as_ref(&self) -> &ChainGraph { + self + } +} + +impl ChainGraph { + /// Returns a reference to the internal [`SparseChain`]. + pub fn chain(&self) -> &SparseChain

{ + &self.chain + } + + /// Returns a reference to the internal [`TxGraph`]. + pub fn graph(&self) -> &TxGraph { + &self.graph + } +} + +impl ChainGraph +where + P: ChainPosition, + T: AsTransaction + Clone + Ord, +{ + /// Create a new chain graph from a `chain` and a `graph`. + /// + /// There are two reasons this can return an `Err`: + /// + /// 1. There is a transaction in the `chain` that does not have its corresponding full + /// transaction in `graph`. + /// 2. The `chain` has two transactions that allegedly in it but they conflict in the `graph` + /// (so could not possibly be in the same chain). + pub fn new(chain: SparseChain

, graph: TxGraph) -> Result> { + let mut missing = HashSet::default(); + for (pos, txid) in chain.txids() { + if let Some(tx) = graph.get_tx(*txid) { + let conflict = graph + .walk_conflicts(tx.as_tx(), |_, txid| { + Some((chain.tx_position(txid)?.clone(), txid)) + }) + .next(); + if let Some((conflict_pos, conflict)) = conflict { + return Err(NewError::Conflict { + a: (pos.clone(), *txid), + b: (conflict_pos, conflict), + }); + } + } else { + missing.insert(*txid); + } + } + + if !missing.is_empty() { + return Err(NewError::Missing(missing)); + } + + Ok(Self { chain, graph }) + } + + /// Take an update in the form of a [`SparseChain

`][`SparseChain`] and attempt to turn it + /// into a chain graph by filling in full transactions from `self` and from `new_txs`. This + /// returns a `ChainGraph>` where the [`Cow<'a, T>`] will borrow the transaction if it + /// got it from `self`. + /// + /// This is useful when interacting with services like an electrum server which returns a list + /// of txids and heights when calling [`script_get_history`] which can easily be inserted into a + /// [`SparseChain`][`SparseChain`]. From there you need to figure out which full + /// transactions you are missing in your chain graph and form `new_txs`. You then use + /// `inflate_update` to turn this into an update `ChainGraph>` and finally + /// use [`determine_changeset`] to generate the changeset from it. + /// + /// [`SparseChain`]: crate::sparse_chain::SparseChain + /// [`Cow<'a, T>`]: std::borrow::Cow + /// [`script_get_history`]: https://docs.rs/electrum-client/latest/electrum_client/trait.ElectrumApi.html#tymethod.script_get_history + /// [`determine_changeset`]: Self::determine_changeset + pub fn inflate_update( + &self, + update: SparseChain

, + new_txs: impl IntoIterator, + ) -> Result>, NewError

> { + let mut inflated_graph = TxGraph::default(); + for (_, txid) in update.txids() { + if let Some(tx) = self.graph.get_tx(*txid) { + let _ = inflated_graph.insert_tx(Cow::Borrowed(tx)); + } + } + + for tx in new_txs { + let _ = inflated_graph.insert_tx(Cow::Owned(tx)); + } + + ChainGraph::new(update, inflated_graph) + } + + /// Sets the checkpoint limit. + /// + /// Refer to [`SparseChain::checkpoint_limit`] for more. + pub fn checkpoint_limit(&self) -> Option { + self.chain.checkpoint_limit() + } + + /// Sets the checkpoint limit. + /// + /// Refer to [`SparseChain::set_checkpoint_limit`] for more. + pub fn set_checkpoint_limit(&mut self, limit: Option) { + self.chain.set_checkpoint_limit(limit) + } + + /// Determines the changes required to invalidate checkpoints `from_height` (inclusive) and + /// above. Displaced transactions will have their positions moved to [`TxHeight::Unconfirmed`]. + pub fn invalidate_checkpoints_preview(&self, from_height: u32) -> ChangeSet { + ChangeSet { + chain: self.chain.invalidate_checkpoints_preview(from_height), + ..Default::default() + } + } + + /// Invalidate checkpoints `from_height` (inclusive) and above. Displaced transactions will be + /// re-positioned to [`TxHeight::Unconfirmed`]. + /// + /// This is equivalent to calling [`Self::invalidate_checkpoints_preview`] and + /// [`Self::apply_changeset`] in sequence. + pub fn invalidate_checkpoints(&mut self, from_height: u32) -> ChangeSet + where + ChangeSet: Clone, + { + let changeset = self.invalidate_checkpoints_preview(from_height); + self.apply_changeset(changeset.clone()); + changeset + } + + /// Get a transaction that is currently in the underlying [`SparseChain`]. + /// + /// This does not necessarily mean that it is *confirmed* in the blockchain, it might just be in + /// the unconfirmed transaction list within the [`SparseChain`]. + pub fn get_tx_in_chain(&self, txid: Txid) -> Option<(&P, &T)> { + let position = self.chain.tx_position(txid)?; + let full_tx = self.graph.get_tx(txid).expect("must exist"); + Some((position, full_tx)) + } + + /// Determines the changes required to insert a transaction into the inner [`ChainGraph`] and + /// [`SparseChain`] at the given `position`. + /// + /// If inserting it into the chain `position` will result in conflicts, the returned + /// [`ChangeSet`] should evict conflicting transactions. + pub fn insert_tx_preview(&self, tx: T, pos: P) -> Result, InsertTxError

> { + let mut changeset = ChangeSet { + chain: self.chain.insert_tx_preview(tx.as_tx().txid(), pos)?, + graph: self.graph.insert_tx_preview(tx), + }; + self.fix_conflicts(&mut changeset)?; + Ok(changeset) + } + + /// Inserts [`Transaction`] at given chain position. + /// + /// This is equivalent to calling [`Self::insert_tx_preview`] and [`Self::apply_changeset`] in + /// sequence. + pub fn insert_tx(&mut self, tx: T, pos: P) -> Result, InsertTxError

> { + let changeset = self.insert_tx_preview(tx, pos)?; + self.apply_changeset(changeset.clone()); + Ok(changeset) + } + + /// Determines the changes required to insert a [`TxOut`] into the internal [`TxGraph`]. + pub fn insert_txout_preview(&self, outpoint: OutPoint, txout: TxOut) -> ChangeSet { + ChangeSet { + chain: Default::default(), + graph: self.graph.insert_txout_preview(outpoint, txout), + } + } + + /// Inserts a [`TxOut`] into the internal [`TxGraph`]. + /// + /// This is equivalent to calling [`Self::insert_txout_preview`] and [`Self::apply_changeset`] + /// in sequence. + pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet { + let changeset = self.insert_txout_preview(outpoint, txout); + self.apply_changeset(changeset.clone()); + changeset + } + + /// Determines the changes required to insert a `block_id` (a height and block hash) into the + /// chain. + /// + /// If a checkpoint already exists at that height with a different hash this will return + /// an error. + pub fn insert_checkpoint_preview( + &self, + block_id: BlockId, + ) -> Result, InsertCheckpointError> { + self.chain + .insert_checkpoint_preview(block_id) + .map(|chain_changeset| ChangeSet { + chain: chain_changeset, + ..Default::default() + }) + } + + /// Inserts checkpoint into [`Self`]. + /// + /// This is equivalent to calling [`Self::insert_checkpoint_preview`] and + /// [`Self::apply_changeset`] in sequence. + pub fn insert_checkpoint( + &mut self, + block_id: BlockId, + ) -> Result, InsertCheckpointError> { + let changeset = self.insert_checkpoint_preview(block_id)?; + self.apply_changeset(changeset.clone()); + Ok(changeset) + } + + /// Calculates the difference between self and `update` in the form of a [`ChangeSet`]. + pub fn determine_changeset<'a, T2>( + &self, + update: &ChainGraph, + ) -> Result, UpdateError

> + where + T2: IntoOwned + Clone, + { + let chain_changeset = self + .chain + .determine_changeset(&update.chain) + .map_err(UpdateError::Chain)?; + + let mut changeset = ChangeSet { + chain: chain_changeset, + graph: self.graph.determine_additions(&update.graph), + }; + + self.fix_conflicts(&mut changeset)?; + Ok(changeset) + } + + /// Given a transaction, return an iterator of `txid`s that conflict with it (spends at least + /// one of the same inputs). This includes all descendants of conflicting transactions. + /// + /// This method only returns conflicts that exist in the [`SparseChain`] as transactions that + /// are not included in [`SparseChain`] are already considered as evicted. + pub fn tx_conflicts_in_chain<'a>( + &'a self, + tx: &'a Transaction, + ) -> impl Iterator + 'a { + self.graph.walk_conflicts(tx, |_, conflict_txid| { + self.chain + .tx_position(conflict_txid) + .map(|conflict_pos| (conflict_pos, conflict_txid)) + }) + } + + /// Fix changeset conflicts. + /// + /// **WARNING:** If there are any missing full txs, conflict resolution will not be complete. In + /// debug mode, this will result in panic. + fn fix_conflicts( + &self, + changeset: &mut ChangeSet, + ) -> Result<(), UnresolvableConflict

> { + let chain_conflicts = changeset + .chain + .txids + .iter() + // we want to find new txid additions by the changeset (all txid entries in the + // changeset with Some(position_change)) + .filter_map(|(&txid, pos_change)| pos_change.as_ref().map(|pos| (txid, pos))) + // we don't care about txids that move, only newly added txids + .filter(|&(txid, _)| self.chain.tx_position(txid).is_none()) + // full tx should exist (either in graph, or additions) + .filter_map(|(txid, pos)| { + let full_tx = self + .graph + .get_tx(txid) + .or_else(|| { + changeset + .graph + .tx + .iter() + .find(|tx| tx.as_tx().txid() == txid) + }) + .map(|tx| (txid, tx, pos)); + debug_assert!(full_tx.is_some(), "should have full tx at this point"); + full_tx + }) + .flat_map(|(new_txid, new_tx, new_pos)| { + self.tx_conflicts_in_chain(new_tx.as_tx()).map( + move |(conflict_pos, conflict_txid)| { + (new_pos.clone(), new_txid, conflict_pos, conflict_txid) + }, + ) + }) + .collect::>(); + + for (update_pos, update_txid, conflicting_pos, conflicting_txid) in chain_conflicts { + // We have found a tx that conflicts with our update txid. Only allow this when the + // conflicting tx will be positioned as "unconfirmed" after the update is applied. + // If so, we will modify the changeset to evict the conflicting txid. + + // determine the position of the conflicting txid after current changeset is applied + let conflicting_new_pos = changeset + .chain + .txids + .get(&conflicting_txid) + .map(Option::as_ref) + .unwrap_or(Some(conflicting_pos)); + + match conflicting_new_pos { + None => { + // conflicting txid will be deleted, can ignore + } + Some(existing_new_pos) => match existing_new_pos.height() { + TxHeight::Confirmed(_) => { + // the new postion of the conflicting tx is "confirmed", therefore cannot be + // evicted, return error + return Err(UnresolvableConflict { + already_confirmed_tx: (conflicting_pos.clone(), conflicting_txid), + update_tx: (update_pos.clone(), update_txid), + }); + } + TxHeight::Unconfirmed => { + // the new position of the conflicting tx is "unconfirmed", therefore it can + // be evicted + changeset.chain.txids.insert(conflicting_txid, None); + } + }, + }; + } + + Ok(()) + } + + /// Applies `changeset` to `self`. + /// + /// **Warning** this method assumes the changeset is assumed to be correctly formed. If it isn't + /// then the chain graph may not behave correctly in the future and may panic unexpectedly. + pub fn apply_changeset(&mut self, changeset: ChangeSet) { + self.chain.apply_changeset(changeset.chain); + self.graph.apply_additions(changeset.graph); + } + + /// Applies the `update` chain graph. Note this is shorthand for calling + /// [`Self::determine_changeset()`] and [`Self::apply_changeset()`] in sequence. + pub fn apply_update + Clone>( + &mut self, + update: ChainGraph, + ) -> Result, UpdateError

> { + let changeset = self.determine_changeset(&update)?; + self.apply_changeset(changeset.clone()); + Ok(changeset) + } + + /// Get the full transaction output at an outpoint if it exists in the chain and the graph. + pub fn full_txout(&self, outpoint: OutPoint) -> Option> { + self.chain.full_txout(&self.graph, outpoint) + } + + /// Iterate over the full transactions and their position in the chain ordered by their position + /// in ascending order. + pub fn transactions_in_chain(&self) -> impl DoubleEndedIterator { + self.chain + .txids() + .map(|(pos, txid)| (pos, self.graph.get_tx(*txid).expect("must exist"))) + } + + /// Finds the transaction in the chain that spends `outpoint` given the input/output + /// relationships in `graph`. Note that the transaction including `outpoint` does not need to be + /// in the `graph` or the `chain` for this to return `Some(_)`. + pub fn spent_by(&self, outpoint: OutPoint) -> Option<(&P, Txid)> { + self.chain.spent_by(&self.graph, outpoint) + } + + /// Whether the chain graph contains any data whatsoever. + pub fn is_empty(&self) -> bool { + self.chain.is_empty() && self.graph.is_empty() + } +} + +/// Represents changes to [`ChainGraph`]. +/// +/// This is essentially a combination of [`sparse_chain::ChangeSet`] and [`tx_graph::Additions`]. +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde( + crate = "serde_crate", + bound( + deserialize = "P: serde::Deserialize<'de>, T: Ord + serde::Deserialize<'de>", + serialize = "P: serde::Serialize, T: Ord + serde::Serialize" + ) + ) +)] +#[must_use] +pub struct ChangeSet { + pub chain: sparse_chain::ChangeSet

, + pub graph: tx_graph::Additions, +} + +impl ChangeSet { + /// Returns `true` if this [`ChangeSet`] records no changes. + pub fn is_empty(&self) -> bool { + self.chain.is_empty() && self.graph.is_empty() + } + + /// Returns `true` if this [`ChangeSet`] contains transaction evictions. + pub fn contains_eviction(&self) -> bool { + self.chain + .txids + .iter() + .any(|(_, new_pos)| new_pos.is_none()) + } + + /// Appends the changes in `other` into self such that applying `self` afterwards has the same + /// effect as sequentially applying the original `self` and `other`. + pub fn append(&mut self, other: ChangeSet) + where + P: ChainPosition, + T: Ord, + { + self.chain.append(other.chain); + self.graph.append(other.graph); + } +} + +impl Default for ChangeSet { + fn default() -> Self { + Self { + chain: Default::default(), + graph: Default::default(), + } + } +} + +impl ForEachTxOut for ChainGraph { + fn for_each_txout(&self, f: impl FnMut((OutPoint, &TxOut))) { + self.graph.for_each_txout(f) + } +} + +impl ForEachTxOut for ChangeSet { + fn for_each_txout(&self, f: impl FnMut((OutPoint, &TxOut))) { + self.graph.for_each_txout(f) + } +} + +/// Error that may occur when calling [`ChainGraph::new`]. +#[derive(Clone, Debug, PartialEq)] +pub enum NewError

{ + /// Two transactions within the sparse chain conflicted with each other + Conflict { a: (P, Txid), b: (P, Txid) }, + /// One or more transactions in the chain were not in the graph + Missing(HashSet), +} + +impl core::fmt::Display for NewError

{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + NewError::Conflict { a, b } => write!( + f, + "Unable to inflate sparse chain to chain graph since transactions {:?} and {:?}", + a, b + ), + NewError::Missing(missing) => write!( + f, + "missing full transactions for {}", + missing + .into_iter() + .map(|txid| txid.to_string()) + .collect::>() + .join(", ") + ), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for NewError

{} + +/// Error that may occur when inserting a transaction. +/// +/// Refer to [`ChainGraph::insert_tx_preview`] and [`ChainGraph::insert_tx`]. +#[derive(Clone, Debug, PartialEq)] +pub enum InsertTxError

{ + Chain(sparse_chain::InsertTxError

), + UnresolvableConflict(UnresolvableConflict

), +} + +impl core::fmt::Display for InsertTxError

{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + InsertTxError::Chain(inner) => core::fmt::Display::fmt(inner, f), + InsertTxError::UnresolvableConflict(inner) => core::fmt::Display::fmt(inner, f), + } + } +} + +impl

From> for InsertTxError

{ + fn from(inner: sparse_chain::InsertTxError

) -> Self { + Self::Chain(inner) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for InsertTxError

{} + +/// A nice alias of [`sparse_chain::InsertCheckpointError`]. +pub type InsertCheckpointError = sparse_chain::InsertCheckpointError; + +/// Represents an update failure. +#[derive(Clone, Debug, PartialEq)] +pub enum UpdateError

{ + /// The update chain was inconsistent with the existing chain + Chain(sparse_chain::UpdateError

), + /// A transaction in the update spent the same input as an already confirmed transaction + UnresolvableConflict(UnresolvableConflict

), +} + +impl core::fmt::Display for UpdateError

{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + UpdateError::Chain(inner) => core::fmt::Display::fmt(inner, f), + UpdateError::UnresolvableConflict(inner) => core::fmt::Display::fmt(inner, f), + } + } +} + +impl

From> for UpdateError

{ + fn from(inner: sparse_chain::UpdateError

) -> Self { + Self::Chain(inner) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for UpdateError

{} + +/// Represents an unresolvable conflict between an update's transaction and an +/// already-confirmed transaction. +#[derive(Clone, Debug, PartialEq)] +pub struct UnresolvableConflict

{ + pub already_confirmed_tx: (P, Txid), + pub update_tx: (P, Txid), +} + +impl core::fmt::Display for UnresolvableConflict

{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let Self { + already_confirmed_tx, + update_tx, + } = self; + write!(f, "update transaction {} at height {:?} conflicts with an already confirmed transaction {} at height {:?}", + update_tx.1, update_tx.0, already_confirmed_tx.1, already_confirmed_tx.0) + } +} + +impl

From> for UpdateError

{ + fn from(inner: UnresolvableConflict

) -> Self { + Self::UnresolvableConflict(inner) + } +} + +impl

From> for InsertTxError

{ + fn from(inner: UnresolvableConflict

) -> Self { + Self::UnresolvableConflict(inner) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for UnresolvableConflict

{} diff --git a/crates/chain/src/descriptor_ext.rs b/crates/chain/src/descriptor_ext.rs new file mode 100644 index 00000000..e74928b8 --- /dev/null +++ b/crates/chain/src/descriptor_ext.rs @@ -0,0 +1,16 @@ +use crate::miniscript::{Descriptor, DescriptorPublicKey}; + +/// A trait to extend the functionality of a miniscript descriptor. +pub trait DescriptorExt { + /// Returns the minimum value (in satoshis) that an output should have to be broadcastable. + fn dust_value(&self) -> u64; +} + +impl DescriptorExt for Descriptor { + fn dust_value(&self) -> u64 { + self.at_derivation_index(0) + .script_pubkey() + .dust_value() + .to_sat() + } +} diff --git a/crates/chain/src/example_utils.rs b/crates/chain/src/example_utils.rs new file mode 100644 index 00000000..3aee352b --- /dev/null +++ b/crates/chain/src/example_utils.rs @@ -0,0 +1,30 @@ +#![allow(unused)] +use alloc::vec::Vec; +use bitcoin::{ + consensus, + hashes::{hex::FromHex, Hash}, + Transaction, +}; + +use crate::BlockId; + +pub const RAW_TX_1: &'static str = "0200000000010116d6174da7183d70d0a7d4dc314d517a7d135db79ad63515028b293a76f4f9d10000000000feffffff023a21fc8350060000160014531c405e1881ef192294b8813631e258bf98ea7a1027000000000000225120a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c024730440220591b1a172a122da49ba79a3e79f98aaa03fd7a372f9760da18890b6a327e6010022013e82319231da6c99abf8123d7c07e13cf9bd8d76e113e18dc452e5024db156d012102318a2d558b2936c52e320decd6d92a88d7f530be91b6fe0af5caf41661e77da3ef2e0100"; +pub const RAW_TX_2: &'static str = "02000000000101a688607020cfae91a61e7c516b5ef1264d5d77f17200c3866826c6c808ebf1620000000000feffffff021027000000000000225120a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c20fd48ff530600001600146886c525e41d4522042bd0b159dfbade2504a6bb024730440220740ff7e665cd20565d4296b549df8d26b941be3f1e3af89a0b60e50c0dbeb69a02206213ab7030cf6edc6c90d4ccf33010644261e029950a688dc0b1a9ebe6ddcc5a012102f2ac6b396a97853cb6cd62242c8ae4842024742074475023532a51e9c53194253e760100"; +pub const RAW_TX_3: &'static str = "0200000000010135d67ee47b557e68b8c6223958f597381965ed719f1207ee2b9e20432a24a5dc0100000000feffffff021027000000000000225120a82f29944d65b86ae6b5e5cc75e294ead6c59391a1edc5e016e3498c67fc7bbb62215a5055060000160014070df7671dea67a50c4799a744b5c9be8f4bac690247304402207ebf8d29f71fd03e7e6977b3ea78ca5fcc5c49a42ae822348fc401862fdd766c02201d7e4ff0684ecb008b6142f36ead1b0b4d615524c4f58c261113d361f4427e25012103e6a75e2fab85e5ecad641afc4ffba7222f998649d9f18cac92f0fcc8618883b3ee760100"; +pub const RAW_TX_4: &'static str = "02000000000101d00e8f76ed313e19b339ee293c0f52b0325c95e24c8f3966fa353fb2bedbcf580100000000feffffff021027000000000000225120882d74e5d0572d5a816cef0041a96b6c1de832f6f9676d9605c44d5e9a97d3dc9cda55fe53060000160014852b5864b8edd42fab4060c87f818e50780865ff0247304402201dccbb9bed7fba924b6d249c5837cc9b37470c0e3d8fbea77cb59baba3efe6fa0220700cc170916913b9bfc2bc0fefb6af776e8b542c561702f136cddc1c7aa43141012103acec3fc79dbbca745815c2a807dc4e81010c80e308e84913f59cb42a275dad97f3760100"; + +pub fn tx_from_hex(s: &str) -> Transaction { + let raw = Vec::from_hex(s).expect("data must be in hex"); + consensus::deserialize(raw.as_slice()).expect("must deserialize") +} + +pub fn new_hash(s: &str) -> H { + ::hash(s.as_bytes()) +} + +pub fn new_block_id(height: u32, hash: &str) -> BlockId { + BlockId { + height, + hash: new_hash(hash), + } +} diff --git a/crates/chain/src/keychain.rs b/crates/chain/src/keychain.rs new file mode 100644 index 00000000..41110833 --- /dev/null +++ b/crates/chain/src/keychain.rs @@ -0,0 +1,313 @@ +//! Module for keychain based structures. +//! +//! A keychain here is a set of application defined indexes for a minscript 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 count per keychain. +//! +//! [`KeychainTracker`] combines [`ChainGraph`] and [`KeychainTxOutIndex`] and enforces atomic +//! changes between both these structures. [`KeychainScan`] is a structure used to update to +//! [`KeychainTracker`] and changes made on a [`KeychainTracker`] are reported by +//! [`KeychainChangeSet`]s. +//! +//! [`SpkTxOutIndex`]: crate::SpkTxOutIndex +use crate::{ + chain_graph::{self, ChainGraph}, + collections::BTreeMap, + sparse_chain::ChainPosition, + tx_graph::TxGraph, + AsTransaction, ForEachTxOut, +}; +use bitcoin::Transaction; + +#[cfg(feature = "miniscript")] +pub mod persist; +#[cfg(feature = "miniscript")] +pub use persist::*; +#[cfg(feature = "miniscript")] +mod tracker; +#[cfg(feature = "miniscript")] +pub use tracker::*; +#[cfg(feature = "miniscript")] +mod txout_index; +#[cfg(feature = "miniscript")] +pub use txout_index::*; + +/// Represents updates to the derivation index of a [`KeychainTxOutIndex`]. +/// +/// It can be applied to [`KeychainTxOutIndex`] with [`apply_additions`]. [`DerivationAdditions] are +/// monotone in that they will never decrease the revealed derivation index. +/// +/// [`KeychainTxOutIndex`]: crate::keychain::KeychainTxOutIndex +/// [`apply_additions`]: crate::keychain::KeychainTxOutIndex::apply_additions +#[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 DerivationAdditions(BTreeMap); + +impl DerivationAdditions { + /// Returns whether the additions are empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Get the inner map of keychain to its new derivation index. + pub fn as_inner(&self) -> &BTreeMap { + &self.0 + } +} + +impl DerivationAdditions { + /// Append another [`DerivationAdditions`] into self. + /// + /// If keychain already exists, increases the index when other's index > self's index. + /// If keychain did not exist, append the new keychain. + pub fn append(&mut self, mut other: Self) { + self.0.iter_mut().for_each(|(key, index)| { + if let Some(other_index) = other.0.remove(key) { + *index = other_index.max(*index); + } + }); + + self.0.append(&mut other.0); + } +} + +impl Default for DerivationAdditions { + fn default() -> Self { + Self(Default::default()) + } +} + +impl AsRef> for DerivationAdditions { + fn as_ref(&self) -> &BTreeMap { + &self.0 + } +} + +#[derive(Clone, Debug, PartialEq)] +/// An update that includes the last active indexes of each keychain. +pub struct KeychainScan { + /// The update data in the form of a chain that could be applied + pub update: ChainGraph, + /// The last active indexes of each keychain + pub last_active_indices: BTreeMap, +} + +impl Default for KeychainScan { + fn default() -> Self { + Self { + update: Default::default(), + last_active_indices: Default::default(), + } + } +} + +impl From> for KeychainScan { + fn from(update: ChainGraph) -> Self { + KeychainScan { + update, + last_active_indices: Default::default(), + } + } +} + +/// Represents changes to a [`KeychainTracker`]. +/// +/// This is essentially a combination of [`DerivationAdditions`] and [`chain_graph::ChangeSet`]. +#[derive(Clone, Debug)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde( + crate = "serde_crate", + bound( + deserialize = "K: Ord + serde::Deserialize<'de>, P: serde::Deserialize<'de>, T: Ord + serde::Deserialize<'de>", + serialize = "K: Ord + serde::Serialize, P: serde::Serialize, T: Ord+ serde::Serialize" + ) + ) +)] +#[must_use] +pub struct KeychainChangeSet { + /// The changes in local keychain derivation indices + pub derivation_indices: DerivationAdditions, + /// The changes that have occurred in the blockchain + pub chain_graph: chain_graph::ChangeSet, +} + +impl Default for KeychainChangeSet { + fn default() -> Self { + Self { + chain_graph: Default::default(), + derivation_indices: Default::default(), + } + } +} + +impl KeychainChangeSet { + /// Returns whether the [`KeychainChangeSet`] is empty (no changes recorded). + pub fn is_empty(&self) -> bool { + self.chain_graph.is_empty() && self.derivation_indices.is_empty() + } + + /// Appends the changes in `other` into `self` such that applying `self` afterwards has the same + /// effect as sequentially applying the original `self` and `other`. + /// + /// Note the derivation indices cannot be decreased so `other` will only change the derivation + /// index for a keychain if it's entry is higher than the one in `self`. + pub fn append(&mut self, other: KeychainChangeSet) + where + K: Ord, + P: ChainPosition, + T: Ord, + { + self.derivation_indices.append(other.derivation_indices); + self.chain_graph.append(other.chain_graph); + } +} + +impl From> for KeychainChangeSet { + fn from(changeset: chain_graph::ChangeSet) -> Self { + Self { + chain_graph: changeset, + ..Default::default() + } + } +} + +impl From> for KeychainChangeSet { + fn from(additions: DerivationAdditions) -> Self { + Self { + derivation_indices: additions, + ..Default::default() + } + } +} + +impl AsRef> for KeychainScan { + fn as_ref(&self) -> &TxGraph { + self.update.graph() + } +} + +impl ForEachTxOut for KeychainChangeSet { + fn for_each_txout(&self, f: impl FnMut((bitcoin::OutPoint, &bitcoin::TxOut))) { + self.chain_graph.for_each_txout(f) + } +} + +/// Balance differentiated in 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: u64, + /// Unconfirmed UTXOs generated by a wallet tx + pub trusted_pending: u64, + /// Unconfirmed UTXOs received from an external wallet + pub untrusted_pending: u64, + /// Confirmed and immediately spendable balance + pub confirmed: u64, +} + +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) -> u64 { + self.confirmed + self.trusted_pending + } + + /// Get the whole balance visible to the wallet. + pub fn total(&self) -> u64 { + 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, + } + } +} + +#[cfg(test)] +mod test { + use bitcoin::Transaction; + + use crate::TxHeight; + + use super::*; + #[test] + fn append_keychain_derivation_indices() { + #[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug)] + enum Keychain { + One, + Two, + Three, + Four, + } + let mut lhs_di = BTreeMap::::default(); + let mut rhs_di = BTreeMap::::default(); + lhs_di.insert(Keychain::One, 7); + lhs_di.insert(Keychain::Two, 0); + rhs_di.insert(Keychain::One, 3); + rhs_di.insert(Keychain::Two, 5); + lhs_di.insert(Keychain::Three, 3); + rhs_di.insert(Keychain::Four, 4); + let mut lhs = KeychainChangeSet { + derivation_indices: DerivationAdditions(lhs_di), + chain_graph: chain_graph::ChangeSet::::default(), + }; + + let rhs = KeychainChangeSet { + derivation_indices: DerivationAdditions(rhs_di), + chain_graph: chain_graph::ChangeSet::::default(), + }; + + lhs.append(rhs); + + // Exiting index doesn't update if new index in `other` is lower than `self` + assert_eq!(lhs.derivation_indices.0.get(&Keychain::One), Some(&7)); + // Existing index updates if new index in `other` is higher than `self. + assert_eq!(lhs.derivation_indices.0.get(&Keychain::Two), Some(&5)); + // Existing index unchanged, if keychain doesn't exist in `other` + assert_eq!(lhs.derivation_indices.0.get(&Keychain::Three), Some(&3)); + // New keychain gets added if keychain is in `other`, but not in `self`. + assert_eq!(lhs.derivation_indices.0.get(&Keychain::Four), Some(&4)); + } +} diff --git a/crates/chain/src/keychain/persist.rs b/crates/chain/src/keychain/persist.rs new file mode 100644 index 00000000..8bf68cb1 --- /dev/null +++ b/crates/chain/src/keychain/persist.rs @@ -0,0 +1,105 @@ +//! Persistence for changes made to a [`KeychainTracker`]. +//! +//! BDK's [`KeychainTracker`] needs somewhere to persist changes it makes during operation. +//! Operations like giving out a new address are crucial to persist so that next time the +//! application is loaded it can find transactions related to that address. +//! +//! Note that the [`KeychainTracker`] does not read this persisted data during operation since it +//! always has a copy in memory. +//! +//! [`KeychainTracker`]: crate::keychain::KeychainTracker + +use crate::{keychain, sparse_chain::ChainPosition}; + +/// `Persist` wraps a [`PersistBackend`] to create a convenient staging area for changes before they +/// are persisted. Not all changes made to the [`KeychainTracker`] need to be written to disk right +/// away so you can use [`Persist::stage`] to *stage* it first and then [`Persist::commit`] to +/// finally write it to disk. +/// +/// [`KeychainTracker`]: keychain::KeychainTracker +#[derive(Debug)] +pub struct Persist { + backend: B, + stage: keychain::KeychainChangeSet, +} + +impl> Persist { + /// Create a new `Persist` from a [`PersistBackend`]. + pub fn new(backend: B) -> Self { + Self { + backend, + stage: Default::default(), + } + } + + /// Stage a `changeset` to later persistence with [`commit`]. + /// + /// [`commit`]: Self::commit + pub fn stage(&mut self, changeset: keychain::KeychainChangeSet) + where + K: Ord, + P: ChainPosition, + { + self.stage.append(changeset) + } + + /// Get the changes that haven't been commited yet + pub fn staged(&self) -> &keychain::KeychainChangeSet { + &self.stage + } + + /// Commit the staged changes to the underlying persistence backend. + /// + /// Retuns a backend defined error if this fails + pub fn commit(&mut self) -> Result<(), B::WriteError> { + self.backend.append_changeset(&self.stage)?; + self.stage = Default::default(); + Ok(()) + } +} + +/// A persistence backend for [`Persist`]. +pub trait PersistBackend { + /// The error the backend returns when it fails to write. + type WriteError: core::fmt::Debug; + + /// The error the backend returns when it fails to load. + type LoadError: core::fmt::Debug; + + /// Appends a new changeset to the persistance backend. + /// + /// It is up to the backend what it does with this. It could store every changeset in a list or + /// it insert the actual changes to a more structured database. All it needs to guarantee is + /// that [`load_into_keychain_tracker`] restores a keychain tracker to what it should be if all + /// changesets had been applied sequentially. + /// + /// [`load_into_keychain_tracker`]: Self::load_into_keychain_tracker + fn append_changeset( + &mut self, + changeset: &keychain::KeychainChangeSet, + ) -> Result<(), Self::WriteError>; + + /// Applies all the changesets the backend has received to `tracker`. + fn load_into_keychain_tracker( + &mut self, + tracker: &mut keychain::KeychainTracker, + ) -> Result<(), Self::LoadError>; +} + +impl PersistBackend for () { + type WriteError = (); + type LoadError = (); + + fn append_changeset( + &mut self, + _changeset: &keychain::KeychainChangeSet, + ) -> Result<(), Self::WriteError> { + Ok(()) + } + fn load_into_keychain_tracker( + &mut self, + _tracker: &mut keychain::KeychainTracker, + ) -> Result<(), Self::LoadError> { + Ok(()) + } +} diff --git a/crates/chain/src/keychain/tracker.rs b/crates/chain/src/keychain/tracker.rs new file mode 100644 index 00000000..ba748897 --- /dev/null +++ b/crates/chain/src/keychain/tracker.rs @@ -0,0 +1,315 @@ +use bitcoin::Transaction; +use miniscript::{Descriptor, DescriptorPublicKey}; + +use crate::{ + chain_graph::{self, ChainGraph}, + collections::*, + keychain::{KeychainChangeSet, KeychainScan, KeychainTxOutIndex}, + sparse_chain::{self, SparseChain}, + tx_graph::TxGraph, + AsTransaction, BlockId, FullTxOut, IntoOwned, TxHeight, +}; + +use super::{Balance, DerivationAdditions}; + +/// A convenient combination of a [`KeychainTxOutIndex`] and a [`ChainGraph`]. +/// +/// The [`KeychainTracker`] atomically updates its [`KeychainTxOutIndex`] whenever new chain data is +/// incorporated into its internal [`ChainGraph`]. +#[derive(Clone, Debug)] +pub struct KeychainTracker { + /// Index between script pubkeys to transaction outputs + pub txout_index: KeychainTxOutIndex, + chain_graph: ChainGraph, +} + +impl KeychainTracker +where + P: sparse_chain::ChainPosition, + K: Ord + Clone + core::fmt::Debug, + T: AsTransaction + Clone + Ord, +{ + /// Add a keychain to the tracker's `txout_index` with a descriptor to derive addresses for it. + /// This is just shorthand for calling [`KeychainTxOutIndex::add_keychain`] on the internal + /// `txout_index`. + /// + /// Adding a keychain means you will be able to derive new script pubkeys under that keychain + /// and the tracker will discover transaction outputs with those script pubkeys. + pub fn add_keychain(&mut self, keychain: K, descriptor: Descriptor) { + self.txout_index.add_keychain(keychain, descriptor) + } + + /// Get the internal map of keychains to their descriptors. This is just shorthand for calling + /// [`KeychainTxOutIndex::keychains`] on the internal `txout_index`. + pub fn keychains(&mut self) -> &BTreeMap> { + self.txout_index.keychains() + } + + /// Get the checkpoint limit of the internal [`SparseChain`]. + /// + /// Refer to [`SparseChain::checkpoint_limit`] for more. + pub fn checkpoint_limit(&self) -> Option { + self.chain_graph.checkpoint_limit() + } + + /// Set the checkpoint limit of the internal [`SparseChain`]. + /// + /// Refer to [`SparseChain::set_checkpoint_limit`] for more. + pub fn set_checkpoint_limit(&mut self, limit: Option) { + self.chain_graph.set_checkpoint_limit(limit) + } + + /// Determines the resultant [`KeychainChangeSet`] if the given [`KeychainScan`] is applied. + /// + /// Internally, we call [`ChainGraph::determine_changeset`] and also determine the additions of + /// [`KeychainTxOutIndex`]. + pub fn determine_changeset( + &self, + scan: &KeychainScan, + ) -> Result, chain_graph::UpdateError

> + where + T2: IntoOwned + Clone, + { + // TODO: `KeychainTxOutIndex::determine_additions` + let mut derivation_indices = scan.last_active_indices.clone(); + derivation_indices.retain(|keychain, index| { + match self.txout_index.last_revealed_index(keychain) { + Some(existing) => *index > existing, + None => true, + } + }); + + Ok(KeychainChangeSet { + derivation_indices: DerivationAdditions(derivation_indices), + chain_graph: self.chain_graph.determine_changeset(&scan.update)?, + }) + } + + /// Directly applies a [`KeychainScan`] on [`KeychainTracker`]. + /// + /// This is equivilant to calling [`determine_changeset`] and [`apply_changeset`] in sequence. + /// + /// [`determine_changeset`]: Self::determine_changeset + /// [`apply_changeset`]: Self::apply_changeset + pub fn apply_update( + &mut self, + scan: KeychainScan, + ) -> Result, chain_graph::UpdateError

> + where + T2: IntoOwned + Clone, + { + let changeset = self.determine_changeset(&scan)?; + self.apply_changeset(changeset.clone()); + Ok(changeset) + } + + /// Applies the changes in `changeset` to [`KeychainTracker`]. + /// + /// Internally, this calls [`KeychainTxOutIndex::apply_additions`] and + /// [`ChainGraph::apply_changeset`] in sequence. + pub fn apply_changeset(&mut self, changeset: KeychainChangeSet) { + let KeychainChangeSet { + derivation_indices, + chain_graph, + } = changeset; + self.txout_index.apply_additions(derivation_indices); + let _ = self.txout_index.scan(&chain_graph); + self.chain_graph.apply_changeset(chain_graph) + } + + /// Iterates through [`FullTxOut`]s that are considered to exist in our representation of the + /// blockchain/mempool. + /// + /// In other words, these are `txout`s of confirmed and in-mempool transactions, based on our + /// view of the blockchain/mempool. + pub fn full_txouts(&self) -> impl Iterator)> + '_ { + self.txout_index + .txouts() + .filter_map(|(spk_i, op, _)| Some((spk_i, self.chain_graph.full_txout(op)?))) + } + + /// Iterates through [`FullTxOut`]s that are unspent outputs. + /// + /// Refer to [`full_txouts`] for more. + /// + /// [`full_txouts`]: Self::full_txouts + pub fn full_utxos(&self) -> impl Iterator)> + '_ { + self.full_txouts() + .filter(|(_, txout)| txout.spent_by.is_none()) + } + + /// Returns a reference to the internal [`ChainGraph`]. + pub fn chain_graph(&self) -> &ChainGraph { + &self.chain_graph + } + + /// Returns a reference to the internal [`TxGraph`] (which is part of the [`ChainGraph`]). + pub fn graph(&self) -> &TxGraph { + &self.chain_graph().graph() + } + + /// Returns a reference to the internal [`SparseChain`] (which is part of the [`ChainGraph`]). + pub fn chain(&self) -> &SparseChain

{ + &self.chain_graph().chain() + } + + /// Determines the changes as result of inserting `block_id` (a height and block hash) into the + /// tracker. + /// + /// The caller is responsible for guaranteeing that a block exists at that height. If a + /// checkpoint already exists at that height with a different hash this will return an error. + /// Otherwise it will return `Ok(true)` if the checkpoint didn't already exist or `Ok(false)` + /// if it did. + /// + /// **Warning**: This function modifies the internal state of the tracker. You are responsible + /// for persisting these changes to disk if you need to restore them. + pub fn insert_checkpoint_preview( + &self, + block_id: BlockId, + ) -> Result, chain_graph::InsertCheckpointError> { + Ok(KeychainChangeSet { + chain_graph: self.chain_graph.insert_checkpoint_preview(block_id)?, + ..Default::default() + }) + } + + /// Directly insert a `block_id` into the tracker. + /// + /// This is equivalent of calling [`insert_checkpoint_preview`] and [`apply_changeset`] in + /// sequence. + /// + /// [`insert_checkpoint_preview`]: Self::insert_checkpoint_preview + /// [`apply_changeset`]: Self::apply_changeset + pub fn insert_checkpoint( + &mut self, + block_id: BlockId, + ) -> Result, chain_graph::InsertCheckpointError> { + let changeset = self.insert_checkpoint_preview(block_id)?; + self.apply_changeset(changeset.clone()); + Ok(changeset) + } + + /// Determines the changes as result of inserting a transaction into the inner [`ChainGraph`] + /// and optionally into the inner chain at `position`. + /// + /// **Warning**: This function modifies the internal state of the chain graph. You are + /// responsible for persisting these changes to disk if you need to restore them. + pub fn insert_tx_preview( + &self, + tx: T, + pos: P, + ) -> Result, chain_graph::InsertTxError

> { + Ok(KeychainChangeSet { + chain_graph: self.chain_graph.insert_tx_preview(tx, pos)?, + ..Default::default() + }) + } + + /// Directly insert a transaction into the inner [`ChainGraph`] and optionally into the inner + /// chain at `position`. + /// + /// This is equivilant of calling [`insert_tx_preview`] and [`apply_changeset`] in sequence. + /// + /// [`insert_tx_preview`]: Self::insert_tx_preview + /// [`apply_changeset`]: Self::apply_changeset + pub fn insert_tx( + &mut self, + tx: T, + pos: P, + ) -> Result, chain_graph::InsertTxError

> { + let changeset = self.insert_tx_preview(tx, pos)?; + self.apply_changeset(changeset.clone()); + Ok(changeset) + } + + /// Returns the *balance* of the keychain i.e. the value of unspent transaction outputs tracked. + /// + /// The caller provides a `should_trust` predicate which must decide whether the value of + /// unconfirmed outputs on this keychain are guaranteed to be realized or not. For example: + /// + /// - For an *internal* (change) keychain `should_trust` should in general be `true` since even if + /// you lose an internal output due to eviction you will always gain back the value from whatever output the + /// unconfirmed transaction was spending (since that output is presumeably from your wallet). + /// - For an *external* keychain you might want `should_trust` to return `false` since someone may cancel (by double spending) + /// a payment made to addresses on that keychain. + /// + /// When in doubt set `should_trust` to return false. This doesn't do anything other than change + /// where the unconfirmed output's value is accounted for in `Balance`. + pub fn balance(&self, mut should_trust: impl FnMut(&K) -> bool) -> Balance { + let mut immature = 0; + let mut trusted_pending = 0; + let mut untrusted_pending = 0; + let mut confirmed = 0; + let last_sync_height = self.chain().latest_checkpoint().map(|latest| latest.height); + for ((keychain, _), utxo) in self.full_utxos() { + let chain_position = &utxo.chain_position; + + match chain_position.height() { + TxHeight::Confirmed(_) => { + if utxo.is_on_coinbase { + if utxo.is_mature( + last_sync_height + .expect("since it's confirmed we must have a checkpoint"), + ) { + confirmed += utxo.txout.value; + } else { + immature += utxo.txout.value; + } + } else { + confirmed += utxo.txout.value; + } + } + TxHeight::Unconfirmed => { + if should_trust(keychain) { + trusted_pending += utxo.txout.value; + } else { + untrusted_pending += utxo.txout.value; + } + } + } + } + + Balance { + immature, + trusted_pending, + untrusted_pending, + confirmed, + } + } + + /// Returns the balance of all spendable confirmed unspent outputs of this tracker at a + /// particular height. + pub fn balance_at(&self, height: u32) -> u64 { + self.full_txouts() + .filter(|(_, full_txout)| full_txout.is_spendable_at(height)) + .map(|(_, full_txout)| full_txout.txout.value) + .sum() + } +} + +impl Default for KeychainTracker { + fn default() -> Self { + Self { + txout_index: Default::default(), + chain_graph: Default::default(), + } + } +} + +impl AsRef> for KeychainTracker { + fn as_ref(&self) -> &SparseChain

{ + self.chain_graph.chain() + } +} + +impl AsRef for KeychainTracker { + fn as_ref(&self) -> &TxGraph { + self.chain_graph.graph() + } +} + +impl AsRef> for KeychainTracker { + fn as_ref(&self) -> &ChainGraph

{ + &self.chain_graph + } +} diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs new file mode 100644 index 00000000..547957d8 --- /dev/null +++ b/crates/chain/src/keychain/txout_index.rs @@ -0,0 +1,585 @@ +use crate::{ + collections::*, + miniscript::{Descriptor, DescriptorPublicKey}, + ForEachTxOut, SpkTxOutIndex, +}; +use alloc::{borrow::Cow, vec::Vec}; +use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, TxOut}; +use core::{fmt::Debug, ops::Deref}; + +use super::DerivationAdditions; + +/// Maximum [BIP32](https://bips.xyz/32) derivation index. +pub const BIP32_MAX_INDEX: u32 = 1 << 31 - 1; + +/// A convenient wrapper around [`SpkTxOutIndex`] that relates script pubkeys to miniscript public +/// [`Descriptor`]s. +/// +/// Descriptors are referenced by the provided keychain generic (`K`). +/// +/// Script pubkeys for a descriptor are revealed chronologically from index 0. I.e. If the last +/// revealed index of a descriptor is 5, scripts of indices 0 to 4 are guaranteed to already be +/// revealed. In addition to revealed scripts, we have a `lookahead` parameter for each keychain +/// which defines the number of script pubkeys to store ahead of the last revealed index. +/// +/// Methods that could update the last revealed index will return [`DerivationAdditions`] to report +/// these changes. This can 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_for_user_42 = external_descriptor.clone(); +/// txout_index.add_keychain(MyKeychain::External, external_descriptor); +/// txout_index.add_keychain(MyKeychain::Internal, internal_descriptor); +/// txout_index.add_keychain(MyKeychain::MyAppUser { user_id: 42 }, descriptor_for_user_42); +/// +/// let new_spk_for_user = txout_index.reveal_next_spk(&MyKeychain::MyAppUser{ user_id: 42 }); +/// ``` +/// +/// [`Ord`]: core::cmp::Ord +/// [`SpkTxOutIndex`]: crate::spk_txout_index::SpkTxOutIndex +/// [`Descriptor`]: crate::miniscript::Descriptor +#[derive(Clone, Debug)] +pub struct KeychainTxOutIndex { + inner: SpkTxOutIndex<(K, u32)>, + // descriptors of each keychain + keychains: BTreeMap>, + // last stored indexes + last_revealed: BTreeMap, + // lookahead settings for each keychain + lookahead: BTreeMap, +} + +impl Default for KeychainTxOutIndex { + fn default() -> Self { + Self { + inner: SpkTxOutIndex::default(), + keychains: BTreeMap::default(), + last_revealed: BTreeMap::default(), + lookahead: BTreeMap::default(), + } + } +} + +impl Deref for KeychainTxOutIndex { + type Target = SpkTxOutIndex<(K, u32)>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl KeychainTxOutIndex { + /// Scans an object for relevant outpoints, which are stored and indexed internally. + /// + /// If the matched script pubkey is part of the lookahead, the last stored index is updated for + /// the script pubkey's keychain and the [`DerivationAdditions`] returned will reflect the + /// change. + /// + /// Typically this method is used in two situations: + /// + /// 1. After loading transaction data from 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 (i.e. `SparseChain`, `ChainGraph`). + /// + /// See [`ForEachTxout`] for the types that support this. + /// + /// [`ForEachTxout`]: crate::ForEachTxOut + pub fn scan(&mut self, txouts: &impl ForEachTxOut) -> DerivationAdditions { + let mut additions = DerivationAdditions::::default(); + txouts.for_each_txout(|(op, txout)| additions.append(self.scan_txout(op, txout))); + additions + } + + /// Scan a single outpoint for a matching script pubkey. + /// + /// If it matches the index will store and index it. + pub fn scan_txout(&mut self, op: OutPoint, txout: &TxOut) -> DerivationAdditions { + match self.inner.scan_txout(op, txout).cloned() { + Some((keychain, index)) => self.reveal_to_target(&keychain, index).1, + None => DerivationAdditions::default(), + } + } + + /// Return a reference to the internal [`SpkTxOutIndex`]. + pub fn inner(&self) -> &SpkTxOutIndex<(K, u32)> { + &self.inner + } + + /// Return a reference to the internal map of keychain to descriptors. + pub fn keychains(&self) -> &BTreeMap> { + &self.keychains + } + + /// Add a keychain to the tracker's `txout_index` with a descriptor to derive addresses for it. + /// + /// Adding a keychain means you will be able to derive new script pubkeys under that keychain + /// and the txout index will discover transaction outputs with those script pubkeys. + /// + /// # Panics + /// + /// This will panic if a different `descriptor` is introduced to the same `keychain`. + pub fn add_keychain(&mut self, keychain: K, descriptor: Descriptor) { + let old_descriptor = &*self.keychains.entry(keychain).or_insert(descriptor.clone()); + assert_eq!( + &descriptor, old_descriptor, + "keychain already contains a different descriptor" + ); + } + + /// Return the lookahead setting for each keychain. + /// + /// Refer to [`set_lookahead`] for a deeper explanation on `lookahead`. + /// + /// [`set_lookahead`]: Self::set_lookahead + pub fn lookaheads(&self) -> &BTreeMap { + &self.lookahead + } + + /// Convenience method to call [`set_lookahead`] for all keychains. + /// + /// [`set_lookahead`]: Self::set_lookahead + pub fn set_lookahead_for_all(&mut self, lookahead: u32) { + for keychain in &self.keychains.keys().cloned().collect::>() { + self.lookahead.insert(keychain.clone(), lookahead); + self.replenish_lookahead(&keychain); + } + } + + /// Set the lookahead count for `keychain`. + /// + /// The lookahead is the number of scripts to cache ahead of the last stored script index. This + /// is useful during a scan via [`scan`] or [`scan_txout`]. + /// + /// # Panics + /// + /// This will panic if `keychain` does not exist. + /// + /// [`scan`]: Self::scan + /// [`scan_txout`]: Self::scan_txout + pub fn set_lookahead(&mut self, keychain: &K, lookahead: u32) { + self.lookahead.insert(keychain.clone(), lookahead); + self.replenish_lookahead(keychain); + } + + /// Convenience method to call [`lookahead_to_target`] for multiple keychains. + /// + /// [`lookahead_to_target`]: Self::lookahead_to_target + pub fn lookahead_to_target_multi(&mut self, target_indexes: BTreeMap) { + for (keychain, target_index) in target_indexes { + self.lookahead_to_target(&keychain, target_index) + } + } + + /// Store lookahead scripts until `target_index`. + /// + /// This does not change the `lookahead` setting. + pub fn lookahead_to_target(&mut self, keychain: &K, target_index: u32) { + let next_index = self.next_store_index(keychain); + if let Some(temp_lookahead) = target_index.checked_sub(next_index).filter(|&v| v > 0) { + let old_lookahead = self.lookahead.insert(keychain.clone(), temp_lookahead); + self.replenish_lookahead(keychain); + + // revert + match old_lookahead { + Some(lookahead) => self.lookahead.insert(keychain.clone(), lookahead), + None => self.lookahead.remove(keychain), + }; + } + } + + fn replenish_lookahead(&mut self, keychain: &K) { + let descriptor = self.keychains.get(keychain).expect("keychain must exist"); + let next_store_index = self.next_store_index(keychain); + let next_reveal_index = self.last_revealed.get(keychain).map_or(0, |v| *v + 1); + let lookahead = self.lookahead.get(keychain).map_or(0, |v| *v); + + for (new_index, new_spk) in range_descriptor_spks( + Cow::Borrowed(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); + } + } + + fn next_store_index(&self, keychain: &K) -> u32 { + self.inner() + .all_spks() + .range((keychain.clone(), u32::MIN)..(keychain.clone(), u32::MAX)) + .last() + .map_or(0, |((_, v), _)| *v + 1) + } + + /// Generates script pubkey iterators for every `keychain`. The iterators iterate over all + /// derivable script pubkeys. + pub fn spks_of_all_keychains( + &self, + ) -> BTreeMap + Clone> { + self.keychains + .iter() + .map(|(keychain, descriptor)| { + ( + keychain.clone(), + range_descriptor_spks(Cow::Owned(descriptor.clone()), 0..), + ) + }) + .collect() + } + + /// Generates a script pubkey iterator for the given `keychain`'s descriptor (if exists). The + /// iterator iterates over all derivable scripts of the keychain's descriptor. + /// + /// # Panics + /// + /// This will panic if `keychain` does not exist. + pub fn spks_of_keychain(&self, keychain: &K) -> impl Iterator + Clone { + let descriptor = self + .keychains + .get(keychain) + .expect("keychain must exist") + .clone(); + range_descriptor_spks(Cow::Owned(descriptor), 0..) + } + + /// Convenience method to get [`revealed_spks_of_keychain`] of all keychains. + /// + /// [`revealed_spks_of_keychain`]: Self::revealed_spks_of_keychain + pub fn revealed_spks_of_all_keychains( + &self, + ) -> BTreeMap + Clone> { + self.keychains + .keys() + .map(|keychain| (keychain.clone(), self.revealed_spks_of_keychain(keychain))) + .collect() + } + + /// Iterates over the script pubkeys revealed by this index under `keychain`. + pub fn revealed_spks_of_keychain( + &self, + keychain: &K, + ) -> impl DoubleEndedIterator + Clone { + let next_index = self.last_revealed.get(keychain).map_or(0, |v| *v + 1); + self.inner + .all_spks() + .range((keychain.clone(), u32::MIN)..(keychain.clone(), next_index)) + .map(|((_, derivation_index), spk)| (*derivation_index, spk)) + } + + /// Get the next derivation index for `keychain`. This 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. + /// + /// # Panics + /// + /// Panics if the `keychain` does not exist. + pub fn next_index(&self, keychain: &K) -> (u32, bool) { + let descriptor = self.keychains.get(keychain).expect("keychain must exist"); + let last_index = self.last_revealed.get(keychain).cloned(); + + // we can only get the next index if wildcard exists + let has_wildcard = descriptor.has_wildcard(); + + 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 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 + } + + /// Get the last derivation index revealed for `keychain`. + pub fn last_revealed_index(&self, keychain: &K) -> Option { + self.last_revealed.get(keychain).cloned() + } + + /// Convenience method to call [`Self::reveal_to_target`] on multiple keychains. + pub fn reveal_to_target_multi( + &mut self, + keychains: &BTreeMap, + ) -> ( + BTreeMap>, + DerivationAdditions, + ) { + let mut additions = DerivationAdditions::default(); + let mut spks = BTreeMap::new(); + + for (keychain, &index) in keychains { + let (new_spks, new_additions) = self.reveal_to_target(&keychain, index); + if !new_additions.is_empty() { + spks.insert(keychain.clone(), new_spks); + additions.append(new_additions); + } + } + + (spks, additions) + } + + /// 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 do a best-effort and + /// reveal up to the last possible index. + /// + /// This returns an iterator of newly revealed indices (along side their scripts), and a + /// [`DerivationAdditions`] which reports updates to the latest revealed index. If no new script + /// pubkeys are revealed, both of these will be empty. + /// + /// # Panics + /// + /// Panics if `keychain` does not exist. + pub fn reveal_to_target( + &mut self, + keychain: &K, + target_index: u32, + ) -> (impl Iterator, DerivationAdditions) { + let descriptor = self.keychains.get(keychain).expect("keychain must exist"); + let has_wildcard = descriptor.has_wildcard(); + + let target_index = if has_wildcard { target_index } else { 0 }; + let next_store_index = self.next_store_index(keychain); + let next_reveal_index = self.last_revealed.get(keychain).map_or(0, |v| *v + 1); + let lookahead = self.lookahead.get(keychain).map_or(0, |v| *v); + + // if we are able to reveal new indexes, the latest revealed index goes here + let mut revealed_index = None; + + // if target is already surpassed, we have nothing to reveal + if next_reveal_index <= target_index + // if target is already stored (due to lookahead), this can be our new revealed index + && target_index < next_reveal_index + lookahead + { + revealed_index = Some(target_index); + } + + // we range over indexes that are not stored + let range = next_reveal_index + lookahead..=target_index + lookahead; + + for (new_index, new_spk) in range_descriptor_spks(Cow::Borrowed(descriptor), range) { + // no need to store if already stored + if new_index >= next_store_index { + let _inserted = self + .inner + .insert_spk((keychain.clone(), new_index), new_spk); + debug_assert!(_inserted, "must not have existing spk",); + } + + // everything after `target_index` is stored for lookahead only + if new_index <= target_index { + revealed_index = Some(new_index); + } + } + + match revealed_index { + Some(index) => { + let _old_index = self.last_revealed.insert(keychain.clone(), index); + debug_assert!(_old_index < Some(index)); + ( + range_descriptor_spks( + Cow::Owned(descriptor.clone()), + next_reveal_index..index + 1, + ), + DerivationAdditions([(keychain.clone(), index)].into()), + ) + } + None => ( + range_descriptor_spks( + Cow::Owned(descriptor.clone()), + next_reveal_index..next_reveal_index, + ), + DerivationAdditions::default(), + ), + } + } + + /// Attempts to reveal the next script pubkey for `keychain`. + /// + /// Returns the derivation index of the revealed script pubkey, the revealed script pubkey and a + /// [`DerivationAdditions`] which represents changes in the last revealed index (if any). + /// + /// When a new script cannot be revealed, we return the last revealed script and an empty + /// [`DerivationAdditions`]. 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. + /// + /// # Panics + /// + /// Panics if the `keychain` does not exist. + pub fn reveal_next_spk(&mut self, keychain: &K) -> ((u32, &Script), DerivationAdditions) { + let (next_index, _) = self.next_index(keychain); + let additions = self.reveal_to_target(keychain, next_index).1; + let script = self + .inner + .spk_at_index(&(keychain.clone(), next_index)) + .expect("script must already be stored"); + ((next_index, script), additions) + } + + /// 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, the last derived script pubkey will be + /// returned. + /// + /// # Panics + /// + /// Panics if `keychain` has never been added to the index + pub fn next_unused_spk(&mut self, keychain: &K) -> ((u32, &Script), DerivationAdditions) { + let need_new = self.unused_spks_of_keychain(keychain).next().is_none(); + // this rather strange branch is needed because of some lifetime issues + if need_new { + self.reveal_next_spk(keychain) + } else { + ( + self.unused_spks_of_keychain(keychain) + .next() + .expect("we already know next exists"), + DerivationAdditions::default(), + ) + } + } + + /// Marks the script pubkey at `index` as used even though it 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 `index` was originally 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 `index` on + /// `keychain` used until you call [`unmark_used`]. + /// + /// [`unmark_used`]: Self::unmark_used + pub fn mark_used(&mut self, keychain: &K, index: u32) -> bool { + self.inner.mark_used(&(keychain.clone(), 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, keychain: &K, index: u32) -> bool { + self.inner.unmark_used(&(keychain.clone(), index)) + } + + /// Iterates over all unused script pubkeys for a `keychain` that have been stored in the index. + pub fn unused_spks_of_keychain( + &self, + keychain: &K, + ) -> impl DoubleEndedIterator { + let next_index = self.last_revealed.get(keychain).map_or(0, |&v| v + 1); + let range = (keychain.clone(), u32::MIN)..(keychain.clone(), next_index); + self.inner + .unused_spks(range) + .map(|((_, i), script)| (*i, script)) + } + + /// Iterates over all the [`OutPoint`] that have a `TxOut` with a script pubkey derived from + /// `keychain`. + pub fn txouts_of_keychain( + &self, + keychain: &K, + ) -> impl DoubleEndedIterator + '_ { + self.inner + .outputs_in_range((keychain.clone(), u32::MIN)..(keychain.clone(), u32::MAX)) + .map(|((_, i), op)| (*i, op)) + } + + /// 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.txouts_of_keychain(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.keychains + .iter() + .filter_map(|(keychain, _)| { + self.last_used_index(keychain) + .map(|index| (keychain.clone(), index)) + }) + .collect() + } + + /// Applies the derivation additions to the [`KeychainTxOutIndex`], extending the number of + /// derived scripts per keychain, as specified in the `additions`. + pub fn apply_additions(&mut self, additions: DerivationAdditions) { + let _ = self.reveal_to_target_multi(&additions.0); + } +} + +fn range_descriptor_spks<'a, R>( + descriptor: Cow<'a, Descriptor>, + range: R, +) -> impl Iterator + Clone + Send + 'a +where + R: Iterator + Clone + Send + 'a, +{ + let secp = Secp256k1::verification_only(); + let has_wildcard = descriptor.has_wildcard(); + range + .into_iter() + // non-wildcard descriptors can only have one derivation index (0) + .take_while(move |&index| has_wildcard || index == 0) + // we can only iterate over non-hardened indices + .take_while(|&index| index <= BIP32_MAX_INDEX) + // take until failure + .map_while(move |index| { + descriptor + .derived_descriptor(&secp, index) + .map(|desc| (index, desc.script_pubkey())) + .ok() + }) +} diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs new file mode 100644 index 00000000..7bb4ed0d --- /dev/null +++ b/crates/chain/src/lib.rs @@ -0,0 +1,89 @@ +//! This crate is a collection of core structures for [Bitcoin Dev Kit] (alpha release). +//! +//! The goal of this crate is give wallets the mechanisms needed to: +//! +//! 1. Figure out what data they need to fetch. +//! 2. Process that data in a way that never leads to inconsistent states. +//! 3. Fully index that data and expose it so that it can be consumed without friction. +//! +//! Our design goals for these mechanisms are: +//! +//! 1. Data source agnostic -- nothing in `bdk_chain` cares about where you get data from or whether +//! you do it synchronously or asynchronously. If you know a fact about the blockchain you can just +//! tell `bdk_chain`'s APIs about it and that information will be integrated if it can be done +//! consistently. +//! 2. Error free APIs. +//! 3. Data persistence agnostic -- `bdk_chain` does not care where you cache on-chain data, what you +//! cache or how you fetch it. +//! +//! [Bitcoin Dev Kit]: https://bitcoindevkit.org/ +#![no_std] +pub use bitcoin; +pub mod chain_graph; +mod spk_txout_index; +pub use spk_txout_index::*; +mod chain_data; +pub use chain_data::*; +pub mod keychain; +pub mod sparse_chain; +mod tx_data_traits; +pub mod tx_graph; +pub use tx_data_traits::*; + +#[doc(hidden)] +pub mod example_utils; + +#[cfg(feature = "miniscript")] +pub use miniscript; +#[cfg(feature = "miniscript")] +mod descriptor_ext; +#[cfg(feature = "miniscript")] +pub use descriptor_ext::DescriptorExt; + +#[allow(unused_imports)] +#[macro_use] +extern crate alloc; + +#[cfg(feature = "serde")] +pub extern crate serde_crate as serde; + +#[cfg(feature = "bincode")] +extern crate bincode; + +#[cfg(feature = "std")] +#[macro_use] +extern crate std; + +#[cfg(all(not(feature = "std"), feature = "hashbrown"))] +extern crate hashbrown; + +// When no-std use `alloc`'s Hash collections. This is activated by default +#[cfg(all(not(feature = "std"), not(feature = "hashbrown")))] +#[doc(hidden)] +pub mod collections { + #![allow(dead_code)] + pub type HashSet = alloc::collections::BTreeSet; + pub type HashMap = alloc::collections::BTreeMap; + pub use alloc::collections::{btree_map as hash_map, *}; +} + +// When we have std use `std`'s all collections +#[cfg(all(feature = "std", not(feature = "hashbrown")))] +#[doc(hidden)] +pub mod collections { + pub use std::collections::{hash_map, *}; +} + +// With special feature `hashbrown` use `hashbrown`'s hash collections, and else from `alloc`. +#[cfg(feature = "hashbrown")] +#[doc(hidden)] +pub mod collections { + #![allow(dead_code)] + pub type HashSet = hashbrown::HashSet; + pub type HashMap = hashbrown::HashMap; + pub use alloc::collections::*; + pub use hashbrown::hash_map; +} + +/// How many confirmations are needed for a coinbase output to be spent +pub const COINBASE_MATURITY: u32 = 100; diff --git a/crates/chain/src/sparse_chain.rs b/crates/chain/src/sparse_chain.rs new file mode 100644 index 00000000..17f1eb6e --- /dev/null +++ b/crates/chain/src/sparse_chain.rs @@ -0,0 +1,1008 @@ +//! Module for structures that maintain sparse (purposely incomplete) snapshots of blockchain data. +//! +//! [`SparseChain`] stores [`Txid`]s ordered by an index that implements [`ChainPosition`] (this +//! represents the transaction's position in the blockchain, by default [`TxHeight`] is used). +//! [`SparseChain`] also contains "checkpoints" which relate block height to block hash. Changes to +//! a [`SparseChain`] are reported by returning [`ChangeSet`]s. +//! +//! # Updating [`SparseChain`] +//! +//! A sparsechain can be thought of as a consistent snapshot of history. A [`SparseChain`] can be +//! updated by applying an update [`SparseChain`] on top, but only if they "connect" via their +//! checkpoints and don't result in unexpected movements of transactions. +//! +//! ``` +//! # use bdk_chain::{BlockId, TxHeight, sparse_chain::*, example_utils::*}; +//! # use bitcoin::BlockHash; +//! # let hash_a = new_hash::("a"); +//! # let hash_b = new_hash::("b"); +//! # let hash_c = new_hash::("c"); +//! # let hash_d = new_hash::("d"); +//! // create empty sparsechain +//! let mut chain = SparseChain::::default(); +//! +//! /* Updating an empty sparsechain will always succeed */ +//! +//! let update = SparseChain::from_checkpoints(vec![ +//! BlockId { height: 1, hash: hash_a }, +//! BlockId { height: 2, hash: hash_b }, +//! ]); +//! let _ = chain +//! .apply_update(update) +//! .expect("updating an empty sparsechain will always succeed"); +//! +//! /* To update a non-empty sparsechain, the update must connect */ +//! +//! let update = SparseChain::from_checkpoints(vec![ +//! BlockId { height: 2, hash: hash_b }, +//! BlockId { height: 3, hash: hash_c }, +//! ]); +//! let _ = chain +//! .apply_update(update) +//! .expect("we have connected at block height 2, so this must succeed"); +//! ``` +//! +//! ## Invalid updates +//! +//! As shown above, sparsechains can be "connected" by comparing their checkpoints. However, there +//! are situations where two sparsechains cannot connect in a way that guarantees consistency. +//! +//! ``` +//! # use bdk_chain::{BlockId, TxHeight, sparse_chain::*, example_utils::*}; +//! # use bitcoin::BlockHash; +//! # let hash_a = new_hash::("a"); +//! # let hash_b = new_hash::("b"); +//! # let hash_c = new_hash::("c"); +//! # let hash_d = new_hash::("d"); +//! // our sparsechain has 2 checkpoints +//! let chain = SparseChain::::from_checkpoints(vec![ +//! BlockId { height: 1, hash: hash_a }, +//! BlockId { height: 2, hash: hash_b }, +//! ]); +//! +//! /* Example of an ambiguous update that does not fully connect */ +//! +//! let ambiguous_update = SparseChain::from_checkpoints(vec![ +//! // the update sort of "connects" at checkpoint 1, but... +//! BlockId { height: 1, hash: hash_a }, +//! // we cannot determine whether checkpoint 3 connects with checkpoint 2 +//! BlockId { height: 3, hash: hash_c }, +//! ]); +//! let _ = chain +//! .determine_changeset(&ambiguous_update) +//! .expect_err("cannot apply ambiguous update"); +//! +//! /* Example of an update that completely misses the point */ +//! +//! let disconnected_update = SparseChain::from_checkpoints(vec![ +//! // the last checkpoint in chain is 2, so 3 and 4 do not connect +//! BlockId { height: 3, hash: hash_c }, +//! BlockId { height: 4, hash: hash_d }, +//! ]); +//! let _ = chain +//! .determine_changeset(&disconnected_update) +//! .expect_err("cannot apply a totally-disconnected update"); +//! ``` +//! +//! ## Handling reorgs +//! +//! Updates can be formed to evict data from the original sparsechain. This is useful for handling +//! blockchain reorgs. +//! +//! ``` +//! # use bdk_chain::{BlockId, TxHeight, sparse_chain::*, example_utils::*}; +//! # use bitcoin::BlockHash; +//! # let hash_a = new_hash::("a"); +//! # let hash_b = new_hash::("b"); +//! # let hash_c = new_hash::("c"); +//! # let hash_d = new_hash::("d"); +//! // our chain has a single checkpoint at height 11 +//! let mut chain = SparseChain::::from_checkpoints(vec![ +//! BlockId { height: 11, hash: hash_a }, +//! ]); +//! +//! // we detect a reorg at height 11, and we introduce a new checkpoint at height 12 +//! let update = SparseChain::from_checkpoints(vec![ +//! BlockId { height: 11, hash: hash_b }, +//! BlockId { height: 12, hash: hash_c }, +//! ]); +//! let _ = chain +//! .apply_update(update) +//! .expect("we can evict/replace checkpoint 11 since it is the only checkpoint"); +//! +//! // now our `chain` has 2 checkpoints (11:hash_b & 12:hash_c) +//! // we detect another reorg, this time at height 12... +//! let update = SparseChain::from_checkpoints(vec![ +//! // we connect at checkpoint 11 as this is our "point of agreement" +//! BlockId { height: 11, hash: hash_b }, +//! BlockId { height: 12, hash: hash_d }, +//! ]); +//! let _ = chain +//! .apply_update(update) +//! .expect("we have provided a valid point of agreement, so our reorg update will succeed"); +//! ``` +//! +//! ## Movement of transactions during update +//! +//! If the original sparsechain and update sparsechain contain the same transaction at different +//! [`ChainPosition`]s, the transaction is considered as "moved". There are various movements of a +//! transaction that are invalid and update will fail. +//! +//! Valid movements: +//! +//! * When the transaction moved from unconfirmed (in original) to confirmed (in update). In other +//! words, confirming transactions are allowed! +//! * If there has been a reorg at height x, an originally confirmed transaction at height x or +//! above, may move to another height (that is at x or above, including becoming unconfirmed). +//! +//! Invalid movements: +//! +//! * A confirmed transaction cannot move without a reorg. +//! * Even with a reorg, an originally confirmed transaction cannot be moved below the height of the +//! reorg. +//! +//! # Custom [`ChainPosition`] +//! +//! [`SparseChain`] maintains a list of txids ordered by [`ChainPosition`]. By default, [`TxHeight`] +//! is used, however additional data can be incorporated into the implementation. +//! +//! For example, we can have "perfect ordering" of transactions if our positional index is a +//! combination of block height and transaction position in block. +//! +//! ``` +//! # use bdk_chain::{BlockId, TxHeight, sparse_chain::*, example_utils::*}; +//! # use bitcoin::{BlockHash, Txid}; +//! # let hash_a = new_hash::("a"); +//! # let txid_1 = new_hash::("1"); +//! # let txid_2 = new_hash::("2"); +//! # let txid_3 = new_hash::("3"); +//! #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +//! pub enum TxPosition { +//! Confirmed { +//! height: u32, // height of block +//! position: u32, // position of transaction in the block +//! }, +//! Unconfirmed, +//! } +//! +//! impl Default for TxPosition { +//! fn default() -> Self { Self::Unconfirmed } +//! } +//! +//! impl ChainPosition for TxPosition { +//! fn height(&self) -> TxHeight { +//! match self { +//! Self::Confirmed{ height, .. } => TxHeight::Confirmed(*height), +//! Self::Unconfirmed => TxHeight::Unconfirmed, +//! } +//! } +//! +//! fn max_ord_of_height(height: TxHeight) -> Self { +//! match height { +//! TxHeight::Confirmed(height) => Self::Confirmed{ height, position: u32::MAX }, +//! TxHeight::Unconfirmed => Self::Unconfirmed, +//! } +//! } +//! +//! fn min_ord_of_height(height: TxHeight) -> Self { +//! match height { +//! TxHeight::Confirmed(height) => Self::Confirmed{ height, position: u32::MIN }, +//! TxHeight::Unconfirmed => Self::Unconfirmed, +//! } +//! } +//! } +//! +//! let mut chain = SparseChain::::default(); +//! let _ = chain.insert_checkpoint(BlockId { height: 10, hash: hash_a }).unwrap(); +//! let _ = chain.insert_tx(txid_1, TxPosition::Confirmed{ height: 9, position: 4321 }).unwrap(); +//! let _ = chain.insert_tx(txid_2, TxPosition::Confirmed{ height: 9, position: 1234 }).unwrap(); +//! let _ = chain.insert_tx(txid_3, TxPosition::Confirmed{ height: 10, position: 321 }).unwrap(); +//! +//! // transactions are ordered correctly +//! assert_eq!( +//! chain.txids().collect::>(), +//! vec![ +//! &(TxPosition::Confirmed{ height: 9, position: 1234 }, txid_2), +//! &(TxPosition::Confirmed{ height: 9, position: 4321 }, txid_1), +//! &(TxPosition::Confirmed{ height: 10, position: 321 }, txid_3), +//! ], +//! ); +//! +//! ``` +use core::{ + fmt::Debug, + ops::{Bound, RangeBounds}, +}; + +use crate::{collections::*, tx_graph::TxGraph, AsTransaction, BlockId, FullTxOut, TxHeight}; +use bitcoin::{hashes::Hash, BlockHash, OutPoint, Txid}; + +/// This is a non-monotone structure that tracks relevant [`Txid`]s that are ordered by chain +/// position `P`. +/// +/// We use [`BlockHash`]s alongside their chain height as "checkpoints" to enforce consistency. +/// +/// To "merge" two [`SparseChain`]s, the [`ChangeSet`] can be calculated by calling +/// [`determine_changeset`] and applying the [`ChangeSet`] via [`apply_changeset`]. For convenience, +/// [`apply_update`] does the above two steps in one call. +/// +/// Refer to [module-level documentation] for more. +/// +/// [`determine_changeset`]: Self::determine_changeset +/// [`apply_changeset`]: Self::apply_changeset +/// [`apply_update`]: Self::apply_update +/// [module-level documentation]: crate::sparse_chain +#[derive(Clone, Debug, PartialEq)] +pub struct SparseChain

{ + /// Block height to checkpoint data. + checkpoints: BTreeMap, + /// Txids ordered by the pos `P`. + ordered_txids: BTreeSet<(P, Txid)>, + /// Confirmation heights of txids. + txid_to_pos: HashMap, + /// Limit number of checkpoints. + checkpoint_limit: Option, +} + +impl

AsRef> for SparseChain

{ + fn as_ref(&self) -> &SparseChain

{ + self + } +} + +impl

Default for SparseChain

{ + fn default() -> Self { + Self { + checkpoints: Default::default(), + ordered_txids: Default::default(), + txid_to_pos: Default::default(), + checkpoint_limit: Default::default(), + } + } +} + +/// Represents a failure when trying to insert a [`Txid`] into [`SparseChain`]. +#[derive(Clone, Debug, PartialEq)] +pub enum InsertTxError

{ + /// Occurs when the [`Txid`] is to be inserted at a hight higher than the [`SparseChain`]'s tip. + TxTooHigh { + txid: Txid, + tx_height: u32, + tip_height: Option, + }, + /// Occurs when the [`Txid`] is already in the [`SparseChain`] and the insertion would result in + /// an unexpected move in [`ChainPosition`]. + TxMovedUnexpectedly { + txid: Txid, + original_pos: P, + update_pos: P, + }, +} + +impl core::fmt::Display for InsertTxError

{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + InsertTxError::TxTooHigh { + txid, + tx_height, + tip_height, + } => write!( + f, + "txid ({}) cannot be inserted at height ({}) greater than chain tip ({:?})", + txid, tx_height, tip_height + ), + InsertTxError::TxMovedUnexpectedly { + txid, + original_pos, + update_pos, + } => write!( + f, + "txid ({}) insertion resulted in an expected positional move from {:?} to {:?}", + txid, original_pos, update_pos + ), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for InsertTxError

{} + +/// Represents a failure when trying to insert a checkpoint into [`SparseChain`]. +#[derive(Clone, Debug, PartialEq)] +pub enum InsertCheckpointError { + /// Occurs when checkpoint of the same height already exists with a different [`BlockHash`]. + HashNotMatching { + height: u32, + original_hash: BlockHash, + update_hash: BlockHash, + }, +} + +impl core::fmt::Display for InsertCheckpointError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for InsertCheckpointError {} + +/// Represents an update failure of [`SparseChain`]. +#[derive(Clone, Debug, PartialEq)] +pub enum UpdateError

{ + /// The update cannot be applied to the chain because the chain suffix it represents did not + /// connect to the existing chain. This error case contains the checkpoint height to include so + /// that the chains can connect. + NotConnected(u32), + /// The update contains inconsistent tx states (e.g. it changed the transaction's height). This + /// error is usually the inconsistency found. + TxInconsistent { + txid: Txid, + original_pos: P, + update_pos: P, + }, +} + +impl core::fmt::Display for UpdateError

{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::NotConnected(h) => + write!(f, "the checkpoints in the update could not be connected to the checkpoints in the chain, try include checkpoint of height {} to connect", + h), + Self::TxInconsistent { txid, original_pos, update_pos } => + write!(f, "tx ({}) had position ({:?}), but is ({:?}) in the update", + txid, original_pos, update_pos), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for UpdateError

{} + +impl SparseChain

{ + /// Creates a new chain from a list of block hashes and heights. The caller must guarantee they + /// are in the same chain. + pub fn from_checkpoints(checkpoints: C) -> Self + where + C: IntoIterator, + { + let mut chain = Self::default(); + chain.checkpoints = checkpoints + .into_iter() + .map(|block_id| block_id.into()) + .collect(); + chain + } + + /// Get the checkpoint for the last known tip. + pub fn latest_checkpoint(&self) -> Option { + self.checkpoints + .iter() + .last() + .map(|(&height, &hash)| BlockId { height, hash }) + } + + /// Get the checkpoint at the given height if it exists. + pub fn checkpoint_at(&self, height: u32) -> Option { + self.checkpoints + .get(&height) + .map(|&hash| BlockId { height, hash }) + } + + /// Return the [`ChainPosition`] of a `txid`. + /// + /// This returns [`None`] if the transation does not exist. + pub fn tx_position(&self, txid: Txid) -> Option<&P> { + self.txid_to_pos.get(&txid) + } + + /// Return a [`BTreeMap`] of all checkpoints (block hashes by height). + pub fn checkpoints(&self) -> &BTreeMap { + &self.checkpoints + } + + /// Return an iterator over checkpoints in a height range, in ascending height order. + pub fn range_checkpoints( + &self, + range: impl RangeBounds, + ) -> impl DoubleEndedIterator + '_ { + self.checkpoints + .range(range) + .map(|(&height, &hash)| BlockId { height, hash }) + } + + /// Preview changes of updating [`Self`] with another chain that connects to it. + /// + /// If the `update` wishes to introduce confirmed transactions, it must contain a checkpoint + /// that is exactly the same height as one of `self`'s checkpoints. + /// + /// To invalidate from a given checkpoint, `update` must contain a checkpoint of the same height + /// but different hash. Invalidated checkpoints result in invalidated transactions becoming + /// "unconfirmed". + /// + /// An error will be returned if an update will result in inconsistencies or if the update does + /// not properly connect with `self`. + /// + /// Refer to [module-level documentation] for more. + /// + /// [module-level documentation]: crate::sparse_chain + pub fn determine_changeset(&self, update: &Self) -> Result, UpdateError

> { + let agreement_point = update + .checkpoints + .iter() + .rev() + .find(|&(height, hash)| self.checkpoints.get(height) == Some(hash)) + .map(|(&h, _)| h); + + let last_update_cp = update.checkpoints.iter().last().map(|(&h, _)| h); + + // the lower bound of the invalidation range + let invalid_lb = if last_update_cp.is_none() || last_update_cp == agreement_point { + // if agreement point is the last update checkpoint, or there is no update checkpoints, + // no invalidation is required + u32::MAX + } else { + agreement_point.map(|h| h + 1).unwrap_or(0) + }; + + // the first checkpoint of the sparsechain to invalidate (if any) + let invalid_from = self.checkpoints.range(invalid_lb..).next().map(|(&h, _)| h); + + // the first checkpoint to invalidate (if any) should be represented in the update + if let Some(first_invalid) = invalid_from { + if !update.checkpoints.contains_key(&first_invalid) { + return Err(UpdateError::NotConnected(first_invalid)); + } + } + + for (&txid, update_pos) in &update.txid_to_pos { + // ensure all currently confirmed txs are still at the same height (unless they are + // within invalidation range, or to be confirmed) + if let Some(original_pos) = &self.txid_to_pos.get(&txid) { + if original_pos.height() < TxHeight::Confirmed(invalid_lb) + && original_pos != &update_pos + { + return Err(UpdateError::TxInconsistent { + txid, + original_pos: P::clone(original_pos), + update_pos: update_pos.clone(), + }); + } + } + } + + // create initial change-set, based on checkpoints and txids that are to be "invalidated" + let mut changeset = invalid_from + .map(|from_height| self.invalidate_checkpoints_preview(from_height)) + .unwrap_or_default(); + + for (&height, &new_hash) in &update.checkpoints { + let original_hash = self.checkpoints.get(&height).cloned(); + + let update_hash = *changeset + .checkpoints + .entry(height) + .and_modify(|change| *change = Some(new_hash)) + .or_insert_with(|| Some(new_hash)); + + if original_hash == update_hash { + changeset.checkpoints.remove(&height); + } + } + + for (txid, new_pos) in &update.txid_to_pos { + let original_pos = self.txid_to_pos.get(txid).cloned(); + + let update_pos = changeset + .txids + .entry(*txid) + .and_modify(|change| *change = Some(new_pos.clone())) + .or_insert_with(|| Some(new_pos.clone())); + + if original_pos == *update_pos { + changeset.txids.remove(txid); + } + } + + Ok(changeset) + } + + /// Updates [`SparseChain`] with another chain that connects to it. + /// + /// This is equivilant to calling [`determine_changeset`] and [`apply_changeset`] in sequence. + /// + /// [`determine_changeset`]: Self::determine_changeset + /// [`apply_changeset`]: Self::apply_changeset + pub fn apply_update(&mut self, update: Self) -> Result, UpdateError

> { + let changeset = self.determine_changeset(&update)?; + self.apply_changeset(changeset.clone()); + Ok(changeset) + } + + pub fn apply_changeset(&mut self, changeset: ChangeSet

) { + for (height, update_hash) in changeset.checkpoints { + let _original_hash = match update_hash { + Some(update_hash) => self.checkpoints.insert(height, update_hash), + None => self.checkpoints.remove(&height), + }; + } + + for (txid, update_pos) in changeset.txids { + let original_pos = self.txid_to_pos.remove(&txid); + + if let Some(pos) = original_pos { + self.ordered_txids.remove(&(pos, txid)); + } + + if let Some(pos) = update_pos { + self.txid_to_pos.insert(txid, pos.clone()); + self.ordered_txids.insert((pos.clone(), txid)); + } + } + + self.prune_checkpoints(); + } + + /// Derives a [`ChangeSet`] that assumes that there are no preceding changesets. + /// + /// The changeset returned will record additions of all [`Txid`]s and checkpoints included in + /// [`Self`]. + pub fn initial_changeset(&self) -> ChangeSet

{ + ChangeSet { + checkpoints: self + .checkpoints + .iter() + .map(|(height, hash)| (*height, Some(*hash))) + .collect(), + txids: self + .ordered_txids + .iter() + .map(|(pos, txid)| (*txid, Some(pos.clone()))) + .collect(), + } + } + + /// Determines the [`ChangeSet`] when checkpoints `from_height` (inclusive) and above are + /// invalidated. Displaced [`Txid`]s will be repositioned to [`TxHeight::Unconfirmed`]. + pub fn invalidate_checkpoints_preview(&self, from_height: u32) -> ChangeSet

{ + ChangeSet::

{ + checkpoints: self + .checkpoints + .range(from_height..) + .map(|(height, _)| (*height, None)) + .collect(), + // invalidated transactions become unconfirmed + txids: self + .range_txids_by_height(TxHeight::Confirmed(from_height)..TxHeight::Unconfirmed) + .map(|(_, txid)| (*txid, Some(P::max_ord_of_height(TxHeight::Unconfirmed)))) + .collect(), + } + } + + /// Invalidate checkpoints `from_height` (inclusive) and above. + /// + /// This is equivalent to calling [`invalidate_checkpoints_preview`] and [`apply_changeset`] in + /// sequence. + /// + /// [`invalidate_checkpoints_preview`]: Self::invalidate_checkpoints_preview + /// [`apply_changeset`]: Self::apply_changeset + pub fn invalidate_checkpoints(&mut self, from_height: u32) -> ChangeSet

{ + let changeset = self.invalidate_checkpoints_preview(from_height); + self.apply_changeset(changeset.clone()); + changeset + } + + /// Determines the [`ChangeSet`] when all transactions of height [`TxHeight::Unconfirmed`] are + /// removed completely. + pub fn clear_mempool_preview(&self) -> ChangeSet

{ + let mempool_range = &( + P::min_ord_of_height(TxHeight::Unconfirmed), + Txid::all_zeros(), + )..; + + let txids = self + .ordered_txids + .range(mempool_range) + .map(|(_, txid)| (*txid, None)) + .collect(); + + ChangeSet::

{ + txids, + ..Default::default() + } + } + + /// Clears all transactions of height [`TxHeight::Unconfirmed`]. + /// + /// This is equivalent to calling [`clear_mempool_preview`] and [`apply_changeset`] in sequence. + /// + /// [`clear_mempool_preview`]: Self::clear_mempool_preview + /// [`apply_changeset`]: Self::apply_changeset + /// [`ChangeSet`]. + pub fn clear_mempool(&mut self) -> ChangeSet

{ + let changeset = self.clear_mempool_preview(); + self.apply_changeset(changeset.clone()); + changeset + } + + /// Determines the resultant [`ChangeSet`] if [`Txid`] was inserted at position `pos`. + /// + /// Changes to the [`Txid`]'s position is allowed (under the rules noted in + /// [module-level documentation]) and will be reflected in the [`ChangeSet`]. + /// + /// [module-level documentation]: crate::sparse_chain + pub fn insert_tx_preview(&self, txid: Txid, pos: P) -> Result, InsertTxError

> { + let mut update = Self::default(); + + if let Some(block_id) = self.latest_checkpoint() { + let _old_hash = update.checkpoints.insert(block_id.height, block_id.hash); + debug_assert!(_old_hash.is_none()); + } + + let tip_height = self.checkpoints.iter().last().map(|(h, _)| *h); + if let TxHeight::Confirmed(tx_height) = pos.height() { + if Some(tx_height) > tip_height { + return Err(InsertTxError::TxTooHigh { + txid, + tx_height, + tip_height, + }); + } + } + + let _old_pos = update.txid_to_pos.insert(txid, pos.clone()); + debug_assert!(_old_pos.is_none()); + + let _inserted = update.ordered_txids.insert((pos, txid)); + debug_assert!(_inserted, "must insert tx"); + + match self.determine_changeset(&update) { + Ok(changeset) => Ok(changeset), + Err(UpdateError::NotConnected(_)) => panic!("should always connect"), + Err(UpdateError::TxInconsistent { + txid: inconsistent_txid, + original_pos, + update_pos, + }) => Err(InsertTxError::TxMovedUnexpectedly { + txid: inconsistent_txid, + original_pos, + update_pos, + }), + } + } + + /// Inserts a given [`Txid`] at `pos`. + /// + /// This is equivilant to calling [`insert_tx_preview`] and [`apply_changeset`] in sequence. + /// + /// [`insert_tx_preview`]: Self::insert_tx_preview + /// [`apply_changeset`]: Self::apply_changeset + pub fn insert_tx(&mut self, txid: Txid, pos: P) -> Result, InsertTxError

> { + let changeset = self.insert_tx_preview(txid, pos)?; + self.apply_changeset(changeset.clone()); + Ok(changeset) + } + + /// Determines the resultant [`ChangeSet`] if [`BlockId`] was inserted. + /// + /// If the change would result in a change in block hash of a certain height, insertion would + /// fail. + pub fn insert_checkpoint_preview( + &self, + block_id: BlockId, + ) -> Result, InsertCheckpointError> { + let mut update = Self::default(); + + if let Some(block_id) = self.latest_checkpoint() { + let _old_hash = update.checkpoints.insert(block_id.height, block_id.hash); + debug_assert!(_old_hash.is_none()); + } + + if let Some(original_hash) = update.checkpoints.insert(block_id.height, block_id.hash) { + if original_hash != block_id.hash { + return Err(InsertCheckpointError::HashNotMatching { + height: block_id.height, + original_hash, + update_hash: block_id.hash, + }); + } + } + + match self.determine_changeset(&update) { + Ok(changeset) => Ok(changeset), + Err(UpdateError::NotConnected(_)) => panic!("error should have caught above"), + Err(UpdateError::TxInconsistent { .. }) => panic!("should never add txs"), + } + } + + /// Insert a checkpoint ([`BlockId`]). + /// + /// This is equivilant to calling [`insert_checkpoint_preview`] and [`apply_changeset`] in + /// sequence. + /// + /// [`insert_checkpoint_preview`]: Self::insert_checkpoint_preview + /// [`apply_changeset`]: Self::apply_changeset + pub fn insert_checkpoint( + &mut self, + block_id: BlockId, + ) -> Result, InsertCheckpointError> { + let changeset = self.insert_checkpoint_preview(block_id)?; + self.apply_changeset(changeset.clone()); + Ok(changeset) + } + + /// Iterate over all [`Txid`]s ordered by their [`ChainPosition`]. + pub fn txids(&self) -> impl DoubleEndedIterator + ExactSizeIterator + '_ { + self.ordered_txids.iter() + } + + /// Iterate over a sub-range of positioned [`Txid`]s. + pub fn range_txids(&self, range: R) -> impl DoubleEndedIterator + '_ + where + R: RangeBounds<(P, Txid)>, + { + let map_bound = |b: Bound<&(P, Txid)>| match b { + Bound::Included((pos, txid)) => Bound::Included((pos.clone(), *txid)), + Bound::Excluded((pos, txid)) => Bound::Excluded((pos.clone(), *txid)), + Bound::Unbounded => Bound::Unbounded, + }; + + self.ordered_txids + .range((map_bound(range.start_bound()), map_bound(range.end_bound()))) + } + + /// Iterate over a sub-range of positioned [`Txid`]s, where the range is defined by + /// [`ChainPosition`] only. + pub fn range_txids_by_position( + &self, + range: R, + ) -> impl DoubleEndedIterator + '_ + where + R: RangeBounds

, + { + let map_bound = |b: Bound<&P>, inc: Txid, exc: Txid| match b { + Bound::Included(pos) => Bound::Included((pos.clone(), inc)), + Bound::Excluded(pos) => Bound::Excluded((pos.clone(), exc)), + Bound::Unbounded => Bound::Unbounded, + }; + + self.ordered_txids.range(( + map_bound(range.start_bound(), min_txid(), max_txid()), + map_bound(range.end_bound(), max_txid(), min_txid()), + )) + } + + /// Iterate over a sub-range of positioned [`Txid`]s, where the range is define by [`TxHeight`] + /// only. + pub fn range_txids_by_height( + &self, + range: R, + ) -> impl DoubleEndedIterator + '_ + where + R: RangeBounds, + { + let ord_it = |height, is_max| match is_max { + true => P::max_ord_of_height(height), + false => P::min_ord_of_height(height), + }; + + let map_bound = |b: Bound<&TxHeight>, inc: (bool, Txid), exc: (bool, Txid)| match b { + Bound::Included(&h) => Bound::Included((ord_it(h, inc.0), inc.1)), + Bound::Excluded(&h) => Bound::Excluded((ord_it(h, exc.0), exc.1)), + Bound::Unbounded => Bound::Unbounded, + }; + + self.ordered_txids.range(( + map_bound(range.start_bound(), (false, min_txid()), (true, max_txid())), + map_bound(range.end_bound(), (true, max_txid()), (false, min_txid())), + )) + } + + /// Attempt to retrieve a [`FullTxOut`] of the given `outpoint`. + /// + /// This will return `Some` only if the output's transaction is in both `self` and `graph`. + pub fn full_txout( + &self, + graph: &TxGraph, + outpoint: OutPoint, + ) -> Option> { + let chain_pos = self.tx_position(outpoint.txid)?; + + let tx = graph.get_tx(outpoint.txid)?; + let is_on_coinbase = tx.as_tx().is_coin_base(); + let txout = tx.as_tx().output.get(outpoint.vout as usize)?.clone(); + + let spent_by = self + .spent_by(graph, outpoint) + .map(|(pos, txid)| (pos.clone(), txid)); + + Some(FullTxOut { + outpoint, + txout, + chain_position: chain_pos.clone(), + spent_by, + is_on_coinbase, + }) + } + + /// Returns the value set as the checkpoint limit. + /// + /// Refer to [`set_checkpoint_limit`]. + /// + /// [`set_checkpoint_limit`]: Self::set_checkpoint_limit + pub fn checkpoint_limit(&self) -> Option { + self.checkpoint_limit + } + + /// Set the checkpoint limit. + /// + /// The checkpoint limit restricts the number of checkpoints that can be stored in [`Self`]. + /// Oldest checkpoints are pruned first. + pub fn set_checkpoint_limit(&mut self, limit: Option) { + self.checkpoint_limit = limit; + self.prune_checkpoints(); + } + + /// Return [`Txid`]s that would be added to the sparse chain if this `changeset` was applied. + pub fn changeset_additions<'a>( + &'a self, + changeset: &'a ChangeSet

, + ) -> impl Iterator + 'a { + changeset + .txids + .iter() + .filter(|(&txid, pos)| { + pos.is_some() /*it was not a deletion*/ && + self.tx_position(txid).is_none() /*we don't have the txid already*/ + }) + .map(|(&txid, _)| txid) + } + + fn prune_checkpoints(&mut self) -> Option> { + let limit = self.checkpoint_limit?; + + // find last height to be pruned + let last_height = *self.checkpoints.keys().rev().nth(limit)?; + // first height to be kept + let keep_height = last_height + 1; + + let mut split = self.checkpoints.split_off(&keep_height); + core::mem::swap(&mut self.checkpoints, &mut split); + + Some(split) + } + + /// Finds the transaction in the chain that spends `outpoint`. + /// + /// [`TxGraph`] is used to provide the spend relationships. + /// + /// Note that the transaction including `outpoint` does not need to be in the `graph` or the + /// `chain` for this to return `Some`. + pub fn spent_by(&self, graph: &TxGraph, outpoint: OutPoint) -> Option<(&P, Txid)> { + graph + .outspends(outpoint) + .iter() + .find_map(|&txid| Some((self.tx_position(txid)?, txid))) + } + + /// Returns whether the sparse chain contains any checkpoints or transactions. + pub fn is_empty(&self) -> bool { + self.checkpoints.is_empty() && self.txid_to_pos.is_empty() + } +} + +/// The return value of [`determine_changeset`]. +/// +/// [`determine_changeset`]: SparseChain::determine_changeset. +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(crate = "serde_crate") +)] +#[must_use] +pub struct ChangeSet

{ + pub checkpoints: BTreeMap>, + pub txids: BTreeMap>, +} + +impl Default for ChangeSet { + fn default() -> Self { + Self { + checkpoints: Default::default(), + txids: Default::default(), + } + } +} + +impl

ChangeSet

{ + /// Appends the changes in `other` into self such that applying `self` afterwards has the same + /// effect as sequentially applying the original `self` and `other`. + pub fn append(&mut self, mut other: Self) + where + P: ChainPosition, + { + self.checkpoints.append(&mut other.checkpoints); + self.txids.append(&mut other.txids); + } + + /// Whether this changeset contains no changes. + pub fn is_empty(&self) -> bool { + self.checkpoints.is_empty() && self.txids.is_empty() + } +} + +fn min_txid() -> Txid { + Txid::from_inner([0x00; 32]) +} + +fn max_txid() -> Txid { + Txid::from_inner([0xff; 32]) +} + +/// Represents an position in which transactions are ordered in [`SparseChain`]. +/// +/// [`ChainPosition`] implementations must be [`Ord`] by [`TxHeight`] first. +pub trait ChainPosition: + core::fmt::Debug + Clone + Eq + PartialOrd + Ord + core::hash::Hash + Send + Sync + 'static +{ + /// Get the transaction height of the positon. + fn height(&self) -> TxHeight; + + /// Get the positon's upper bound of a given height. + fn max_ord_of_height(height: TxHeight) -> Self; + + /// Get the position's lower bound of a given height. + fn min_ord_of_height(height: TxHeight) -> Self; + + /// Get the unconfirmed position. + fn unconfirmed() -> Self { + Self::max_ord_of_height(TxHeight::Unconfirmed) + } +} + +#[cfg(test)] +pub mod verify_chain_position { + use crate::{sparse_chain::ChainPosition, ConfirmationTime, TxHeight}; + use alloc::vec::Vec; + + pub fn verify_chain_position(head_count: u32, tail_count: u32) { + let values = (0..head_count) + .chain(u32::MAX - tail_count..u32::MAX) + .flat_map(|i| { + [ + P::min_ord_of_height(TxHeight::Confirmed(i)), + P::max_ord_of_height(TxHeight::Confirmed(i)), + ] + }) + .chain([ + P::min_ord_of_height(TxHeight::Unconfirmed), + P::max_ord_of_height(TxHeight::Unconfirmed), + ]) + .collect::>(); + + for i in 0..values.len() { + for j in 0..values.len() { + if i == j { + assert_eq!(values[i], values[j]); + } + if i < j { + assert!(values[i] <= values[j]); + } + if i > j { + assert!(values[i] >= values[j]); + } + } + } + } + + #[test] + fn verify_tx_height() { + verify_chain_position::(1000, 1000); + } + + #[test] + fn verify_confirmation_time() { + verify_chain_position::(1000, 1000); + } +} diff --git a/crates/chain/src/spk_txout_index.rs b/crates/chain/src/spk_txout_index.rs new file mode 100644 index 00000000..6e6afc9a --- /dev/null +++ b/crates/chain/src/spk_txout_index.rs @@ -0,0 +1,312 @@ +use core::ops::RangeBounds; + +use crate::{ + collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap}, + ForEachTxOut, +}; +use bitcoin::{self, OutPoint, Script, 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 [`scan`] 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 a 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 etc you must use other sources of information like a [`SparseChain`]. +/// +/// [`TxOut`]: bitcoin::TxOut +/// [`insert_spk`]: Self::insert_spk +/// [`Ord`]: core::cmp::Ord +/// [`scan`]: Self::scan +/// [`SparseChain`]: crate::sparse_chain::SparseChain +#[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(), + } + } +} + +/// This macro is used instead of a member function of `SpkTxOutIndex` which would result in a +/// compiler error[E0521]: "borrowed data escapes out of closure" when we attempt to take a +/// reference out of the `FprEachTxOut` closure during scanning. +macro_rules! scan_txout { + ($self:ident, $op:expr, $txout:expr) => {{ + 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 + }}; +} + +impl SpkTxOutIndex { + /// Scans an object containing many txouts. + /// + /// Typically this is used in two situations: + /// + /// 1. After loading transaction data from 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. + /// + /// See [`ForEachTxout`] for the types that support this. + /// + /// [`ForEachTxout`]: crate::ForEachTxOut + pub fn scan(&mut self, txouts: &impl ForEachTxOut) -> BTreeSet<&I> { + // let scanner = &mut SpkTxOutScanner::new(self); + let mut scanned_indices = BTreeSet::new(); + + txouts.for_each_txout(|(op, txout)| { + if let Some(spk_i) = scan_txout!(self, op, txout) { + scanned_indices.insert(spk_i); + } + }); + + scanned_indices + } + + /// Scan a single `TxOut` for a matching script pubkey, and returns the index that matched the + /// script pubkey (if any). + pub fn scan_txout(&mut self, op: OutPoint, txout: &TxOut) -> Option<&I> { + scan_txout!(self, op, txout) + } + + /// 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 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::from_inner([0x00; 32]), + vout: u32::MIN, + }; + let max_op = OutPoint { + txid: Txid::from_inner([0xff; 32]), + 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(|(spk_i, txout)| (spk_i, txout)) + } + + /// 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) + } + + /// The script pubkeys 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 whenever it scans new data. + pub fn insert_spk(&mut self, index: I, spk: Script) -> 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 a unused script pubkeys in a 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 + where + R: RangeBounds, + { + self.unused + .range(range) + .map(|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.get(index).is_none() + } + + /// Marks the script pubkey at `index` as used even though it 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 `index` was originally 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 `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 index as unused when it does not exist + if !self.spks.contains_key(index) { + return false; + } + // we cannot set index as unused when txouts are indexed under it + if self.outputs_in_range(index..=index).next().is_some() { + return false; + } + return 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 total input value going from script pubkeys in the index (sent) and total output + /// value going to script pubkeys in the index (received) in `tx`. For the `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 will be correct even if it has not + /// been scanned. + pub fn sent_and_received(&self, tx: &Transaction) -> (u64, u64) { + let mut sent = 0; + let mut received = 0; + + for txin in &tx.input { + if let Some((_, txout)) = self.txout(txin.previous_output) { + sent += txout.value; + } + } + for txout in &tx.output { + if self.index_of_spk(&txout.script_pubkey).is_some() { + received += txout.value; + } + } + + (sent, received) + } + + /// 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. + /// + /// [`sent_and_received`]: Self::sent_and_received + pub fn net_value(&self, tx: &Transaction) -> i64 { + let (sent, received) = self.sent_and_received(tx); + received as i64 - sent as i64 + } + + /// 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() + .find(|input| self.txouts.contains_key(&input.previous_output)) + .is_some(); + let output_matches = tx + .output + .iter() + .find(|output| self.spk_indices.contains_key(&output.script_pubkey)) + .is_some(); + input_matches || output_matches + } +} diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs new file mode 100644 index 00000000..af4fe3b6 --- /dev/null +++ b/crates/chain/src/tx_data_traits.rs @@ -0,0 +1,115 @@ +use core::borrow::Borrow; + +use alloc::{borrow::Cow, boxed::Box, rc::Rc, sync::Arc}; +use bitcoin::{Block, OutPoint, Transaction, TxOut}; + +/// Trait to do something with every txout contained in a structure. +/// +/// We would prefer just work with things that can give us a `Iterator` +/// here but rust's type system makes it extremely hard to do this (without trait objects). +pub trait ForEachTxOut { + /// The provided closure `f` will called with each `outpoint/txout` pair. + fn for_each_txout(&self, f: impl FnMut((OutPoint, &TxOut))); +} + +impl ForEachTxOut for Block { + fn for_each_txout(&self, mut f: impl FnMut((OutPoint, &TxOut))) { + for tx in self.txdata.iter() { + tx.for_each_txout(&mut f) + } + } +} + +/// Trait for things that have a single [`Transaction`] in them. +/// +/// This alows polymorphism in structures such as [`TxGraph`] where `T` can be anything that +/// implements `AsTransaction`. You might think that we could just use [`core::convert::AsRef`] for +/// this but the problem is that we need to implement it on `Cow` where `T: AsTransaction` which +/// we can't do with a foreign trait like `AsTransaction`. +/// +/// [`Transaction`]: bitcoin::Transaction +/// [`TxGraph`]: crate::tx_graph::TxGraph +pub trait AsTransaction { + /// Get a reference to the transaction. + fn as_tx(&self) -> &Transaction; +} + +impl AsTransaction for Transaction { + fn as_tx(&self) -> &Transaction { + self + } +} + +impl AsTransaction for Rc { + fn as_tx(&self) -> &Transaction { + self.as_ref().as_tx() + } +} + +impl AsTransaction for Arc { + fn as_tx(&self) -> &Transaction { + self.as_ref().as_tx() + } +} + +impl AsTransaction for Box { + fn as_tx(&self) -> &Transaction { + self.as_ref().as_tx() + } +} + +impl<'a, T: AsTransaction + Clone> AsTransaction for Cow<'a, T> { + fn as_tx(&self) -> &Transaction { + as Borrow>::borrow(self).as_tx() + } +} + +impl ForEachTxOut for T +where + T: AsTransaction, +{ + fn for_each_txout(&self, mut f: impl FnMut((OutPoint, &TxOut))) { + let tx = self.as_tx(); + let txid = tx.txid(); + for (i, txout) in tx.output.iter().enumerate() { + f(( + OutPoint { + txid, + vout: i as u32, + }, + txout, + )) + } + } +} + +/// A trait like [`core::convert::Into`] for converting one thing into another. +/// +/// We use it to convert one transaction type into another so that an update for `T2` can be used on +/// a `TxGraph` as long as `T2: IntoOwned`. +/// +/// We couldn't use `Into` because we needed to implement it for [`Cow<'a, T>`]. +/// +/// [`Cow<'a, T>`]: std::borrow::Cow +pub trait IntoOwned { + /// Converts the provided type into another (owned) type. + fn into_owned(self) -> T; +} + +impl IntoOwned for T { + fn into_owned(self) -> T { + self + } +} + +impl<'a, T: Clone> IntoOwned for Cow<'a, T> { + fn into_owned(self) -> T { + Cow::into_owned(self) + } +} + +impl<'a, T: Clone> IntoOwned for &'a T { + fn into_owned(self) -> T { + self.clone() + } +} diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs new file mode 100644 index 00000000..df95b827 --- /dev/null +++ b/crates/chain/src/tx_graph.rs @@ -0,0 +1,637 @@ +//! Module for structures that store and traverse transactions. +//! +//! [`TxGraph`] is a monotone structure that inserts transactions and indexes spends. The +//! [`Additions`] structure reports changes of [`TxGraph`], but can also be applied on to a +//! [`TxGraph`] as well. Lastly, [`TxDescendants`] is an [`Iterator`] which traverses descendants of +//! a given transaction. +//! +//! Conflicting transactions are allowed to coexist within a [`TxGraph`]. This is useful for +//! identifying and traversing conflicts and descendants of a given transaction. +//! +//! # Previewing and applying changes +//! +//! Methods that either preview or apply changes to [`TxGraph`] will return [`Additions`]. +//! [`Additions`] can be applied back on to a [`TxGraph`], or be used to inform persistent storage +//! of the changes to [`TxGraph`]. +//! +//! ``` +//! # use bdk_chain::tx_graph::TxGraph; +//! # use bdk_chain::example_utils::*; +//! # use bitcoin::Transaction; +//! # let tx_a = tx_from_hex(RAW_TX_1); +//! # let tx_b = tx_from_hex(RAW_TX_2); +//! let mut graph = TxGraph::::default(); +//! +//! // preview a transaction insertion (not actually inserted) +//! let additions = graph.insert_tx_preview(tx_a); +//! // apply the insertion +//! graph.apply_additions(additions); +//! +//! // you can also insert a transaction directly +//! let already_applied_additions = graph.insert_tx(tx_b); +//! ``` +//! +//! A [`TxGraph`] can also be updated with another [`TxGraph`]. +//! +//! ``` +//! # use bdk_chain::tx_graph::TxGraph; +//! # use bdk_chain::example_utils::*; +//! # use bitcoin::Transaction; +//! # let tx_a = tx_from_hex(RAW_TX_1); +//! # let tx_b = tx_from_hex(RAW_TX_2); +//! let mut graph = TxGraph::::default(); +//! let update = TxGraph::::new(vec![tx_a, tx_b]); +//! +//! // preview additions as result of the update +//! let additions = graph.determine_additions(&update); +//! // apply the additions +//! graph.apply_additions(additions); +//! +//! // we can also apply the update graph directly +//! // the additions will be empty as we have already applied the same update above +//! let additions = graph.apply_update(update); +//! assert!(additions.is_empty()); +//! ``` +//! +use crate::{collections::*, AsTransaction, ForEachTxOut, IntoOwned}; +use alloc::vec::Vec; +use bitcoin::{OutPoint, Transaction, TxOut, Txid}; +use core::ops::RangeInclusive; + +/// A graph of transactions and spends. +/// +/// See the [module-level documentation] for more. +/// +/// [module-level documentation]: crate::tx_graph +#[derive(Clone, Debug, PartialEq)] +pub struct TxGraph { + txs: HashMap>, + spends: BTreeMap>, + + // This atrocity exists so that `TxGraph::outspends()` can return a reference. + // FIXME: This can be removed once `HashSet::new` is a const fn. + empty_outspends: HashSet, +} + +impl Default for TxGraph { + fn default() -> Self { + Self { + txs: Default::default(), + spends: Default::default(), + empty_outspends: Default::default(), + } + } +} + +/// Node of a [`TxGraph`]. This can either be a whole transaction, or a partial transaction (where +/// we only have select outputs). +#[derive(Clone, Debug, PartialEq)] +enum TxNode { + Whole(T), + Partial(BTreeMap), +} + +impl Default for TxNode { + fn default() -> Self { + Self::Partial(BTreeMap::new()) + } +} + +impl TxGraph { + /// Iterate over all tx outputs known by [`TxGraph`]. + pub fn all_txouts(&self) -> impl Iterator { + self.txs.iter().flat_map(|(txid, tx)| match tx { + TxNode::Whole(tx) => tx + .as_tx() + .output + .iter() + .enumerate() + .map(|(vout, txout)| (OutPoint::new(*txid, vout as _), txout)) + .collect::>(), + TxNode::Partial(txouts) => txouts + .iter() + .map(|(vout, txout)| (OutPoint::new(*txid, *vout as _), txout)) + .collect::>(), + }) + } + + /// Iterate over all full transactions in the graph. + pub fn full_transactions(&self) -> impl Iterator { + self.txs.iter().filter_map(|(_, tx)| match tx { + TxNode::Whole(tx) => Some(tx), + TxNode::Partial(_) => None, + }) + } + + /// Get a transaction by txid. This only returns `Some` for full transactions. + /// + /// Refer to [`get_txout`] for getting a specific [`TxOut`]. + /// + /// [`get_txout`]: Self::get_txout + pub fn get_tx(&self, txid: Txid) -> Option<&T> { + match self.txs.get(&txid)? { + TxNode::Whole(tx) => Some(tx), + TxNode::Partial(_) => None, + } + } + + /// Obtains a single tx output (if any) at specified outpoint. + pub fn get_txout(&self, outpoint: OutPoint) -> Option<&TxOut> { + match self.txs.get(&outpoint.txid)? { + TxNode::Whole(tx) => tx.as_tx().output.get(outpoint.vout as usize), + TxNode::Partial(txouts) => txouts.get(&outpoint.vout), + } + } + + /// Returns a [`BTreeMap`] of vout to output of the provided `txid`. + pub fn txouts(&self, txid: Txid) -> Option> { + Some(match self.txs.get(&txid)? { + TxNode::Whole(tx) => tx + .as_tx() + .output + .iter() + .enumerate() + .map(|(vout, txout)| (vout as u32, txout)) + .collect::>(), + TxNode::Partial(txouts) => txouts + .iter() + .map(|(vout, txout)| (*vout, txout)) + .collect::>(), + }) + } + + /// Calculates the fee of a given transaction. Returns 0 if `tx` is a coinbase transaction. + /// Returns `Some(_)` if we have all the `TxOut`s being spent by `tx` in the graph (either as + /// the full transactions or individual txouts). If the returned value is negative then the + /// transaction is invalid according to the graph. + /// + /// Returns `None` if we're missing an input for the tx in the graph. + /// + /// Note `tx` does not have to be in the graph for this to work. + pub fn calculate_fee(&self, tx: &Transaction) -> Option { + if tx.is_coin_base() { + return Some(0); + } + let inputs_sum = tx + .input + .iter() + .map(|txin| { + self.get_txout(txin.previous_output) + .map(|txout| txout.value as i64) + }) + .sum::>()?; + + let outputs_sum = tx + .output + .iter() + .map(|txout| txout.value as i64) + .sum::(); + + Some(inputs_sum - outputs_sum) + } +} + +impl TxGraph { + /// Contruct a new [`TxGraph`] from a list of transaction. + pub fn new(txs: impl IntoIterator) -> Self { + let mut new = Self::default(); + for tx in txs.into_iter() { + let _ = new.insert_tx(tx); + } + new + } + /// Inserts the given [`TxOut`] at [`OutPoint`]. + /// + /// Note this will ignore the action if we already have the full transaction that the txout is + /// alledged to be on (even if it doesn't match it!). + pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> Additions { + let additions = self.insert_txout_preview(outpoint, txout); + self.apply_additions(additions.clone()); + additions + } + + /// Inserts the given transaction into [`TxGraph`]. + /// + /// The [`Additions`] returned will be empty if `tx` already exists. + pub fn insert_tx(&mut self, tx: T) -> Additions { + let additions = self.insert_tx_preview(tx); + self.apply_additions(additions.clone()); + additions + } + + /// Extends this graph with another so that `self` becomes the union of the two sets of + /// transactions. + /// + /// The returned [`Additions`] is the set difference of `update` and `self` (transactions that + /// exist in `update` but not in `self`). + pub fn apply_update(&mut self, update: TxGraph) -> Additions + where + T2: IntoOwned + Clone, + { + let additions = self.determine_additions(&update); + self.apply_additions(additions.clone()); + additions + } + + /// Applies [`Additions`] to [`TxGraph`]. + pub fn apply_additions(&mut self, additions: Additions) { + for tx in additions.tx { + let txid = tx.as_tx().txid(); + + tx.as_tx() + .input + .iter() + .map(|txin| txin.previous_output) + // coinbase spends are not to be counted + .filter(|outpoint| !outpoint.is_null()) + // record spend as this tx has spent this outpoint + .for_each(|outpoint| { + self.spends.entry(outpoint).or_default().insert(txid); + }); + + if let Some(TxNode::Whole(old_tx)) = self.txs.insert(txid, TxNode::Whole(tx)) { + debug_assert_eq!( + old_tx.as_tx().txid(), + txid, + "old tx of same txid should not be different" + ); + } + } + + for (outpoint, txout) in additions.txout { + let tx_entry = self + .txs + .entry(outpoint.txid) + .or_insert_with(TxNode::default); + + match tx_entry { + TxNode::Whole(_) => { /* do nothing since we already have full tx */ } + TxNode::Partial(txouts) => { + txouts.insert(outpoint.vout, txout); + } + } + } + } + + /// Previews the resultant [`Additions`] when [`Self`] is updated against the `update` graph. + /// + /// The [`Additions`] would be the set difference of `update` and `self` (transactions that + /// exist in `update` but not in `self`). + pub fn determine_additions<'a, T2>(&self, update: &'a TxGraph) -> Additions + where + T2: IntoOwned + Clone, + { + let mut additions = Additions::::default(); + + for (&txid, update_tx) in &update.txs { + if self.get_tx(txid).is_some() { + continue; + } + + match update_tx { + TxNode::Whole(tx) => { + if matches!(self.txs.get(&txid), None | Some(TxNode::Partial(_))) { + additions + .tx + .insert(>::into_owned(tx.clone())); + } + } + TxNode::Partial(partial) => { + for (&vout, update_txout) in partial { + let outpoint = OutPoint::new(txid, vout); + + if self.get_txout(outpoint) != Some(&update_txout) { + additions.txout.insert(outpoint, update_txout.clone()); + } + } + } + } + } + + additions + } + + /// Returns the resultant [`Additions`] if the given transaction is inserted. Does not actually + /// mutate [`Self`]. + /// + /// The [`Additions`] result will be empty if `tx` already existed in `self`. + pub fn insert_tx_preview(&self, tx: T) -> Additions { + let mut update = Self::default(); + update.txs.insert(tx.as_tx().txid(), TxNode::Whole(tx)); + self.determine_additions(&update) + } + + /// Returns the resultant [`Additions`] if the given `txout` is inserted at `outpoint`. Does not + /// mutate `self`. + /// + /// The [`Additions`] result will be empty if the `outpoint` (or a full transaction containing + /// the `outpoint`) already existed in `self`. + pub fn insert_txout_preview(&self, outpoint: OutPoint, txout: TxOut) -> Additions { + let mut update = Self::default(); + update.txs.insert( + outpoint.txid, + TxNode::Partial([(outpoint.vout, txout)].into()), + ); + self.determine_additions(&update) + } +} + +impl TxGraph { + /// The transactions spending from this output. + /// + /// `TxGraph` allows conflicting transactions within the graph. Obviously the transactions in + /// the returned will never be in the same blockchain. + pub fn outspends(&self, outpoint: OutPoint) -> &HashSet { + self.spends.get(&outpoint).unwrap_or(&self.empty_outspends) + } + + /// Iterates over the transactions spending from `txid`. + /// + /// The iterator item is a union of `(vout, txid-set)` where: + /// + /// - `vout` is the provided `txid`'s outpoint that is being spent + /// - `txid-set` is the set of txids that is spending the `vout` + pub fn tx_outspends( + &self, + txid: Txid, + ) -> impl DoubleEndedIterator)> + '_ { + let start = OutPoint { txid, vout: 0 }; + let end = OutPoint { + txid, + vout: u32::MAX, + }; + self.spends + .range(start..=end) + .map(|(outpoint, spends)| (outpoint.vout, spends)) + } + + /// Iterate over all partial transactions (outputs only) in the graph. + pub fn partial_transactions(&self) -> impl Iterator)> { + self.txs.iter().filter_map(|(txid, tx)| match tx { + TxNode::Whole(_) => None, + TxNode::Partial(partial) => Some((*txid, partial)), + }) + } + + /// Creates an iterator that both filters and maps descendants from the starting `txid`. + /// + /// The supplied closure takes in two inputs `(depth, descendant_txid)`: + /// + /// * `depth` is the distance between the starting `txid` and the `descendant_txid`. I.e. if the + /// descendant is spending an output of the starting `txid`, the `depth` will be 1. + /// * `descendant_txid` is the descendant's txid which we are considering to walk. + /// + /// The supplied closure returns an `Option`, allowing the caller to map each node it vists + /// and decide whether to visit descendants. + pub fn walk_descendants<'g, F, O>(&'g self, txid: Txid, walk_map: F) -> TxDescendants + where + F: FnMut(usize, Txid) -> Option + 'g, + { + TxDescendants::new_exclude_root(self, txid, walk_map) + } + + /// Creates an iterator that both filters and maps conflicting transactions (this includes + /// descendants of directly-conflicting transactions, which are also considered conflicts). + /// + /// Refer to [`Self::walk_descendants`] for `walk_map` usage. + pub fn walk_conflicts<'g, F, O>( + &'g self, + tx: &'g Transaction, + walk_map: F, + ) -> TxDescendants + where + F: FnMut(usize, Txid) -> Option + 'g, + { + let txids = self.direct_conflicts_of_tx(tx).map(|(_, txid)| txid); + TxDescendants::from_multiple_include_root(self, txids, walk_map) + } + + /// Given a transaction, return an iterator of txids which directly conflict with the given + /// transaction's inputs (spends). The conflicting txids are returned with the given + /// transaction's vin (in which it conflicts). + /// + /// Note that this only returns directly conflicting txids and does not include descendants of + /// those txids (which are technically also conflicting). + pub fn direct_conflicts_of_tx<'g>( + &'g self, + tx: &'g Transaction, + ) -> impl Iterator + '_ { + let txid = tx.txid(); + tx.input + .iter() + .enumerate() + .filter_map(|(vin, txin)| self.spends.get(&txin.previous_output).zip(Some(vin))) + .flat_map(|(spends, vin)| core::iter::repeat(vin).zip(spends.iter().cloned())) + .filter(move |(_, conflicting_txid)| *conflicting_txid != txid) + } + + /// Whether the graph has any transactions or outputs in it. + pub fn is_empty(&self) -> bool { + self.txs.is_empty() + } +} + +/// A structure that represents changes to a [`TxGraph`]. +/// +/// It is named "additions" because [`TxGraph`] is monotone so transactions can only be added and +/// not removed. +/// +/// Refer to [module-level documentation] for more. +/// +/// [module-level documentation]: crate::tx_graph +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde( + crate = "serde_crate", + bound( + deserialize = "T: Ord + serde::Deserialize<'de>", + serialize = "T: Ord + serde::Serialize" + ) + ) +)] +#[must_use] +pub struct Additions { + pub tx: BTreeSet, + pub txout: BTreeMap, +} + +impl Additions { + /// Returns true if the [`Additions`] is empty (no transactions or txouts). + pub fn is_empty(&self) -> bool { + self.tx.is_empty() && self.txout.is_empty() + } + + /// Iterates over all outpoints contained within [`Additions`]. + pub fn txouts(&self) -> impl Iterator + where + T: AsTransaction, + { + self.tx + .iter() + .flat_map(|tx| { + tx.as_tx() + .output + .iter() + .enumerate() + .map(|(vout, txout)| (OutPoint::new(tx.as_tx().txid(), vout as _), txout)) + }) + .chain(self.txout.iter().map(|(op, txout)| (*op, txout))) + } + + /// Appends the changes in `other` into self such that applying `self` afterwards has the same + /// effect as sequentially applying the original `self` and `other`. + pub fn append(&mut self, mut other: Additions) + where + T: Ord, + { + self.tx.append(&mut other.tx); + self.txout.append(&mut other.txout); + } +} + +impl Default for Additions { + fn default() -> Self { + Self { + tx: Default::default(), + txout: Default::default(), + } + } +} + +impl AsRef for TxGraph { + fn as_ref(&self) -> &TxGraph { + self + } +} + +impl ForEachTxOut for Additions { + fn for_each_txout(&self, f: impl FnMut((OutPoint, &TxOut))) { + self.txouts().for_each(f) + } +} + +impl ForEachTxOut for TxGraph { + fn for_each_txout(&self, f: impl FnMut((OutPoint, &TxOut))) { + self.all_txouts().for_each(f) + } +} + +/// An iterator that traverses transaction descendants. +/// +/// This `struct` is created by the [`walk_descendants`] method of [`TxGraph`]. +/// +/// [`walk_descendants`]: TxGraph::walk_descendants +pub struct TxDescendants<'g, F, T> { + graph: &'g TxGraph, + visited: HashSet, + stack: Vec<(usize, Txid)>, + filter_map: F, +} + +impl<'g, F, T> TxDescendants<'g, F, T> { + /// Creates a `TxDescendants` that includes the starting `txid` when iterating. + #[allow(unused)] + pub(crate) fn new_include_root(graph: &'g TxGraph, txid: Txid, filter_map: F) -> Self { + Self { + graph, + visited: Default::default(), + stack: [(0, txid)].into(), + filter_map, + } + } + + /// Creates a `TxDescendants` that excludes the starting `txid` when iterating. + pub(crate) fn new_exclude_root(graph: &'g TxGraph, txid: Txid, filter_map: F) -> Self { + let mut descendants = Self { + graph, + visited: Default::default(), + stack: Default::default(), + filter_map, + }; + descendants.populate_stack(1, txid); + descendants + } + + /// Creates a `TxDescendants` from multiple starting transactions that includes the starting + /// `txid`s when iterating. + pub(crate) fn from_multiple_include_root( + graph: &'g TxGraph, + txids: I, + filter_map: F, + ) -> Self + where + I: IntoIterator, + { + Self { + graph, + visited: Default::default(), + stack: txids.into_iter().map(|txid| (0, txid)).collect(), + filter_map, + } + } + + /// Creates a `TxDescendants` from multiple starting transactions that excludes the starting + /// `txid`s when iterating. + #[allow(unused)] + pub(crate) fn from_multiple_exclude_root( + graph: &'g TxGraph, + txids: I, + filter_map: F, + ) -> Self + where + I: IntoIterator, + { + let mut descendants = Self { + graph, + visited: Default::default(), + stack: Default::default(), + filter_map, + }; + for txid in txids { + descendants.populate_stack(1, txid); + } + descendants + } +} + +impl<'g, F, T> TxDescendants<'g, F, T> { + fn populate_stack(&mut self, depth: usize, txid: Txid) { + let spend_paths = self + .graph + .spends + .range(tx_outpoint_range(txid)) + .flat_map(|(_, spends)| spends) + .map(|&txid| (depth, txid)); + self.stack.extend(spend_paths); + } +} + +impl<'g, F, O, T> Iterator for TxDescendants<'g, F, T> +where + F: FnMut(usize, Txid) -> Option, +{ + type Item = O; + + fn next(&mut self) -> Option { + let (op_spends, txid, item) = loop { + // we have exhausted all paths when stack is empty + let (op_spends, txid) = self.stack.pop()?; + // we do not want to visit the same transaction twice + if self.visited.insert(txid) { + // ignore paths when user filters them out + if let Some(item) = (self.filter_map)(op_spends, txid) { + break (op_spends, txid, item); + } + } + }; + + self.populate_stack(op_spends + 1, txid); + return Some(item); + } +} + +fn tx_outpoint_range(txid: Txid) -> RangeInclusive { + OutPoint::new(txid, u32::MIN)..=OutPoint::new(txid, u32::MAX) +} diff --git a/crates/chain/tests/common/mod.rs b/crates/chain/tests/common/mod.rs new file mode 100644 index 00000000..e9b7a101 --- /dev/null +++ b/crates/chain/tests/common/mod.rs @@ -0,0 +1,60 @@ +#[allow(unused_macros)] +macro_rules! h { + ($index:literal) => {{ + bitcoin::hashes::Hash::hash($index.as_bytes()) + }}; +} + +#[allow(unused_macros)] +macro_rules! chain { + ($([$($tt:tt)*]),*) => { chain!( checkpoints: [$([$($tt)*]),*] ) }; + (checkpoints: $($tail:tt)*) => { chain!( index: TxHeight, checkpoints: $($tail)*) }; + (index: $ind:ty, checkpoints: [ $([$height:expr, $block_hash:expr]),* ] $(,txids: [$(($txid:expr, $tx_height:expr)),*])?) => {{ + #[allow(unused_mut)] + let mut chain = bdk_chain::sparse_chain::SparseChain::<$ind>::from_checkpoints([$(($height, $block_hash).into()),*]); + + $( + $( + let _ = chain.insert_tx($txid, $tx_height).expect("should succeed"); + )* + )? + + chain + }}; +} + +#[allow(unused_macros)] +macro_rules! changeset { + (checkpoints: $($tail:tt)*) => { changeset!(index: TxHeight, checkpoints: $($tail)*) }; + ( + index: $ind:ty, + checkpoints: [ $(( $height:expr, $cp_to:expr )),* ] + $(,txids: [ $(( $txid:expr, $tx_to:expr )),* ])? + ) => {{ + use bdk_chain::collections::BTreeMap; + + #[allow(unused_mut)] + bdk_chain::sparse_chain::ChangeSet::<$ind> { + checkpoints: { + let mut changes = BTreeMap::default(); + $(changes.insert($height, $cp_to);)* + changes + }, + txids: { + let mut changes = BTreeMap::default(); + $($(changes.insert($txid, $tx_to.map(|h: TxHeight| h.into()));)*)? + changes + } + } + }}; +} + +#[allow(unused)] +pub fn new_tx(lt: u32) -> bitcoin::Transaction { + bitcoin::Transaction { + version: 0x00, + lock_time: bitcoin::PackedLockTime(lt), + input: vec![], + output: vec![], + } +} diff --git a/crates/chain/tests/test_chain_graph.rs b/crates/chain/tests/test_chain_graph.rs new file mode 100644 index 00000000..e071dacd --- /dev/null +++ b/crates/chain/tests/test_chain_graph.rs @@ -0,0 +1,653 @@ +#[macro_use] +mod common; + +use bdk_chain::{ + chain_graph::*, + collections::HashSet, + sparse_chain, + tx_graph::{self, TxGraph}, + BlockId, TxHeight, +}; +use bitcoin::{OutPoint, PackedLockTime, Script, Sequence, Transaction, TxIn, TxOut, Witness}; + +#[test] +fn test_spent_by() { + let tx1 = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut::default()], + }; + + let op = OutPoint { + txid: tx1.txid(), + vout: 0, + }; + + let tx2 = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![TxIn { + previous_output: op, + ..Default::default() + }], + output: vec![], + }; + let tx3 = Transaction { + version: 0x01, + lock_time: PackedLockTime(42), + input: vec![TxIn { + previous_output: op, + ..Default::default() + }], + output: vec![], + }; + + let mut cg1 = ChainGraph::default(); + let _ = cg1 + .insert_tx(tx1, TxHeight::Unconfirmed) + .expect("should insert"); + let mut cg2 = cg1.clone(); + let _ = cg1 + .insert_tx(tx2.clone(), TxHeight::Unconfirmed) + .expect("should insert"); + let _ = cg2 + .insert_tx(tx3.clone(), TxHeight::Unconfirmed) + .expect("should insert"); + + assert_eq!(cg1.spent_by(op), Some((&TxHeight::Unconfirmed, tx2.txid()))); + assert_eq!(cg2.spent_by(op), Some((&TxHeight::Unconfirmed, tx3.txid()))); +} + +#[test] +fn update_evicts_conflicting_tx() { + let cp_a = BlockId { + height: 0, + hash: h!("A"), + }; + let cp_b = BlockId { + height: 1, + hash: h!("B"), + }; + let cp_b2 = BlockId { + height: 1, + hash: h!("B'"), + }; + + let tx_a = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut::default()], + }; + + let tx_b = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![TxIn { + previous_output: OutPoint::new(tx_a.txid(), 0), + script_sig: Script::new(), + sequence: Sequence::default(), + witness: Witness::new(), + }], + output: vec![TxOut::default()], + }; + + let tx_b2 = Transaction { + version: 0x02, + lock_time: PackedLockTime(0), + input: vec![TxIn { + previous_output: OutPoint::new(tx_a.txid(), 0), + script_sig: Script::new(), + sequence: Sequence::default(), + witness: Witness::new(), + }], + output: vec![TxOut::default(), TxOut::default()], + }; + { + let mut cg1 = { + let mut cg = ChainGraph::default(); + let _ = cg.insert_checkpoint(cp_a).expect("should insert cp"); + let _ = cg + .insert_tx(tx_a.clone(), TxHeight::Confirmed(0)) + .expect("should insert tx"); + let _ = cg + .insert_tx(tx_b.clone(), TxHeight::Unconfirmed) + .expect("should insert tx"); + cg + }; + let cg2 = { + let mut cg = ChainGraph::default(); + let _ = cg + .insert_tx(tx_b2.clone(), TxHeight::Unconfirmed) + .expect("should insert tx"); + cg + }; + + let changeset = ChangeSet:: { + chain: sparse_chain::ChangeSet { + checkpoints: Default::default(), + txids: [ + (tx_b.txid(), None), + (tx_b2.txid(), Some(TxHeight::Unconfirmed)), + ] + .into(), + }, + graph: tx_graph::Additions { + tx: [tx_b2.clone()].into(), + txout: [].into(), + }, + }; + assert_eq!( + cg1.determine_changeset(&cg2), + Ok(changeset.clone()), + "tx should be evicted from mempool" + ); + + cg1.apply_changeset(changeset); + } + + { + let cg1 = { + let mut cg = ChainGraph::default(); + let _ = cg.insert_checkpoint(cp_a).expect("should insert cp"); + let _ = cg.insert_checkpoint(cp_b).expect("should insert cp"); + let _ = cg + .insert_tx(tx_a.clone(), TxHeight::Confirmed(0)) + .expect("should insert tx"); + let _ = cg + .insert_tx(tx_b.clone(), TxHeight::Confirmed(1)) + .expect("should insert tx"); + cg + }; + let cg2 = { + let mut cg = ChainGraph::default(); + let _ = cg + .insert_tx(tx_b2.clone(), TxHeight::Unconfirmed) + .expect("should insert tx"); + cg + }; + assert_eq!( + cg1.determine_changeset(&cg2), + Err(UpdateError::UnresolvableConflict(UnresolvableConflict { + already_confirmed_tx: (TxHeight::Confirmed(1), tx_b.txid()), + update_tx: (TxHeight::Unconfirmed, tx_b2.txid()), + })), + "fail if tx is evicted from valid block" + ); + } + + { + // Given 2 blocks `{A, B}`, and an update that invalidates block B with + // `{A, B'}`, we expect txs that exist in `B` that conflicts with txs + // introduced in the update to be successfully evicted. + let mut cg1 = { + let mut cg = ChainGraph::default(); + let _ = cg.insert_checkpoint(cp_a).expect("should insert cp"); + let _ = cg.insert_checkpoint(cp_b).expect("should insert cp"); + let _ = cg + .insert_tx(tx_a.clone(), TxHeight::Confirmed(0)) + .expect("should insert tx"); + let _ = cg + .insert_tx(tx_b.clone(), TxHeight::Confirmed(1)) + .expect("should insert tx"); + cg + }; + let cg2 = { + let mut cg = ChainGraph::default(); + let _ = cg.insert_checkpoint(cp_a).expect("should insert cp"); + let _ = cg.insert_checkpoint(cp_b2).expect("should insert cp"); + let _ = cg + .insert_tx(tx_b2.clone(), TxHeight::Unconfirmed) + .expect("should insert tx"); + cg + }; + + let changeset = ChangeSet:: { + chain: sparse_chain::ChangeSet { + checkpoints: [(1, Some(h!("B'")))].into(), + txids: [ + (tx_b.txid(), None), + (tx_b2.txid(), Some(TxHeight::Unconfirmed)), + ] + .into(), + }, + graph: tx_graph::Additions { + tx: [tx_b2.clone()].into(), + txout: [].into(), + }, + }; + assert_eq!( + cg1.determine_changeset(&cg2), + Ok(changeset.clone()), + "tx should be evicted from B", + ); + + cg1.apply_changeset(changeset); + } +} + +#[test] +fn chain_graph_new_missing() { + let tx_a = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut::default()], + }; + let tx_b = Transaction { + version: 0x02, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut::default()], + }; + + let update = chain!( + index: TxHeight, + checkpoints: [[0, h!("A")]], + txids: [ + (tx_a.txid(), TxHeight::Confirmed(0)), + (tx_b.txid(), TxHeight::Confirmed(0)) + ] + ); + let mut graph = TxGraph::default(); + + let mut expected_missing = HashSet::new(); + expected_missing.insert(tx_a.txid()); + expected_missing.insert(tx_b.txid()); + + assert_eq!( + ChainGraph::new(update.clone(), graph.clone()), + Err(NewError::Missing(expected_missing.clone())) + ); + + let _ = graph.insert_tx(tx_b.clone()); + expected_missing.remove(&tx_b.txid()); + + assert_eq!( + ChainGraph::new(update.clone(), graph.clone()), + Err(NewError::Missing(expected_missing.clone())) + ); + + let _ = graph.insert_txout( + OutPoint { + txid: tx_a.txid(), + vout: 0, + }, + tx_a.output[0].clone(), + ); + + assert_eq!( + ChainGraph::new(update.clone(), graph.clone()), + Err(NewError::Missing(expected_missing)), + "inserting an output instead of full tx doesn't satisfy constraint" + ); + + let _ = graph.insert_tx(tx_a.clone()); + + let new_graph = ChainGraph::new(update.clone(), graph.clone()).unwrap(); + let expected_graph = { + let mut cg = ChainGraph::::default(); + let _ = cg + .insert_checkpoint(update.latest_checkpoint().unwrap()) + .unwrap(); + let _ = cg.insert_tx(tx_a, TxHeight::Confirmed(0)).unwrap(); + let _ = cg.insert_tx(tx_b, TxHeight::Confirmed(0)).unwrap(); + cg + }; + + assert_eq!(new_graph, expected_graph); +} + +#[test] +fn chain_graph_new_conflicts() { + let tx_a = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut::default()], + }; + + let tx_b = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![TxIn { + previous_output: OutPoint::new(tx_a.txid(), 0), + script_sig: Script::new(), + sequence: Sequence::default(), + witness: Witness::new(), + }], + output: vec![TxOut::default()], + }; + + let tx_b2 = Transaction { + version: 0x02, + lock_time: PackedLockTime(0), + input: vec![TxIn { + previous_output: OutPoint::new(tx_a.txid(), 0), + script_sig: Script::new(), + sequence: Sequence::default(), + witness: Witness::new(), + }], + output: vec![TxOut::default(), TxOut::default()], + }; + + let chain = chain!( + index: TxHeight, + checkpoints: [[5, h!("A")]], + txids: [ + (tx_a.txid(), TxHeight::Confirmed(1)), + (tx_b.txid(), TxHeight::Confirmed(2)), + (tx_b2.txid(), TxHeight::Confirmed(3)) + ] + ); + + let graph = TxGraph::new([tx_a, tx_b, tx_b2]); + + assert!(matches!( + ChainGraph::new(chain, graph), + Err(NewError::Conflict { .. }) + )); +} + +#[test] +fn test_get_tx_in_chain() { + let mut cg = ChainGraph::default(); + let tx = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut::default()], + }; + + let _ = cg.insert_tx(tx.clone(), TxHeight::Unconfirmed).unwrap(); + assert_eq!( + cg.get_tx_in_chain(tx.txid()), + Some((&TxHeight::Unconfirmed, &tx)) + ); +} + +#[test] +fn test_iterate_transactions() { + let mut cg = ChainGraph::default(); + let txs = (0..3) + .map(|i| Transaction { + version: i, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut::default()], + }) + .collect::>(); + let _ = cg + .insert_checkpoint(BlockId { + height: 1, + hash: h!("A"), + }) + .unwrap(); + let _ = cg + .insert_tx(txs[0].clone(), TxHeight::Confirmed(1)) + .unwrap(); + let _ = cg.insert_tx(txs[1].clone(), TxHeight::Unconfirmed).unwrap(); + let _ = cg + .insert_tx(txs[2].clone(), TxHeight::Confirmed(0)) + .unwrap(); + + assert_eq!( + cg.transactions_in_chain().collect::>(), + vec![ + (&TxHeight::Confirmed(0), &txs[2]), + (&TxHeight::Confirmed(1), &txs[0]), + (&TxHeight::Unconfirmed, &txs[1]), + ] + ); +} + +/// Start with: block1, block2a, tx1, tx2a +/// Update 1: block2a -> block2b , tx2a -> tx2b +/// Update 2: block2b -> block2c , tx2b -> tx2a +#[test] +fn test_apply_changes_reintroduce_tx() { + let block1 = BlockId { + height: 1, + hash: h!("block 1"), + }; + let block2a = BlockId { + height: 2, + hash: h!("block 2a"), + }; + let block2b = BlockId { + height: 2, + hash: h!("block 2b"), + }; + let block2c = BlockId { + height: 2, + hash: h!("block 2c"), + }; + + let tx1 = Transaction { + version: 0, + lock_time: PackedLockTime(1), + input: Vec::new(), + output: [TxOut { + value: 1, + script_pubkey: Script::new(), + }] + .into(), + }; + + let tx2a = Transaction { + version: 0, + lock_time: PackedLockTime('a'.into()), + input: [TxIn { + previous_output: OutPoint::new(tx1.txid(), 0), + ..Default::default() + }] + .into(), + output: [TxOut { + value: 0, + ..Default::default() + }] + .into(), + }; + + let tx2b = Transaction { + lock_time: PackedLockTime('b'.into()), + ..tx2a.clone() + }; + + // block1, block2a, tx1, tx2a + let mut cg = { + let mut cg = ChainGraph::default(); + let _ = cg.insert_checkpoint(block1).unwrap(); + let _ = cg.insert_checkpoint(block2a).unwrap(); + let _ = cg.insert_tx(tx1.clone(), TxHeight::Confirmed(1)).unwrap(); + let _ = cg.insert_tx(tx2a.clone(), TxHeight::Confirmed(2)).unwrap(); + cg + }; + + // block2a -> block2b , tx2a -> tx2b + let update = { + let mut update = ChainGraph::default(); + let _ = update.insert_checkpoint(block1).unwrap(); + let _ = update.insert_checkpoint(block2b).unwrap(); + let _ = update + .insert_tx(tx2b.clone(), TxHeight::Confirmed(2)) + .unwrap(); + update + }; + assert_eq!( + cg.apply_update(update).expect("should update"), + ChangeSet { + chain: changeset! { + checkpoints: [(2, Some(block2b.hash))], + txids: [(tx2a.txid(), None), (tx2b.txid(), Some(TxHeight::Confirmed(2)))] + }, + graph: tx_graph::Additions { + tx: [tx2b.clone()].into(), + ..Default::default() + }, + } + ); + + // block2b -> block2c , tx2b -> tx2a + let update = { + let mut update = ChainGraph::default(); + let _ = update.insert_checkpoint(block1).unwrap(); + let _ = update.insert_checkpoint(block2c).unwrap(); + let _ = update + .insert_tx(tx2a.clone(), TxHeight::Confirmed(2)) + .unwrap(); + update + }; + assert_eq!( + cg.apply_update(update).expect("should update"), + ChangeSet { + chain: changeset! { + checkpoints: [(2, Some(block2c.hash))], + txids: [(tx2b.txid(), None), (tx2a.txid(), Some(TxHeight::Confirmed(2)))] + }, + ..Default::default() + } + ); +} + +#[test] +fn test_evict_descendants() { + let block_1 = BlockId { + height: 1, + hash: h!("block 1"), + }; + + let block_2a = BlockId { + height: 2, + hash: h!("block 2 a"), + }; + + let block_2b = BlockId { + height: 2, + hash: h!("block 2 b"), + }; + + let tx_1 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(h!("fake tx"), 0), + ..Default::default() + }], + output: vec![TxOut { + value: 10_000, + script_pubkey: Script::new(), + }], + ..common::new_tx(1) + }; + let tx_2 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_1.txid(), 0), + ..Default::default() + }], + output: vec![ + TxOut { + value: 20_000, + script_pubkey: Script::new(), + }, + TxOut { + value: 30_000, + script_pubkey: Script::new(), + }, + ], + ..common::new_tx(2) + }; + let tx_3 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_2.txid(), 0), + ..Default::default() + }], + output: vec![TxOut { + value: 40_000, + script_pubkey: Script::new(), + }], + ..common::new_tx(3) + }; + let tx_4 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_2.txid(), 1), + ..Default::default() + }], + output: vec![TxOut { + value: 40_000, + script_pubkey: Script::new(), + }], + ..common::new_tx(4) + }; + let tx_5 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_4.txid(), 0), + ..Default::default() + }], + output: vec![TxOut { + value: 40_000, + script_pubkey: Script::new(), + }], + ..common::new_tx(5) + }; + + let tx_conflict = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_1.txid(), 0), + ..Default::default() + }], + output: vec![TxOut { + value: 12345, + script_pubkey: Script::new(), + }], + ..common::new_tx(6) + }; + + // 1 is spent by 2, 2 is spent by 3 and 4, 4 is spent by 5 + let _txid_1 = tx_1.txid(); + let txid_2 = tx_2.txid(); + let txid_3 = tx_3.txid(); + let txid_4 = tx_4.txid(); + let txid_5 = tx_5.txid(); + + // this tx conflicts with 2 + let txid_conflict = tx_conflict.txid(); + + let cg = { + let mut cg = ChainGraph::::default(); + let _ = cg.insert_checkpoint(block_1); + let _ = cg.insert_checkpoint(block_2a); + let _ = cg.insert_tx(tx_1, TxHeight::Confirmed(1)); + let _ = cg.insert_tx(tx_2, TxHeight::Confirmed(2)); + let _ = cg.insert_tx(tx_3, TxHeight::Confirmed(2)); + let _ = cg.insert_tx(tx_4, TxHeight::Confirmed(2)); + let _ = cg.insert_tx(tx_5, TxHeight::Confirmed(2)); + cg + }; + + let update = { + let mut cg = ChainGraph::::default(); + let _ = cg.insert_checkpoint(block_1); + let _ = cg.insert_checkpoint(block_2b); + let _ = cg.insert_tx(tx_conflict.clone(), TxHeight::Confirmed(2)); + cg + }; + + assert_eq!( + cg.determine_changeset(&update), + Ok(ChangeSet { + chain: changeset! { + checkpoints: [(2, Some(block_2b.hash))], + txids: [(txid_2, None), (txid_3, None), (txid_4, None), (txid_5, None), (txid_conflict, Some(TxHeight::Confirmed(2)))] + }, + graph: tx_graph::Additions { + tx: [tx_conflict.clone()].into(), + ..Default::default() + } + }) + ); + + let err = cg + .insert_tx_preview(tx_conflict.clone(), TxHeight::Unconfirmed) + .expect_err("must fail due to conflicts"); + assert!(matches!(err, InsertTxError::UnresolvableConflict(_))); +} diff --git a/crates/chain/tests/test_keychain_tracker.rs b/crates/chain/tests/test_keychain_tracker.rs new file mode 100644 index 00000000..67eaeb4a --- /dev/null +++ b/crates/chain/tests/test_keychain_tracker.rs @@ -0,0 +1,243 @@ +#![cfg(feature = "miniscript")] +#[macro_use] +mod common; +use bdk_chain::{ + keychain::{Balance, KeychainTracker}, + miniscript::{ + bitcoin::{secp256k1::Secp256k1, OutPoint, PackedLockTime, Transaction, TxOut}, + Descriptor, + }, + BlockId, ConfirmationTime, TxHeight, +}; +use bitcoin::TxIn; + +#[test] +fn test_insert_tx() { + let mut tracker = KeychainTracker::default(); + let secp = Secp256k1::new(); + let (descriptor, _) = Descriptor::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap(); + tracker.add_keychain((), descriptor.clone()); + let txout = TxOut { + value: 100_000, + script_pubkey: descriptor.at_derivation_index(5).script_pubkey(), + }; + + let tx = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![txout], + }; + + let _ = tracker.txout_index.reveal_to_target(&(), 5); + + let changeset = tracker + .insert_tx_preview(tx.clone(), ConfirmationTime::Unconfirmed) + .unwrap(); + tracker.apply_changeset(changeset); + assert_eq!( + tracker + .chain_graph() + .transactions_in_chain() + .collect::>(), + vec![(&ConfirmationTime::Unconfirmed, &tx)] + ); + + assert_eq!( + tracker + .txout_index + .txouts_of_keychain(&()) + .collect::>(), + vec![( + 5, + OutPoint { + txid: tx.txid(), + vout: 0 + } + )] + ); +} + +#[test] +fn test_balance() { + use core::str::FromStr; + #[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] + enum Keychain { + One, + Two, + } + let mut tracker = KeychainTracker::::default(); + let one = Descriptor::from_str("tr([73c5da0a/86'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/0/*)#rg247h69").unwrap(); + let two = Descriptor::from_str("tr([73c5da0a/86'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/1/*)#ju05rz2a").unwrap(); + tracker.add_keychain(Keychain::One, one.clone()); + tracker.add_keychain(Keychain::Two, two.clone()); + + let tx1 = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut { + value: 13_000, + script_pubkey: tracker + .txout_index + .reveal_next_spk(&Keychain::One) + .0 + .1 + .clone(), + }], + }; + + let tx2 = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut { + value: 7_000, + script_pubkey: tracker + .txout_index + .reveal_next_spk(&Keychain::Two) + .0 + .1 + .clone(), + }], + }; + + let tx_coinbase = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![TxIn::default()], + output: vec![TxOut { + value: 11_000, + script_pubkey: tracker + .txout_index + .reveal_next_spk(&Keychain::Two) + .0 + .1 + .clone(), + }], + }; + + assert!(tx_coinbase.is_coin_base()); + + let _ = tracker + .insert_checkpoint(BlockId { + height: 5, + hash: h!("1"), + }) + .unwrap(); + + let should_trust = |keychain: &Keychain| match keychain { + &Keychain::One => false, + &Keychain::Two => true, + }; + + assert_eq!(tracker.balance(should_trust), Balance::default()); + + let _ = tracker + .insert_tx(tx1.clone(), TxHeight::Unconfirmed) + .unwrap(); + + assert_eq!( + tracker.balance(should_trust), + Balance { + untrusted_pending: 13_000, + ..Default::default() + } + ); + + let _ = tracker + .insert_tx(tx2.clone(), TxHeight::Unconfirmed) + .unwrap(); + + assert_eq!( + tracker.balance(should_trust), + Balance { + trusted_pending: 7_000, + untrusted_pending: 13_000, + ..Default::default() + } + ); + + let _ = tracker + .insert_tx(tx_coinbase, TxHeight::Confirmed(0)) + .unwrap(); + + assert_eq!( + tracker.balance(should_trust), + Balance { + trusted_pending: 7_000, + untrusted_pending: 13_000, + immature: 11_000, + ..Default::default() + } + ); + + let _ = tracker + .insert_tx(tx1.clone(), TxHeight::Confirmed(1)) + .unwrap(); + + assert_eq!( + tracker.balance(should_trust), + Balance { + trusted_pending: 7_000, + untrusted_pending: 0, + immature: 11_000, + confirmed: 13_000, + } + ); + + let _ = tracker + .insert_tx(tx2.clone(), TxHeight::Confirmed(2)) + .unwrap(); + + assert_eq!( + tracker.balance(should_trust), + Balance { + trusted_pending: 0, + untrusted_pending: 0, + immature: 11_000, + confirmed: 20_000, + } + ); + + let _ = tracker + .insert_checkpoint(BlockId { + height: 98, + hash: h!("98"), + }) + .unwrap(); + + assert_eq!( + tracker.balance(should_trust), + Balance { + trusted_pending: 0, + untrusted_pending: 0, + immature: 11_000, + confirmed: 20_000, + } + ); + + let _ = tracker + .insert_checkpoint(BlockId { + height: 99, + hash: h!("99"), + }) + .unwrap(); + + assert_eq!( + tracker.balance(should_trust), + Balance { + trusted_pending: 0, + untrusted_pending: 0, + immature: 0, + confirmed: 31_000, + } + ); + + assert_eq!(tracker.balance_at(0), 0); + assert_eq!(tracker.balance_at(1), 13_000); + assert_eq!(tracker.balance_at(2), 20_000); + assert_eq!(tracker.balance_at(98), 20_000); + assert_eq!(tracker.balance_at(99), 31_000); + assert_eq!(tracker.balance_at(100), 31_000); +} diff --git a/crates/chain/tests/test_keychain_txout_index.rs b/crates/chain/tests/test_keychain_txout_index.rs new file mode 100644 index 00000000..48be994d --- /dev/null +++ b/crates/chain/tests/test_keychain_txout_index.rs @@ -0,0 +1,328 @@ +#![cfg(feature = "miniscript")] + +#[macro_use] +mod common; +use bdk_chain::{ + collections::BTreeMap, + keychain::{DerivationAdditions, KeychainTxOutIndex}, +}; + +use bitcoin::{secp256k1::Secp256k1, Script, Transaction, TxOut}; +use miniscript::{Descriptor, DescriptorPublicKey}; + +#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)] +enum TestKeychain { + External, + Internal, +} + +fn init_txout_index() -> ( + bdk_chain::keychain::KeychainTxOutIndex, + Descriptor, + Descriptor, +) { + let mut txout_index = bdk_chain::keychain::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(); + + txout_index.add_keychain(TestKeychain::External, external_descriptor.clone()); + txout_index.add_keychain(TestKeychain::Internal, internal_descriptor.clone()); + + (txout_index, external_descriptor, internal_descriptor) +} + +fn spk_at_index(descriptor: &Descriptor, index: u32) -> Script { + descriptor + .derived_descriptor(&Secp256k1::verification_only(), index) + .expect("must derive") + .script_pubkey() +} + +#[test] +fn test_set_all_derivation_indices() { + let (mut txout_index, _, _) = init_txout_index(); + let derive_to: BTreeMap<_, _> = + [(TestKeychain::External, 12), (TestKeychain::Internal, 24)].into(); + assert_eq!( + txout_index.reveal_to_target_multi(&derive_to).1.as_inner(), + &derive_to + ); + assert_eq!(txout_index.last_revealed_indices(), &derive_to); + assert_eq!( + txout_index.reveal_to_target_multi(&derive_to).1, + DerivationAdditions::default(), + "no changes if we set to the same thing" + ); +} + +#[test] +fn test_lookahead() { + let (mut txout_index, external_desc, internal_desc) = init_txout_index(); + + // ensure it does not break anything if lookahead is set multiple times + (0..=10).for_each(|lookahead| txout_index.set_lookahead(&TestKeychain::External, lookahead)); + (0..=20) + .filter(|v| v % 2 == 0) + .for_each(|lookahead| txout_index.set_lookahead(&TestKeychain::Internal, lookahead)); + + assert_eq!(txout_index.inner().all_spks().len(), 30); + + // given: + // - external lookahead set to 10 + // - internal lookahead set to 20 + // when: + // - set external derivation index to value higher than last, but within the lookahead value + // expect: + // - scripts cached in spk_txout_index should increase correctly + // - stored scripts of external keychain should be of expected counts + for index in (0..20).skip_while(|i| i % 2 == 1) { + let (revealed_spks, revealed_additions) = + txout_index.reveal_to_target(&TestKeychain::External, index); + assert_eq!( + revealed_spks.collect::>(), + vec![(index, spk_at_index(&external_desc, index))], + ); + assert_eq!( + revealed_additions.as_inner(), + &[(TestKeychain::External, index)].into() + ); + + assert_eq!( + txout_index.inner().all_spks().len(), + 10 /* external lookahead */ + + 20 /* internal lookahead */ + + index as usize + 1 /* `derived` count */ + ); + assert_eq!( + txout_index + .revealed_spks_of_keychain(&TestKeychain::External) + .count(), + index as usize + 1, + ); + assert_eq!( + txout_index + .revealed_spks_of_keychain(&TestKeychain::Internal) + .count(), + 0, + ); + assert_eq!( + txout_index + .unused_spks_of_keychain(&TestKeychain::External) + .count(), + index as usize + 1, + ); + assert_eq!( + txout_index + .unused_spks_of_keychain(&TestKeychain::Internal) + .count(), + 0, + ); + } + + // given: + // - internal lookahead is 20 + // - internal derivation index is `None` + // when: + // - derivation index is set ahead of current derivation index + lookahead + // expect: + // - scripts cached in spk_txout_index should increase correctly, a.k.a. no scripts are skipped + let (revealed_spks, revealed_additions) = + txout_index.reveal_to_target(&TestKeychain::Internal, 24); + assert_eq!( + revealed_spks.collect::>(), + (0..=24) + .map(|index| (index, spk_at_index(&internal_desc, index))) + .collect::>(), + ); + assert_eq!( + revealed_additions.as_inner(), + &[(TestKeychain::Internal, 24)].into() + ); + assert_eq!( + txout_index.inner().all_spks().len(), + 10 /* external lookahead */ + + 20 /* internal lookahead */ + + 20 /* external stored index count */ + + 25 /* internal stored index count */ + ); + assert_eq!( + txout_index + .revealed_spks_of_keychain(&TestKeychain::Internal) + .count(), + 25, + ); + + // ensure derivation indices are expected for each keychain + let last_external_index = txout_index + .last_revealed_index(&TestKeychain::External) + .expect("already derived"); + let last_internal_index = txout_index + .last_revealed_index(&TestKeychain::Internal) + .expect("already derived"); + assert_eq!(last_external_index, 19); + assert_eq!(last_internal_index, 24); + + // when: + // - scanning txouts with spks within stored indexes + // expect: + // - no changes to stored index counts + let external_iter = 0..=last_external_index; + let internal_iter = last_internal_index - last_external_index..=last_internal_index; + for (external_index, internal_index) in external_iter.zip(internal_iter) { + let tx = Transaction { + output: vec![ + TxOut { + script_pubkey: external_desc + .at_derivation_index(external_index) + .script_pubkey(), + value: 10_000, + }, + TxOut { + script_pubkey: internal_desc + .at_derivation_index(internal_index) + .script_pubkey(), + value: 10_000, + }, + ], + ..common::new_tx(external_index) + }; + assert_eq!(txout_index.scan(&tx), DerivationAdditions::default()); + assert_eq!( + txout_index.last_revealed_index(&TestKeychain::External), + Some(last_external_index) + ); + assert_eq!( + txout_index.last_revealed_index(&TestKeychain::Internal), + Some(last_internal_index) + ); + assert_eq!( + txout_index + .revealed_spks_of_keychain(&TestKeychain::External) + .count(), + last_external_index as usize + 1, + ); + assert_eq!( + txout_index + .revealed_spks_of_keychain(&TestKeychain::Internal) + .count(), + last_internal_index as usize + 1, + ); + } + + // when: + // - scanning txouts with spks above last stored index + // expect: + // - cached scripts count should increase as expected + // - last stored index should increase as expected + // TODO! +} + +#[test] +fn test_wildcard_derivations() { + let (mut txout_index, external_desc, _) = init_txout_index(); + let external_spk_0 = external_desc.at_derivation_index(0).script_pubkey(); + let external_spk_16 = external_desc.at_derivation_index(16).script_pubkey(); + let external_spk_26 = external_desc.at_derivation_index(26).script_pubkey(); + let external_spk_27 = external_desc.at_derivation_index(27).script_pubkey(); + + // - nothing is derived + // - unused list is also empty + // + // - next_derivation_index() == (0, true) + // - derive_new() == ((0, ), DerivationAdditions) + // - next_unused() == ((0, ), DerivationAdditions:is_empty()) + assert_eq!(txout_index.next_index(&TestKeychain::External), (0, true)); + let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External); + assert_eq!(spk, (0_u32, &external_spk_0)); + assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 0)].into()); + let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External); + assert_eq!(spk, (0_u32, &external_spk_0)); + assert_eq!(changeset.as_inner(), &[].into()); + + // - derived till 25 + // - used all spks till 15. + // - used list : [0..=15, 17, 20, 23] + // - unused list: [16, 18, 19, 21, 22, 24, 25] + + // - next_derivation_index() = (26, true) + // - derive_new() = ((26, ), DerivationAdditions) + // - next_unused() == ((16, ), DerivationAdditions::is_empty()) + let _ = txout_index.reveal_to_target(&TestKeychain::External, 25); + + (0..=15) + .into_iter() + .chain([17, 20, 23].into_iter()) + .for_each(|index| assert!(txout_index.mark_used(&TestKeychain::External, index))); + + assert_eq!(txout_index.next_index(&TestKeychain::External), (26, true)); + + let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External); + assert_eq!(spk, (26, &external_spk_26)); + + assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 26)].into()); + + let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External); + assert_eq!(spk, (16, &external_spk_16)); + assert_eq!(changeset.as_inner(), &[].into()); + + // - Use all the derived till 26. + // - next_unused() = ((27, ), DerivationAdditions) + (0..=26).into_iter().for_each(|index| { + txout_index.mark_used(&TestKeychain::External, index); + }); + + let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External); + assert_eq!(spk, (27, &external_spk_27)); + assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 27)].into()); +} + +#[test] +fn test_non_wildcard_derivations() { + let mut txout_index = KeychainTxOutIndex::::default(); + + let secp = bitcoin::secp256k1::Secp256k1::signing_only(); + let (no_wildcard_descriptor, _) = Descriptor::::parse_descriptor(&secp, "wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0)").unwrap(); + let external_spk = no_wildcard_descriptor + .at_derivation_index(0) + .script_pubkey(); + + txout_index.add_keychain(TestKeychain::External, no_wildcard_descriptor); + + // given: + // - `txout_index` with no stored scripts + // expect: + // - next derivation index should be new + // - when we derive a new script, script @ index 0 + // - when we get the next unused script, script @ index 0 + assert_eq!(txout_index.next_index(&TestKeychain::External), (0, true)); + let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External); + assert_eq!(spk, (0, &external_spk)); + assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 0)].into()); + + let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External); + assert_eq!(spk, (0, &external_spk)); + assert_eq!(changeset.as_inner(), &[].into()); + + // given: + // - the non-wildcard descriptor already has a stored and used script + // expect: + // - next derivation index should not be new + // - derive new and next unused should return the old script + // - store_up_to should not panic and return empty additions + assert_eq!(txout_index.next_index(&TestKeychain::External), (0, false)); + txout_index.mark_used(&TestKeychain::External, 0); + + let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External); + assert_eq!(spk, (0, &external_spk)); + assert_eq!(changeset.as_inner(), &[].into()); + + let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External); + assert_eq!(spk, (0, &external_spk)); + assert_eq!(changeset.as_inner(), &[].into()); + let (revealed_spks, revealed_additions) = + txout_index.reveal_to_target(&TestKeychain::External, 200); + assert_eq!(revealed_spks.count(), 0); + assert!(revealed_additions.is_empty()); +} diff --git a/crates/chain/tests/test_sparse_chain.rs b/crates/chain/tests/test_sparse_chain.rs new file mode 100644 index 00000000..76bb22c1 --- /dev/null +++ b/crates/chain/tests/test_sparse_chain.rs @@ -0,0 +1,773 @@ +#[macro_use] +mod common; + +use bdk_chain::{collections::BTreeSet, sparse_chain::*, BlockId, TxHeight}; +use bitcoin::{hashes::Hash, Txid}; +use core::ops::Bound; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)] +pub struct TestIndex(TxHeight, u32); + +impl ChainPosition for TestIndex { + fn height(&self) -> TxHeight { + self.0 + } + + fn max_ord_of_height(height: TxHeight) -> Self { + Self(height, u32::MAX) + } + + fn min_ord_of_height(height: TxHeight) -> Self { + Self(height, u32::MIN) + } +} + +impl TestIndex { + pub fn new(height: H, ext: u32) -> Self + where + H: Into, + { + Self(height.into(), ext) + } +} + +#[test] +fn add_first_checkpoint() { + let chain = SparseChain::default(); + assert_eq!( + chain.determine_changeset(&chain!([0, h!("A")])), + Ok(changeset! { + checkpoints: [(0, Some(h!("A")))], + txids: [] + },), + "add first tip" + ); +} + +#[test] +fn add_second_tip() { + let chain = chain!([0, h!("A")]); + assert_eq!( + chain.determine_changeset(&chain!([0, h!("A")], [1, h!("B")])), + Ok(changeset! { + checkpoints: [(1, Some(h!("B")))], + txids: [] + },), + "extend tip by one" + ); +} + +#[test] +fn two_disjoint_chains_cannot_merge() { + let chain1 = chain!([0, h!("A")]); + let chain2 = chain!([1, h!("B")]); + assert_eq!( + chain1.determine_changeset(&chain2), + Err(UpdateError::NotConnected(0)) + ); +} + +#[test] +fn duplicate_chains_should_merge() { + let chain1 = chain!([0, h!("A")]); + let chain2 = chain!([0, h!("A")]); + assert_eq!( + chain1.determine_changeset(&chain2), + Ok(ChangeSet::default()) + ); +} + +#[test] +fn duplicate_chains_with_txs_should_merge() { + let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]); + let chain2 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]); + assert_eq!( + chain1.determine_changeset(&chain2), + Ok(ChangeSet::default()) + ); +} + +#[test] +fn duplicate_chains_with_different_txs_should_merge() { + let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]); + let chain2 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx1"), TxHeight::Confirmed(0))]); + assert_eq!( + chain1.determine_changeset(&chain2), + Ok(changeset! { + checkpoints: [], + txids: [(h!("tx1"), Some(TxHeight::Confirmed(0)))] + }) + ); +} + +#[test] +fn invalidate_first_and_only_checkpoint_without_tx_changes() { + let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]); + let chain2 = chain!(checkpoints: [[0,h!("A'")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]); + assert_eq!( + chain1.determine_changeset(&chain2), + Ok(changeset! { + checkpoints: [(0, Some(h!("A'")))], + txids: [] + },) + ); +} + +#[test] +fn invalidate_first_and_only_checkpoint_with_tx_move_forward() { + let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]); + let chain2 = chain!(checkpoints: [[0,h!("A'")],[1, h!("B")]], txids: [(h!("tx0"), TxHeight::Confirmed(1))]); + assert_eq!( + chain1.determine_changeset(&chain2), + Ok(changeset! { + checkpoints: [(0, Some(h!("A'"))), (1, Some(h!("B")))], + txids: [(h!("tx0"), Some(TxHeight::Confirmed(1)))] + },) + ); +} + +#[test] +fn invalidate_first_and_only_checkpoint_with_tx_move_backward() { + let chain1 = chain!(checkpoints: [[1,h!("B")]], txids: [(h!("tx0"), TxHeight::Confirmed(1))]); + let chain2 = chain!(checkpoints: [[0,h!("A")],[1, h!("B'")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]); + assert_eq!( + chain1.determine_changeset(&chain2), + Ok(changeset! { + checkpoints: [(0, Some(h!("A"))), (1, Some(h!("B'")))], + txids: [(h!("tx0"), Some(TxHeight::Confirmed(0)))] + },) + ); +} + +#[test] +fn invalidate_a_checkpoint_and_try_and_move_tx_when_it_wasnt_within_invalidation() { + let chain1 = chain!(checkpoints: [[0, h!("A")], [1, h!("B")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]); + let chain2 = chain!(checkpoints: [[0, h!("A")], [1, h!("B'")]], txids: [(h!("tx0"), TxHeight::Confirmed(1))]); + assert_eq!( + chain1.determine_changeset(&chain2), + Err(UpdateError::TxInconsistent { + txid: h!("tx0"), + original_pos: TxHeight::Confirmed(0).into(), + update_pos: TxHeight::Confirmed(1).into(), + }) + ); +} + +/// This test doesn't make much sense. We're invalidating a block at height 1 and moving it to +/// height 0. It should be impossible for it to be at height 1 at any point if it was at height 0 +/// all along. +#[test] +fn move_invalidated_tx_into_earlier_checkpoint() { + let chain1 = chain!(checkpoints: [[0, h!("A")], [1, h!("B")]], txids: [(h!("tx0"), TxHeight::Confirmed(1))]); + let chain2 = chain!(checkpoints: [[0, h!("A")], [1, h!("B'")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]); + assert_eq!( + chain1.determine_changeset(&chain2), + Ok(changeset! { + checkpoints: [(1, Some(h!("B'")))], + txids: [(h!("tx0"), Some(TxHeight::Confirmed(0)))] + },) + ); +} + +#[test] +fn invalidate_first_and_only_checkpoint_with_tx_move_to_mempool() { + let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]); + let chain2 = chain!(checkpoints: [[0,h!("A'")]], txids: [(h!("tx0"), TxHeight::Unconfirmed)]); + assert_eq!( + chain1.determine_changeset(&chain2), + Ok(changeset! { + checkpoints: [(0, Some(h!("A'")))], + txids: [(h!("tx0"), Some(TxHeight::Unconfirmed))] + },) + ); +} + +#[test] +fn confirm_tx_without_extending_chain() { + let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Unconfirmed)]); + let chain2 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]); + assert_eq!( + chain1.determine_changeset(&chain2), + Ok(changeset! { + checkpoints: [], + txids: [(h!("tx0"), Some(TxHeight::Confirmed(0)))] + },) + ); +} + +#[test] +fn confirm_tx_backwards_while_extending_chain() { + let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Unconfirmed)]); + let chain2 = chain!(checkpoints: [[0,h!("A")],[1,h!("B")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]); + assert_eq!( + chain1.determine_changeset(&chain2), + Ok(changeset! { + checkpoints: [(1, Some(h!("B")))], + txids: [(h!("tx0"), Some(TxHeight::Confirmed(0)))] + },) + ); +} + +#[test] +fn confirm_tx_in_new_block() { + let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Unconfirmed)]); + let chain2 = chain! { + checkpoints: [[0,h!("A")], [1,h!("B")]], + txids: [(h!("tx0"), TxHeight::Confirmed(1))] + }; + assert_eq!( + chain1.determine_changeset(&chain2), + Ok(changeset! { + checkpoints: [(1, Some(h!("B")))], + txids: [(h!("tx0"), Some(TxHeight::Confirmed(1)))] + },) + ); +} + +#[test] +fn merging_mempool_of_empty_chains_doesnt_fail() { + let chain1 = chain!(checkpoints: [], txids: [(h!("tx0"), TxHeight::Unconfirmed)]); + let chain2 = chain!(checkpoints: [], txids: [(h!("tx1"), TxHeight::Unconfirmed)]); + + assert_eq!( + chain1.determine_changeset(&chain2), + Ok(changeset! { + checkpoints: [], + txids: [(h!("tx1"), Some(TxHeight::Unconfirmed))] + },) + ); +} + +#[test] +fn cannot_insert_confirmed_tx_without_checkpoints() { + let chain = SparseChain::default(); + assert_eq!( + chain.insert_tx_preview(h!("A"), TxHeight::Confirmed(0)), + Err(InsertTxError::TxTooHigh { + txid: h!("A"), + tx_height: 0, + tip_height: None + }) + ); +} + +#[test] +fn empty_chain_can_add_unconfirmed_transactions() { + let chain1 = chain!(checkpoints: [[0, h!("A")]], txids: []); + let chain2 = chain!(checkpoints: [], txids: [(h!("tx0"), TxHeight::Unconfirmed)]); + + assert_eq!( + chain1.determine_changeset(&chain2), + Ok(changeset! { + checkpoints: [], + txids: [ (h!("tx0"), Some(TxHeight::Unconfirmed)) ] + },) + ); +} + +#[test] +fn can_update_with_shorter_chain() { + let chain1 = chain!(checkpoints: [[1, h!("B")],[2, h!("C")]], txids: []); + let chain2 = chain!(checkpoints: [[1, h!("B")]], txids: [(h!("tx0"), TxHeight::Confirmed(1))]); + + assert_eq!( + chain1.determine_changeset(&chain2), + Ok(changeset! { + checkpoints: [], + txids: [(h!("tx0"), Some(TxHeight::Confirmed(1)))] + },) + ) +} + +#[test] +fn can_introduce_older_checkpoints() { + let chain1 = chain!(checkpoints: [[2, h!("C")], [3, h!("D")]], txids: []); + let chain2 = chain!(checkpoints: [[1, h!("B")], [2, h!("C")]], txids: []); + + assert_eq!( + chain1.determine_changeset(&chain2), + Ok(changeset! { + checkpoints: [(1, Some(h!("B")))], + txids: [] + },) + ); +} + +#[test] +fn fix_blockhash_before_agreement_point() { + let chain1 = chain!([0, h!("im-wrong")], [1, h!("we-agree")]); + let chain2 = chain!([0, h!("fix")], [1, h!("we-agree")]); + + assert_eq!( + chain1.determine_changeset(&chain2), + Ok(changeset! { + checkpoints: [(0, Some(h!("fix")))], + txids: [] + },) + ) +} + +// TODO: Use macro +#[test] +fn cannot_change_ext_index_of_confirmed_tx() { + let chain1 = chain!( + index: TestIndex, + checkpoints: [[1, h!("A")]], + txids: [(h!("tx0"), TestIndex(TxHeight::Confirmed(1), 10))] + ); + let chain2 = chain!( + index: TestIndex, + checkpoints: [[1, h!("A")]], + txids: [(h!("tx0"), TestIndex(TxHeight::Confirmed(1), 20))] + ); + + assert_eq!( + chain1.determine_changeset(&chain2), + Err(UpdateError::TxInconsistent { + txid: h!("tx0"), + original_pos: TestIndex(TxHeight::Confirmed(1), 10), + update_pos: TestIndex(TxHeight::Confirmed(1), 20), + }), + ) +} + +#[test] +fn can_change_index_of_unconfirmed_tx() { + let chain1 = chain!( + index: TestIndex, + checkpoints: [[1, h!("A")]], + txids: [(h!("tx1"), TestIndex(TxHeight::Unconfirmed, 10))] + ); + let chain2 = chain!( + index: TestIndex, + checkpoints: [[1, h!("A")]], + txids: [(h!("tx1"), TestIndex(TxHeight::Unconfirmed, 20))] + ); + + assert_eq!( + chain1.determine_changeset(&chain2), + Ok(ChangeSet { + checkpoints: [].into(), + txids: [(h!("tx1"), Some(TestIndex(TxHeight::Unconfirmed, 20)),)].into() + },), + ) +} + +/// B and C are in both chain and update +/// ``` +/// | 0 | 1 | 2 | 3 | 4 +/// chain | B C +/// update | A B C D +/// ``` +/// This should succeed with the point of agreement being C and A should be added in addition. +#[test] +fn two_points_of_agreement() { + let chain1 = chain!([1, h!("B")], [2, h!("C")]); + let chain2 = chain!([0, h!("A")], [1, h!("B")], [2, h!("C")], [3, h!("D")]); + + assert_eq!( + chain1.determine_changeset(&chain2), + Ok(changeset! { + checkpoints: [(0, Some(h!("A"))), (3, Some(h!("D")))] + },), + ); +} + +/// Update and chain does not connect: +/// ``` +/// | 0 | 1 | 2 | 3 | 4 +/// chain | B C +/// update | A B D +/// ``` +/// This should fail as we cannot figure out whether C & D are on the same chain +#[test] +fn update_and_chain_does_not_connect() { + let chain1 = chain!([1, h!("B")], [2, h!("C")]); + let chain2 = chain!([0, h!("A")], [1, h!("B")], [3, h!("D")]); + + assert_eq!( + chain1.determine_changeset(&chain2), + Err(UpdateError::NotConnected(2)), + ); +} + +/// Transient invalidation: +/// ``` +/// | 0 | 1 | 2 | 3 | 4 | 5 +/// chain | A B C E +/// update | A B' C' D +/// ``` +/// This should succeed and invalidate B,C and E with point of agreement being A. +/// It should also invalidate transactions at height 1. +#[test] +fn transitive_invalidation_applies_to_checkpoints_higher_than_invalidation() { + let chain1 = chain! { + checkpoints: [[0, h!("A")], [2, h!("B")], [3, h!("C")], [5, h!("E")]], + txids: [ + (h!("a"), TxHeight::Confirmed(0)), + (h!("b1"), TxHeight::Confirmed(1)), + (h!("b2"), TxHeight::Confirmed(2)), + (h!("d"), TxHeight::Confirmed(3)), + (h!("e"), TxHeight::Confirmed(5)) + ] + }; + let chain2 = chain! { + checkpoints: [[0, h!("A")], [2, h!("B'")], [3, h!("C'")], [4, h!("D")]], + txids: [(h!("b1"), TxHeight::Confirmed(4)), (h!("b2"), TxHeight::Confirmed(3))] + }; + + assert_eq!( + chain1.determine_changeset(&chain2), + Ok(changeset! { + checkpoints: [ + (2, Some(h!("B'"))), + (3, Some(h!("C'"))), + (4, Some(h!("D"))), + (5, None) + ], + txids: [ + (h!("b1"), Some(TxHeight::Confirmed(4))), + (h!("b2"), Some(TxHeight::Confirmed(3))), + (h!("d"), Some(TxHeight::Unconfirmed)), + (h!("e"), Some(TxHeight::Unconfirmed)) + ] + },) + ); +} + +/// Transient invalidation: +/// ``` +/// | 0 | 1 | 2 | 3 | 4 +/// chain | B C E +/// update | B' C' D +/// ``` +/// +/// This should succeed and invalidate B, C and E with no point of agreement +#[test] +fn transitive_invalidation_applies_to_checkpoints_higher_than_invalidation_no_point_of_agreement() { + let chain1 = chain!([1, h!("B")], [2, h!("C")], [4, h!("E")]); + let chain2 = chain!([1, h!("B'")], [2, h!("C'")], [3, h!("D")]); + + assert_eq!( + chain1.determine_changeset(&chain2), + Ok(changeset! { + checkpoints: [ + (1, Some(h!("B'"))), + (2, Some(h!("C'"))), + (3, Some(h!("D"))), + (4, None) + ] + },) + ) +} + +/// Transient invalidation: +/// ``` +/// | 0 | 1 | 2 | 3 | 4 +/// chain | A B C E +/// update | B' C' D +/// ``` +/// +/// This should fail since although it tells us that B and C are invalid it doesn't tell us whether +/// A was invalid. +#[test] +fn invalidation_but_no_connection() { + let chain1 = chain!([0, h!("A")], [1, h!("B")], [2, h!("C")], [4, h!("E")]); + let chain2 = chain!([1, h!("B'")], [2, h!("C'")], [3, h!("D")]); + + assert_eq!( + chain1.determine_changeset(&chain2), + Err(UpdateError::NotConnected(0)) + ) +} + +#[test] +fn checkpoint_limit_is_respected() { + let mut chain1 = SparseChain::default(); + let _ = chain1 + .apply_update(chain!( + [1, h!("A")], + [2, h!("B")], + [3, h!("C")], + [4, h!("D")], + [5, h!("E")] + )) + .unwrap(); + + assert_eq!(chain1.checkpoints().len(), 5); + chain1.set_checkpoint_limit(Some(4)); + assert_eq!(chain1.checkpoints().len(), 4); + + let _ = chain1 + .insert_checkpoint(BlockId { + height: 6, + hash: h!("F"), + }) + .unwrap(); + assert_eq!(chain1.checkpoints().len(), 4); + + let changeset = chain1.determine_changeset(&chain!([6, h!("F")], [7, h!("G")])); + assert_eq!(changeset, Ok(changeset!(checkpoints: [(7, Some(h!("G")))]))); + + chain1.apply_changeset(changeset.unwrap()); + + assert_eq!(chain1.checkpoints().len(), 4); +} + +#[test] +fn range_txids_by_height() { + let mut chain = chain!(index: TestIndex, checkpoints: [[1, h!("block 1")], [2, h!("block 2")]]); + + let txids: [(TestIndex, Txid); 4] = [ + ( + TestIndex(TxHeight::Confirmed(1), u32::MIN), + Txid::from_inner([0x00; 32]), + ), + ( + TestIndex(TxHeight::Confirmed(1), u32::MAX), + Txid::from_inner([0xfe; 32]), + ), + ( + TestIndex(TxHeight::Confirmed(2), u32::MIN), + Txid::from_inner([0x01; 32]), + ), + ( + TestIndex(TxHeight::Confirmed(2), u32::MAX), + Txid::from_inner([0xff; 32]), + ), + ]; + + // populate chain with txids + for (index, txid) in txids { + let _ = chain.insert_tx(txid, index).expect("should succeed"); + } + + // inclusive start + assert_eq!( + chain + .range_txids_by_height(TxHeight::Confirmed(1)..) + .collect::>(), + txids.iter().collect::>(), + ); + + // exclusive start + assert_eq!( + chain + .range_txids_by_height((Bound::Excluded(TxHeight::Confirmed(1)), Bound::Unbounded,)) + .collect::>(), + txids[2..].iter().collect::>(), + ); + + // inclusive end + assert_eq!( + chain + .range_txids_by_height((Bound::Unbounded, Bound::Included(TxHeight::Confirmed(2)))) + .collect::>(), + txids[..4].iter().collect::>(), + ); + + // exclusive end + assert_eq!( + chain + .range_txids_by_height(..TxHeight::Confirmed(2)) + .collect::>(), + txids[..2].iter().collect::>(), + ); +} + +#[test] +fn range_txids_by_index() { + let mut chain = chain!(index: TestIndex, checkpoints: [[1, h!("block 1")],[2, h!("block 2")]]); + + let txids: [(TestIndex, Txid); 4] = [ + (TestIndex(TxHeight::Confirmed(1), u32::MIN), h!("tx 1 min")), + (TestIndex(TxHeight::Confirmed(1), u32::MAX), h!("tx 1 max")), + (TestIndex(TxHeight::Confirmed(2), u32::MIN), h!("tx 2 min")), + (TestIndex(TxHeight::Confirmed(2), u32::MAX), h!("tx 2 max")), + ]; + + // populate chain with txids + for (index, txid) in txids { + let _ = chain.insert_tx(txid, index).expect("should succeed"); + } + + // inclusive start + assert_eq!( + chain + .range_txids_by_position(TestIndex(TxHeight::Confirmed(1), u32::MIN)..) + .collect::>(), + txids.iter().collect::>(), + ); + assert_eq!( + chain + .range_txids_by_position(TestIndex(TxHeight::Confirmed(1), u32::MAX)..) + .collect::>(), + txids[1..].iter().collect::>(), + ); + + // exclusive start + assert_eq!( + chain + .range_txids_by_position(( + Bound::Excluded(TestIndex(TxHeight::Confirmed(1), u32::MIN)), + Bound::Unbounded + )) + .collect::>(), + txids[1..].iter().collect::>(), + ); + assert_eq!( + chain + .range_txids_by_position(( + Bound::Excluded(TestIndex(TxHeight::Confirmed(1), u32::MAX)), + Bound::Unbounded + )) + .collect::>(), + txids[2..].iter().collect::>(), + ); + + // inclusive end + assert_eq!( + chain + .range_txids_by_position(( + Bound::Unbounded, + Bound::Included(TestIndex(TxHeight::Confirmed(2), u32::MIN)) + )) + .collect::>(), + txids[..3].iter().collect::>(), + ); + assert_eq!( + chain + .range_txids_by_position(( + Bound::Unbounded, + Bound::Included(TestIndex(TxHeight::Confirmed(2), u32::MAX)) + )) + .collect::>(), + txids[..4].iter().collect::>(), + ); + + // exclusive end + assert_eq!( + chain + .range_txids_by_position(..TestIndex(TxHeight::Confirmed(2), u32::MIN)) + .collect::>(), + txids[..2].iter().collect::>(), + ); + assert_eq!( + chain + .range_txids_by_position(..TestIndex(TxHeight::Confirmed(2), u32::MAX)) + .collect::>(), + txids[..3].iter().collect::>(), + ); +} + +#[test] +fn range_txids() { + let mut chain = SparseChain::default(); + + let txids = (0..100) + .map(|v| Txid::hash(v.to_string().as_bytes())) + .collect::>(); + + // populate chain + for txid in &txids { + let _ = chain + .insert_tx(*txid, TxHeight::Unconfirmed) + .expect("should succeed"); + } + + for txid in &txids { + assert_eq!( + chain + .range_txids((TxHeight::Unconfirmed, *txid)..) + .map(|(_, txid)| txid) + .collect::>(), + txids.range(*txid..).collect::>(), + "range with inclusive start should succeed" + ); + + assert_eq!( + chain + .range_txids(( + Bound::Excluded((TxHeight::Unconfirmed, *txid)), + Bound::Unbounded, + )) + .map(|(_, txid)| txid) + .collect::>(), + txids + .range((Bound::Excluded(*txid), Bound::Unbounded,)) + .collect::>(), + "range with exclusive start should succeed" + ); + + assert_eq!( + chain + .range_txids(..(TxHeight::Unconfirmed, *txid)) + .map(|(_, txid)| txid) + .collect::>(), + txids.range(..*txid).collect::>(), + "range with exclusive end should succeed" + ); + + assert_eq!( + chain + .range_txids(( + Bound::Included((TxHeight::Unconfirmed, *txid)), + Bound::Unbounded, + )) + .map(|(_, txid)| txid) + .collect::>(), + txids + .range((Bound::Included(*txid), Bound::Unbounded,)) + .collect::>(), + "range with inclusive end should succeed" + ); + } +} + +#[test] +fn invalidated_txs_move_to_unconfirmed() { + let chain1 = chain! { + checkpoints: [[0, h!("A")], [1, h!("B")], [2, h!("C")]], + txids: [ + (h!("a"), TxHeight::Confirmed(0)), + (h!("b"), TxHeight::Confirmed(1)), + (h!("c"), TxHeight::Confirmed(2)), + (h!("d"), TxHeight::Unconfirmed) + ] + }; + + let chain2 = chain!([0, h!("A")], [1, h!("B'")]); + + assert_eq!( + chain1.determine_changeset(&chain2), + Ok(changeset! { + checkpoints: [ + (1, Some(h!("B'"))), + (2, None) + ], + txids: [ + (h!("b"), Some(TxHeight::Unconfirmed)), + (h!("c"), Some(TxHeight::Unconfirmed)) + ] + },) + ); +} + +#[test] +fn change_tx_position_from_unconfirmed_to_confirmed() { + let mut chain = SparseChain::::default(); + let txid = h!("txid"); + + let _ = chain.insert_tx(txid, TxHeight::Unconfirmed).unwrap(); + + assert_eq!(chain.tx_position(txid), Some(&TxHeight::Unconfirmed)); + let _ = chain + .insert_checkpoint(BlockId { + height: 0, + hash: h!("0"), + }) + .unwrap(); + let _ = chain.insert_tx(txid, TxHeight::Confirmed(0)).unwrap(); + + assert_eq!(chain.tx_position(txid), Some(&TxHeight::Confirmed(0))); +} diff --git a/crates/chain/tests/test_spk_txout_index.rs b/crates/chain/tests/test_spk_txout_index.rs new file mode 100644 index 00000000..a49df08b --- /dev/null +++ b/crates/chain/tests/test_spk_txout_index.rs @@ -0,0 +1,101 @@ +use bdk_chain::SpkTxOutIndex; +use bitcoin::{hashes::hex::FromHex, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut}; + +#[test] +fn spk_txout_sent_and_received() { + let spk1 = Script::from_hex("001404f1e52ce2bab3423c6a8c63b7cd730d8f12542c").unwrap(); + let spk2 = Script::from_hex("00142b57404ae14f08c3a0c903feb2af7830605eb00f").unwrap(); + + let mut index = SpkTxOutIndex::default(); + index.insert_spk(0, spk1.clone()); + index.insert_spk(1, spk2.clone()); + + let tx1 = Transaction { + version: 0x02, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut { + value: 42_000, + script_pubkey: spk1.clone(), + }], + }; + + assert_eq!(index.sent_and_received(&tx1), (0, 42_000)); + assert_eq!(index.net_value(&tx1), 42_000); + index.scan(&tx1); + assert_eq!( + index.sent_and_received(&tx1), + (0, 42_000), + "shouldn't change after scanning" + ); + + let tx2 = Transaction { + version: 0x1, + lock_time: PackedLockTime(0), + input: vec![TxIn { + previous_output: OutPoint { + txid: tx1.txid(), + vout: 0, + }, + ..Default::default() + }], + output: vec![ + TxOut { + value: 20_000, + script_pubkey: spk2.clone(), + }, + TxOut { + script_pubkey: spk1.clone(), + value: 30_000, + }, + ], + }; + + assert_eq!(index.sent_and_received(&tx2), (42_000, 50_000)); + assert_eq!(index.net_value(&tx2), 8_000); +} + +#[test] +fn mark_used() { + let spk1 = Script::from_hex("001404f1e52ce2bab3423c6a8c63b7cd730d8f12542c").unwrap(); + let spk2 = Script::from_hex("00142b57404ae14f08c3a0c903feb2af7830605eb00f").unwrap(); + + let mut spk_index = SpkTxOutIndex::default(); + spk_index.insert_spk(1, spk1.clone()); + spk_index.insert_spk(2, spk2.clone()); + + assert_eq!(spk_index.is_used(&1), false); + spk_index.mark_used(&1); + assert_eq!(spk_index.is_used(&1), true); + spk_index.unmark_used(&1); + assert_eq!(spk_index.is_used(&1), false); + spk_index.mark_used(&1); + assert_eq!(spk_index.is_used(&1), true); + + let tx1 = Transaction { + version: 0x02, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut { + value: 42_000, + script_pubkey: spk1.clone(), + }], + }; + + spk_index.scan(&tx1); + spk_index.unmark_used(&1); + assert_eq!( + spk_index.is_used(&1), + true, + "even though we unmark_used it doesn't matter because there was a tx scanned that used it" + ); +} + +#[test] +fn unmark_used_does_not_result_in_invalid_representation() { + let mut spk_index = SpkTxOutIndex::default(); + assert_eq!(spk_index.unmark_used(&0), false); + assert_eq!(spk_index.unmark_used(&1), false); + assert_eq!(spk_index.unmark_used(&2), false); + assert!(spk_index.unused_spks(..).collect::>().is_empty()); +} diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs new file mode 100644 index 00000000..b7e333c8 --- /dev/null +++ b/crates/chain/tests/test_tx_graph.rs @@ -0,0 +1,512 @@ +#[macro_use] +mod common; +use bdk_chain::{ + collections::*, + tx_graph::{Additions, TxGraph}, +}; +use bitcoin::{hashes::Hash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid}; +use core::iter; + +#[test] +fn insert_txouts() { + let original_ops = [ + ( + OutPoint::new(h!("tx1"), 1), + TxOut { + value: 10_000, + script_pubkey: Script::new(), + }, + ), + ( + OutPoint::new(h!("tx1"), 2), + TxOut { + value: 20_000, + script_pubkey: Script::new(), + }, + ), + ]; + + let update_ops = [( + OutPoint::new(h!("tx2"), 0), + TxOut { + value: 20_000, + script_pubkey: Script::new(), + }, + )]; + + let mut graph = { + let mut graph = TxGraph::::default(); + for (outpoint, txout) in &original_ops { + assert_eq!( + graph.insert_txout(*outpoint, txout.clone()), + Additions { + txout: [(*outpoint, txout.clone())].into(), + ..Default::default() + } + ); + } + graph + }; + + let update = { + let mut graph = TxGraph::::default(); + for (outpoint, txout) in &update_ops { + assert_eq!( + graph.insert_txout(*outpoint, txout.clone()), + Additions { + txout: [(*outpoint, txout.clone())].into(), + ..Default::default() + } + ); + } + graph + }; + + let additions = graph.determine_additions(&update); + + assert_eq!( + additions, + Additions { + tx: [].into(), + txout: update_ops.into(), + } + ); + + graph.apply_additions(additions); + assert_eq!(graph.all_txouts().count(), 3); + assert_eq!(graph.full_transactions().count(), 0); + assert_eq!(graph.partial_transactions().count(), 2); +} + +#[test] +fn insert_tx_graph_doesnt_count_coinbase_as_spent() { + let tx = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![TxIn { + previous_output: OutPoint::null(), + ..Default::default() + }], + output: vec![], + }; + + let mut graph = TxGraph::default(); + let _ = graph.insert_tx(tx); + assert!(graph.outspends(OutPoint::null()).is_empty()); + assert!(graph.tx_outspends(Txid::all_zeros()).next().is_none()); +} + +#[test] +fn insert_tx_graph_keeps_track_of_spend() { + let tx1 = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut::default()], + }; + + let op = OutPoint { + txid: tx1.txid(), + vout: 0, + }; + + let tx2 = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![TxIn { + previous_output: op, + ..Default::default() + }], + output: vec![], + }; + + let mut graph1 = TxGraph::default(); + let mut graph2 = TxGraph::default(); + + // insert in different order + let _ = graph1.insert_tx(tx1.clone()); + let _ = graph1.insert_tx(tx2.clone()); + + let _ = graph2.insert_tx(tx2.clone()); + let _ = graph2.insert_tx(tx1.clone()); + + assert_eq!( + &*graph1.outspends(op), + &iter::once(tx2.txid()).collect::>() + ); + assert_eq!(graph2.outspends(op), graph1.outspends(op)); +} + +#[test] +fn insert_tx_can_retrieve_full_tx_from_graph() { + let tx = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![TxIn { + previous_output: OutPoint::null(), + ..Default::default() + }], + output: vec![TxOut::default()], + }; + + let mut graph = TxGraph::default(); + let _ = graph.insert_tx(tx.clone()); + assert_eq!(graph.get_tx(tx.txid()), Some(&tx)); +} + +#[test] +fn insert_tx_displaces_txouts() { + let mut tx_graph = TxGraph::default(); + let tx = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut { + value: 42_000, + script_pubkey: Script::default(), + }], + }; + + let _ = tx_graph.insert_txout( + OutPoint { + txid: tx.txid(), + vout: 0, + }, + TxOut { + value: 1337_000, + script_pubkey: Script::default(), + }, + ); + + let _ = tx_graph.insert_txout( + OutPoint { + txid: tx.txid(), + vout: 0, + }, + TxOut { + value: 1_000_000_000, + script_pubkey: Script::default(), + }, + ); + + let _additions = tx_graph.insert_tx(tx.clone()); + + assert_eq!( + tx_graph + .get_txout(OutPoint { + txid: tx.txid(), + vout: 0 + }) + .unwrap() + .value, + 42_000 + ); + assert_eq!( + tx_graph.get_txout(OutPoint { + txid: tx.txid(), + vout: 1 + }), + None + ); +} + +#[test] +fn insert_txout_does_not_displace_tx() { + let mut tx_graph = TxGraph::default(); + let tx = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut { + value: 42_000, + script_pubkey: Script::default(), + }], + }; + + let _additions = tx_graph.insert_tx(tx.clone()); + + let _ = tx_graph.insert_txout( + OutPoint { + txid: tx.txid(), + vout: 0, + }, + TxOut { + value: 1337_000, + script_pubkey: Script::default(), + }, + ); + + let _ = tx_graph.insert_txout( + OutPoint { + txid: tx.txid(), + vout: 0, + }, + TxOut { + value: 1_000_000_000, + script_pubkey: Script::default(), + }, + ); + + assert_eq!( + tx_graph + .get_txout(OutPoint { + txid: tx.txid(), + vout: 0 + }) + .unwrap() + .value, + 42_000 + ); + assert_eq!( + tx_graph.get_txout(OutPoint { + txid: tx.txid(), + vout: 1 + }), + None + ); +} + +#[test] +fn test_calculate_fee() { + let mut graph = TxGraph::default(); + let intx1 = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut { + value: 100, + ..Default::default() + }], + }; + let intx2 = Transaction { + version: 0x02, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut { + value: 200, + ..Default::default() + }], + }; + + let intxout1 = ( + OutPoint { + txid: h!("dangling output"), + vout: 0, + }, + TxOut { + value: 300, + ..Default::default() + }, + ); + + let _ = graph.insert_tx(intx1.clone()); + let _ = graph.insert_tx(intx2.clone()); + let _ = graph.insert_txout(intxout1.0, intxout1.1); + + let mut tx = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![ + TxIn { + previous_output: OutPoint { + txid: intx1.txid(), + vout: 0, + }, + ..Default::default() + }, + TxIn { + previous_output: OutPoint { + txid: intx2.txid(), + vout: 0, + }, + ..Default::default() + }, + TxIn { + previous_output: intxout1.0, + ..Default::default() + }, + ], + output: vec![TxOut { + value: 500, + ..Default::default() + }], + }; + + assert_eq!(graph.calculate_fee(&tx), Some(100)); + + tx.input.remove(2); + + // fee would be negative + assert_eq!(graph.calculate_fee(&tx), Some(-200)); + + // If we have an unknown outpoint, fee should return None. + tx.input.push(TxIn { + previous_output: OutPoint { + txid: h!("unknown_txid"), + vout: 0, + }, + ..Default::default() + }); + assert_eq!(graph.calculate_fee(&tx), None); +} + +#[test] +fn test_calculate_fee_on_coinbase() { + let tx = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![TxIn { + previous_output: OutPoint::null(), + ..Default::default() + }], + output: vec![TxOut::default()], + }; + + let graph = TxGraph::::default(); + + assert_eq!(graph.calculate_fee(&tx), Some(0)); +} + +#[test] +fn test_conflicting_descendants() { + let previous_output = OutPoint::new(h!("op"), 2); + + // tx_a spends previous_output + let tx_a = Transaction { + input: vec![TxIn { + previous_output, + ..TxIn::default() + }], + output: vec![TxOut::default()], + ..common::new_tx(0) + }; + + // tx_a2 spends previous_output and conflicts with tx_a + let tx_a2 = Transaction { + input: vec![TxIn { + previous_output, + ..TxIn::default() + }], + output: vec![TxOut::default(), TxOut::default()], + ..common::new_tx(1) + }; + + // tx_b spends tx_a + let tx_b = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_a.txid(), 0), + ..TxIn::default() + }], + output: vec![TxOut::default()], + ..common::new_tx(2) + }; + + let txid_a = tx_a.txid(); + let txid_b = tx_b.txid(); + + let mut graph = TxGraph::default(); + let _ = graph.insert_tx(tx_a); + let _ = graph.insert_tx(tx_b); + + assert_eq!( + graph + .walk_conflicts(&tx_a2, |depth, txid| Some((depth, txid))) + .collect::>(), + vec![(0_usize, txid_a), (1_usize, txid_b),], + ); +} + +#[test] +fn test_descendants_no_repeat() { + let tx_a = Transaction { + output: vec![TxOut::default(), TxOut::default(), TxOut::default()], + ..common::new_tx(0) + }; + + let txs_b = (0..3) + .map(|vout| Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_a.txid(), vout), + ..TxIn::default() + }], + output: vec![TxOut::default()], + ..common::new_tx(1) + }) + .collect::>(); + + let txs_c = (0..2) + .map(|vout| Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(txs_b[vout as usize].txid(), vout), + ..TxIn::default() + }], + output: vec![TxOut::default()], + ..common::new_tx(2) + }) + .collect::>(); + + let tx_d = Transaction { + input: vec![ + TxIn { + previous_output: OutPoint::new(txs_c[0].txid(), 0), + ..TxIn::default() + }, + TxIn { + previous_output: OutPoint::new(txs_c[1].txid(), 0), + ..TxIn::default() + }, + ], + output: vec![TxOut::default()], + ..common::new_tx(3) + }; + + let tx_e = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_d.txid(), 0), + ..TxIn::default() + }], + output: vec![TxOut::default()], + ..common::new_tx(4) + }; + + let txs_not_connected = (10..20) + .map(|v| Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(h!("tx_does_not_exist"), v), + ..TxIn::default() + }], + output: vec![TxOut::default()], + ..common::new_tx(v) + }) + .collect::>(); + + let mut graph = TxGraph::default(); + let mut expected_txids = BTreeSet::new(); + + // these are NOT descendants of `tx_a` + for tx in txs_not_connected { + let _ = graph.insert_tx(tx.clone()); + } + + // these are the expected descendants of `tx_a` + for tx in txs_b + .iter() + .chain(&txs_c) + .chain(core::iter::once(&tx_d)) + .chain(core::iter::once(&tx_e)) + { + let _ = graph.insert_tx(tx.clone()); + assert!(expected_txids.insert(tx.txid())); + } + + let descendants = graph + .walk_descendants(tx_a.txid(), |_, txid| Some(txid)) + .collect::>(); + + assert_eq!(descendants.len(), expected_txids.len()); + + for txid in descendants { + assert!(expected_txids.remove(&txid)); + } + assert!(expected_txids.is_empty()); +}