diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index 59444d7f..a360b304 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -2,9 +2,29 @@ use bitcoin::{hashes::Hash, BlockHash, OutPoint, TxOut, Txid}; use crate::{ sparse_chain::{self, ChainPosition}, - COINBASE_MATURITY, + Anchor, COINBASE_MATURITY, }; +/// Represents an observation of some chain data. +/// +/// The generic `A` should be a [`Anchor`] implementation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)] +pub enum ObservedAs { + /// The chain data is seen as confirmed, and in anchored by `A`. + Confirmed(A), + /// The chain data is seen in mempool at this given timestamp. + Unconfirmed(u64), +} + +impl ObservedAs<&A> { + pub fn cloned(self) -> ObservedAs { + match self { + ObservedAs::Confirmed(a) => ObservedAs::Confirmed(a.clone()), + ObservedAs::Unconfirmed(last_seen) => ObservedAs::Unconfirmed(last_seen), + } + } +} + /// Represents the height at which a transaction is confirmed. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr( @@ -118,7 +138,7 @@ impl ConfirmationTime { } /// A reference to a block in the canonical chain. -#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)] #[cfg_attr( feature = "serde", derive(serde::Deserialize, serde::Serialize), @@ -140,6 +160,12 @@ impl Default for BlockId { } } +impl Anchor for BlockId { + fn anchor_block(&self) -> BlockId { + *self + } +} + impl From<(u32, BlockHash)> for BlockId { fn from((height, hash): (u32, BlockHash)) -> Self { Self { height, hash } @@ -162,21 +188,21 @@ impl From<(&u32, &BlockHash)> for BlockId { } /// A `TxOut` with as much data as we can retrieve about it -#[derive(Debug, Clone, PartialEq)] -pub struct FullTxOut { +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +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, + pub chain_position: P, /// The txid and chain position of the transaction (if any) that has spent this output. - pub spent_by: Option<(I, Txid)>, + pub spent_by: Option<(P, Txid)>, /// Whether this output is on a coinbase transaction. pub is_on_coinbase: bool, } -impl FullTxOut { +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 @@ -215,4 +241,69 @@ impl FullTxOut { } } -// TODO: make test +impl FullTxOut> { + /// Whether the `txout` is considered mature. + /// + /// This is the alternative version of [`is_mature`] which depends on `chain_position` being a + /// [`ObservedAs`] where `A` implements [`Anchor`]. + /// + /// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this + /// method may return false-negatives. In other words, interpretted confirmation count may be + /// less than the actual value. + /// + /// [`is_mature`]: Self::is_mature + /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound + pub fn is_mature(&self, tip: u32) -> bool { + if self.is_on_coinbase { + let tx_height = match &self.chain_position { + ObservedAs::Confirmed(anchor) => anchor.confirmation_height_upper_bound(), + ObservedAs::Unconfirmed(_) => { + debug_assert!(false, "coinbase tx can never be unconfirmed"); + return false; + } + }; + let age = tip.saturating_sub(tx_height); + if age + 1 < COINBASE_MATURITY { + return false; + } + } + + true + } + + /// Whether the utxo is/was/will be spendable with chain `tip`. + /// + /// This method does not take into account the locktime. + /// + /// This is the alternative version of [`is_spendable_at`] which depends on `chain_position` + /// being a [`ObservedAs`] where `A` implements [`Anchor`]. + /// + /// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this + /// method may return false-negatives. In other words, interpretted confirmation count may be + /// less than the actual value. + /// + /// [`is_spendable_at`]: Self::is_spendable_at + /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound + pub fn is_confirmed_and_spendable(&self, tip: u32) -> bool { + if !self.is_mature(tip) { + return false; + } + + let confirmation_height = match &self.chain_position { + ObservedAs::Confirmed(anchor) => anchor.confirmation_height_upper_bound(), + ObservedAs::Unconfirmed(_) => return false, + }; + if confirmation_height > tip { + return false; + } + + // if the spending tx is confirmed within tip height, the txout is no longer spendable + if let Some((ObservedAs::Confirmed(spending_anchor), _)) = &self.spent_by { + if spending_anchor.anchor_block().height <= tip { + return false; + } + } + + true + } +} diff --git a/crates/chain/src/chain_graph.rs b/crates/chain/src/chain_graph.rs index acf104e7..47845c5a 100644 --- a/crates/chain/src/chain_graph.rs +++ b/crates/chain/src/chain_graph.rs @@ -3,7 +3,7 @@ use crate::{ collections::HashSet, sparse_chain::{self, ChainPosition, SparseChain}, tx_graph::{self, TxGraph}, - BlockId, ForEachTxOut, FullTxOut, TxHeight, + Append, BlockId, ForEachTxOut, FullTxOut, TxHeight, }; use alloc::{string::ToString, vec::Vec}; use bitcoin::{OutPoint, Transaction, TxOut, Txid}; diff --git a/crates/chain/src/chain_oracle.rs b/crates/chain/src/chain_oracle.rs new file mode 100644 index 00000000..2b4ad36f --- /dev/null +++ b/crates/chain/src/chain_oracle.rs @@ -0,0 +1,21 @@ +use crate::BlockId; + +/// Represents a service that tracks the blockchain. +/// +/// The main method is [`is_block_in_chain`] which determines whether a given block of [`BlockId`] +/// is an ancestor of another "static block". +/// +/// [`is_block_in_chain`]: Self::is_block_in_chain +pub trait ChainOracle { + /// Error type. + type Error: core::fmt::Debug; + + /// Determines whether `block` of [`BlockId`] exists as an ancestor of `static_block`. + /// + /// If `None` is returned, it means the implementation cannot determine whether `block` exists. + fn is_block_in_chain( + &self, + block: BlockId, + static_block: BlockId, + ) -> Result, Self::Error>; +} diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs new file mode 100644 index 00000000..6d8c16ff --- /dev/null +++ b/crates/chain/src/indexed_tx_graph.rs @@ -0,0 +1,325 @@ +use core::convert::Infallible; + +use alloc::vec::Vec; +use bitcoin::{OutPoint, Script, Transaction, TxOut}; + +use crate::{ + keychain::Balance, + tx_graph::{Additions, TxGraph}, + Anchor, Append, BlockId, ChainOracle, FullTxOut, ObservedAs, +}; + +/// A struct that combines [`TxGraph`] and an [`Indexer`] implementation. +/// +/// This structure ensures that [`TxGraph`] and [`Indexer`] are updated atomically. +pub struct IndexedTxGraph { + /// Transaction index. + pub index: I, + graph: TxGraph, +} + +impl Default for IndexedTxGraph { + fn default() -> Self { + Self { + graph: Default::default(), + index: Default::default(), + } + } +} + +impl IndexedTxGraph { + /// Get a reference of the internal transaction graph. + pub fn graph(&self) -> &TxGraph { + &self.graph + } + + /// Applies the [`IndexedAdditions`] to the [`IndexedTxGraph`]. + pub fn apply_additions(&mut self, additions: IndexedAdditions) { + let IndexedAdditions { + graph_additions, + index_additions, + } = additions; + + self.index.apply_additions(index_additions); + + for tx in &graph_additions.tx { + self.index.index_tx(tx); + } + for (&outpoint, txout) in &graph_additions.txout { + self.index.index_txout(outpoint, txout); + } + + self.graph.apply_additions(graph_additions); + } +} + +impl IndexedTxGraph +where + I::Additions: Default + Append, +{ + /// Apply an `update` directly. + /// + /// `update` is a [`TxGraph`] and the resultant changes is returned as [`IndexedAdditions`]. + pub fn apply_update(&mut self, update: TxGraph) -> IndexedAdditions { + let graph_additions = self.graph.apply_update(update); + + let mut index_additions = I::Additions::default(); + for added_tx in &graph_additions.tx { + index_additions.append(self.index.index_tx(added_tx)); + } + for (&added_outpoint, added_txout) in &graph_additions.txout { + index_additions.append(self.index.index_txout(added_outpoint, added_txout)); + } + + IndexedAdditions { + graph_additions, + index_additions, + } + } + + /// Insert a floating `txout` of given `outpoint`. + pub fn insert_txout( + &mut self, + outpoint: OutPoint, + txout: &TxOut, + ) -> IndexedAdditions { + let mut update = TxGraph::::default(); + let _ = update.insert_txout(outpoint, txout.clone()); + self.apply_update(update) + } + + /// Insert and index a transaction into the graph. + /// + /// `anchors` can be provided to anchor the transaction to various blocks. `seen_at` is a + /// unix timestamp of when the transaction is last seen. + pub fn insert_tx( + &mut self, + tx: &Transaction, + anchors: impl IntoIterator, + seen_at: Option, + ) -> IndexedAdditions { + let txid = tx.txid(); + + let mut update = TxGraph::::default(); + if self.graph.get_tx(txid).is_none() { + let _ = update.insert_tx(tx.clone()); + } + for anchor in anchors.into_iter() { + let _ = update.insert_anchor(txid, anchor); + } + if let Some(seen_at) = seen_at { + let _ = update.insert_seen_at(txid, seen_at); + } + + self.apply_update(update) + } + + /// Insert relevant transactions from the given `txs` iterator. + /// + /// Relevancy is determined by the [`Indexer::is_tx_relevant`] implementation of `I`. Irrelevant + /// transactions in `txs` will be ignored. `txs` do not need to be in topological order. + /// + /// `anchors` can be provided to anchor the transactions to blocks. `seen_at` is a unix + /// timestamp of when the transactions are last seen. + pub fn insert_relevant_txs<'t>( + &mut self, + txs: impl IntoIterator)>, + seen_at: Option, + ) -> IndexedAdditions { + // The algorithm below allows for non-topologically ordered transactions by using two loops. + // This is achieved by: + // 1. insert all txs into the index. If they are irrelevant then that's fine it will just + // not store anything about them. + // 2. decide whether to insert them into the graph depending on whether `is_tx_relevant` + // returns true or not. (in a second loop). + let mut additions = IndexedAdditions::::default(); + let mut transactions = Vec::new(); + for (tx, anchors) in txs.into_iter() { + additions.index_additions.append(self.index.index_tx(tx)); + transactions.push((tx, anchors)); + } + additions.append( + transactions + .into_iter() + .filter_map(|(tx, anchors)| match self.index.is_tx_relevant(tx) { + true => Some(self.insert_tx(tx, anchors, seen_at)), + false => None, + }) + .fold(Default::default(), |mut acc, other| { + acc.append(other); + acc + }), + ); + additions + } +} + +impl IndexedTxGraph { + pub fn try_list_owned_txouts<'a, C: ChainOracle + 'a>( + &'a self, + chain: &'a C, + chain_tip: BlockId, + ) -> impl Iterator>, C::Error>> + 'a { + self.graph() + .try_list_chain_txouts(chain, chain_tip) + .filter(|r| { + if let Ok(full_txout) = r { + if !self.index.is_spk_owned(&full_txout.txout.script_pubkey) { + return false; + } + } + true + }) + } + + pub fn list_owned_txouts<'a, C: ChainOracle + 'a>( + &'a self, + chain: &'a C, + chain_tip: BlockId, + ) -> impl Iterator>> + 'a { + self.try_list_owned_txouts(chain, chain_tip) + .map(|r| r.expect("oracle is infallible")) + } + + pub fn try_list_owned_unspents<'a, C: ChainOracle + 'a>( + &'a self, + chain: &'a C, + chain_tip: BlockId, + ) -> impl Iterator>, C::Error>> + 'a { + self.graph() + .try_list_chain_unspents(chain, chain_tip) + .filter(|r| { + if let Ok(full_txout) = r { + if !self.index.is_spk_owned(&full_txout.txout.script_pubkey) { + return false; + } + } + true + }) + } + + pub fn list_owned_unspents<'a, C: ChainOracle + 'a>( + &'a self, + chain: &'a C, + chain_tip: BlockId, + ) -> impl Iterator>> + 'a { + self.try_list_owned_unspents(chain, chain_tip) + .map(|r| r.expect("oracle is infallible")) + } + + pub fn try_balance( + &self, + chain: &C, + chain_tip: BlockId, + mut should_trust: F, + ) -> Result + where + C: ChainOracle, + F: FnMut(&Script) -> bool, + { + let tip_height = chain_tip.anchor_block().height; + + let mut immature = 0; + let mut trusted_pending = 0; + let mut untrusted_pending = 0; + let mut confirmed = 0; + + for res in self.try_list_owned_unspents(chain, chain_tip) { + let txout = res?; + + match &txout.chain_position { + ObservedAs::Confirmed(_) => { + if txout.is_confirmed_and_spendable(tip_height) { + confirmed += txout.txout.value; + } else if !txout.is_mature(tip_height) { + immature += txout.txout.value; + } + } + ObservedAs::Unconfirmed(_) => { + if should_trust(&txout.txout.script_pubkey) { + trusted_pending += txout.txout.value; + } else { + untrusted_pending += txout.txout.value; + } + } + } + } + + Ok(Balance { + immature, + trusted_pending, + untrusted_pending, + confirmed, + }) + } + + pub fn balance(&self, chain: &C, chain_tip: BlockId, should_trust: F) -> Balance + where + C: ChainOracle, + F: FnMut(&Script) -> bool, + { + self.try_balance(chain, chain_tip, should_trust) + .expect("error is infallible") + } +} + +/// A structure that represents changes to an [`IndexedTxGraph`]. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde( + crate = "serde_crate", + bound( + deserialize = "A: Ord + serde::Deserialize<'de>, IA: serde::Deserialize<'de>", + serialize = "A: Ord + serde::Serialize, IA: serde::Serialize" + ) + ) +)] +#[must_use] +pub struct IndexedAdditions { + /// [`TxGraph`] additions. + pub graph_additions: Additions, + /// [`Indexer`] additions. + pub index_additions: IA, +} + +impl Default for IndexedAdditions { + fn default() -> Self { + Self { + graph_additions: Default::default(), + index_additions: Default::default(), + } + } +} + +impl Append for IndexedAdditions { + fn append(&mut self, other: Self) { + self.graph_additions.append(other.graph_additions); + self.index_additions.append(other.index_additions); + } +} + +/// Represents a structure that can index transaction data. +pub trait Indexer { + /// The resultant "additions" when new transaction data is indexed. + type Additions; + + /// Scan and index the given `outpoint` and `txout`. + fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions; + + /// Scan and index the given transaction. + fn index_tx(&mut self, tx: &Transaction) -> Self::Additions; + + /// Apply additions to itself. + fn apply_additions(&mut self, additions: Self::Additions); + + /// Determines whether the transaction should be included in the index. + fn is_tx_relevant(&self, tx: &Transaction) -> bool; +} + +/// A trait that extends [`Indexer`] to also index "owned" script pubkeys. +pub trait OwnedIndexer: Indexer { + /// Determines whether a given script pubkey (`spk`) is owned. + fn is_spk_owned(&self, spk: &Script) -> bool; +} diff --git a/crates/chain/src/keychain.rs b/crates/chain/src/keychain.rs index 32176936..81503049 100644 --- a/crates/chain/src/keychain.rs +++ b/crates/chain/src/keychain.rs @@ -14,12 +14,13 @@ //! [`KeychainChangeSet`]s. //! //! [`SpkTxOutIndex`]: crate::SpkTxOutIndex + use crate::{ chain_graph::{self, ChainGraph}, collections::BTreeMap, sparse_chain::ChainPosition, tx_graph::TxGraph, - ForEachTxOut, + Append, ForEachTxOut, }; #[cfg(feature = "miniscript")] @@ -69,12 +70,12 @@ impl DerivationAdditions { } } -impl DerivationAdditions { +impl Append for DerivationAdditions { /// Append another [`DerivationAdditions`] into self. /// /// If the keychain already exists, increase the index when the other's index > self's index. /// If the keychain did not exist, append the new keychain. - pub fn append(&mut self, mut other: Self) { + 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); diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs index feb71edb..fbe67d1f 100644 --- a/crates/chain/src/keychain/txout_index.rs +++ b/crates/chain/src/keychain/txout_index.rs @@ -1,5 +1,6 @@ use crate::{ collections::*, + indexed_tx_graph::{Indexer, OwnedIndexer}, miniscript::{Descriptor, DescriptorPublicKey}, ForEachTxOut, SpkTxOutIndex, }; @@ -7,6 +8,8 @@ use alloc::{borrow::Cow, vec::Vec}; use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, TxOut}; use core::{fmt::Debug, ops::Deref}; +use crate::Append; + use super::DerivationAdditions; /// Maximum [BIP32](https://bips.xyz/32) derivation index. @@ -88,6 +91,32 @@ impl Deref for KeychainTxOutIndex { } } +impl Indexer for KeychainTxOutIndex { + type Additions = DerivationAdditions; + + fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions { + self.scan_txout(outpoint, txout) + } + + fn index_tx(&mut self, tx: &bitcoin::Transaction) -> Self::Additions { + self.scan(tx) + } + + fn apply_additions(&mut self, additions: Self::Additions) { + self.apply_additions(additions) + } + + fn is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool { + self.is_relevant(tx) + } +} + +impl OwnedIndexer for KeychainTxOutIndex { + fn is_spk_owned(&self, spk: &Script) -> bool { + self.inner().is_spk_owned(spk) + } +} + impl KeychainTxOutIndex { /// Scans an object for relevant outpoints, which are stored and indexed internally. /// diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 4e49e34e..26527623 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -24,11 +24,15 @@ mod spk_txout_index; pub use spk_txout_index::*; mod chain_data; pub use chain_data::*; +pub mod indexed_tx_graph; pub mod keychain; +pub mod local_chain; pub mod sparse_chain; mod tx_data_traits; pub mod tx_graph; pub use tx_data_traits::*; +mod chain_oracle; +pub use chain_oracle::*; #[doc(hidden)] pub mod example_utils; diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs new file mode 100644 index 00000000..30dfe80b --- /dev/null +++ b/crates/chain/src/local_chain.rs @@ -0,0 +1,203 @@ +use core::convert::Infallible; + +use alloc::collections::{BTreeMap, BTreeSet}; +use bitcoin::BlockHash; + +use crate::{BlockId, ChainOracle}; + +/// This is a local implementation of [`ChainOracle`]. +/// +/// TODO: We need a cache/snapshot thing for chain oracle. +/// * Minimize calls to remotes. +/// * Can we cache it forever? Should we drop stuff? +/// * Assume anything deeper than (i.e. 10) blocks won't be reorged. +/// * Is this a cache on txs or block? or both? +/// TODO: Parents of children are confirmed if children are confirmed. +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct LocalChain { + blocks: BTreeMap, +} + +impl ChainOracle for LocalChain { + type Error = Infallible; + + fn is_block_in_chain( + &self, + block: BlockId, + static_block: BlockId, + ) -> Result, Self::Error> { + if block.height > static_block.height { + return Ok(None); + } + Ok( + match ( + self.blocks.get(&block.height), + self.blocks.get(&static_block.height), + ) { + (Some(&hash), Some(&static_hash)) => { + Some(hash == block.hash && static_hash == static_block.hash) + } + _ => None, + }, + ) + } +} + +impl AsRef> for LocalChain { + fn as_ref(&self) -> &BTreeMap { + &self.blocks + } +} + +impl From for BTreeMap { + fn from(value: LocalChain) -> Self { + value.blocks + } +} + +impl From> for LocalChain { + fn from(value: BTreeMap) -> Self { + Self { blocks: value } + } +} + +impl LocalChain { + pub fn from_blocks(blocks: B) -> Self + where + B: IntoIterator, + { + Self { + blocks: blocks.into_iter().map(|b| (b.height, b.hash)).collect(), + } + } + + pub fn tip(&self) -> Option { + self.blocks + .iter() + .last() + .map(|(&height, &hash)| BlockId { height, hash }) + } + + /// Get a block at the given height. + pub fn get_block(&self, height: u32) -> Option { + self.blocks + .get(&height) + .map(|&hash| BlockId { height, hash }) + } + + /// This is like the sparsechain's logic, expect we must guarantee that all invalidated heights + /// are to be re-filled. + pub fn determine_changeset(&self, update: &Self) -> Result { + let update = update.as_ref(); + let update_tip = match update.keys().last().cloned() { + Some(tip) => tip, + None => return Ok(ChangeSet::default()), + }; + + // this is the latest height where both the update and local chain has the same block hash + let agreement_height = update + .iter() + .rev() + .find(|&(u_height, u_hash)| self.blocks.get(u_height) == Some(u_hash)) + .map(|(&height, _)| height); + + // the lower bound of the range to invalidate + let invalidate_lb = match agreement_height { + Some(height) if height == update_tip => u32::MAX, + Some(height) => height + 1, + None => 0, + }; + + // the first block's height to invalidate in the local chain + let invalidate_from_height = self.blocks.range(invalidate_lb..).next().map(|(&h, _)| h); + + // the first block of height to invalidate (if any) should be represented in the update + if let Some(first_invalid_height) = invalidate_from_height { + if !update.contains_key(&first_invalid_height) { + return Err(UpdateNotConnectedError(first_invalid_height)); + } + } + + let mut changeset: BTreeMap> = match invalidate_from_height { + Some(first_invalid_height) => { + // the first block of height to invalidate should be represented in the update + if !update.contains_key(&first_invalid_height) { + return Err(UpdateNotConnectedError(first_invalid_height)); + } + self.blocks + .range(first_invalid_height..) + .map(|(height, _)| (*height, None)) + .collect() + } + None => BTreeMap::new(), + }; + for (height, update_hash) in update { + let original_hash = self.blocks.get(height); + if Some(update_hash) != original_hash { + changeset.insert(*height, Some(*update_hash)); + } + } + + Ok(changeset) + } + + /// Applies the given `changeset`. + pub fn apply_changeset(&mut self, changeset: ChangeSet) { + for (height, blockhash) in changeset { + match blockhash { + Some(blockhash) => self.blocks.insert(height, blockhash), + None => self.blocks.remove(&height), + }; + } + } + + /// Updates [`LocalChain`] with an update [`LocalChain`]. + /// + /// This is equivalent 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 { + let changeset = self.determine_changeset(&update)?; + self.apply_changeset(changeset.clone()); + Ok(changeset) + } + + pub fn initial_changeset(&self) -> ChangeSet { + self.blocks + .iter() + .map(|(&height, &hash)| (height, Some(hash))) + .collect() + } + + pub fn heights(&self) -> BTreeSet { + self.blocks.keys().cloned().collect() + } +} + +/// This is the return value of [`determine_changeset`] and represents changes to [`LocalChain`]. +/// +/// [`determine_changeset`]: LocalChain::determine_changeset +pub type ChangeSet = BTreeMap>; + +/// Represents an update failure of [`LocalChain`] due to the update not connecting to the original +/// chain. +/// +/// 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. +#[derive(Clone, Debug, PartialEq)] +pub struct UpdateNotConnectedError(pub u32); + +impl core::fmt::Display for UpdateNotConnectedError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "the update cannot connect with the chain, try include block at height {}", + self.0 + ) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for UpdateNotConnectedError {} diff --git a/crates/chain/src/spk_txout_index.rs b/crates/chain/src/spk_txout_index.rs index 7f46604f..9fdf3bc0 100644 --- a/crates/chain/src/spk_txout_index.rs +++ b/crates/chain/src/spk_txout_index.rs @@ -2,6 +2,7 @@ use core::ops::RangeBounds; use crate::{ collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap}, + indexed_tx_graph::{Indexer, OwnedIndexer}, ForEachTxOut, }; use bitcoin::{self, OutPoint, Script, Transaction, TxOut, Txid}; @@ -52,6 +53,34 @@ impl Default for SpkTxOutIndex { } } +impl Indexer for SpkTxOutIndex { + type Additions = (); + + fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions { + self.scan_txout(outpoint, txout); + Default::default() + } + + fn index_tx(&mut self, tx: &Transaction) -> Self::Additions { + self.scan(tx); + Default::default() + } + + fn apply_additions(&mut self, _additions: Self::Additions) { + // This applies nothing. + } + + fn is_tx_relevant(&self, tx: &Transaction) -> bool { + self.is_relevant(tx) + } +} + +impl OwnedIndexer for SpkTxOutIndex { + fn is_spk_owned(&self, spk: &Script) -> bool { + self.spk_indices.get(spk).is_some() + } +} + /// 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 `ForEachTxOut` closure during scanning. diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 432592b8..8ec695ad 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -1,3 +1,6 @@ +use crate::collections::BTreeMap; +use crate::collections::BTreeSet; +use crate::BlockId; use bitcoin::{Block, OutPoint, Transaction, TxOut}; /// Trait to do something with every txout contained in a structure. @@ -31,3 +34,50 @@ impl ForEachTxOut for Transaction { } } } + +/// Trait that "anchors" blockchain data to a specific block of height and hash. +/// +/// I.e. If transaction A is anchored in block B, then if block B is in the best chain, we can +/// assume that transaction A is also confirmed in the best chain. This does not necessarily mean +/// that transaction A is confirmed in block B. It could also mean transaction A is confirmed in a +/// parent block of B. +pub trait Anchor: core::fmt::Debug + Clone + Eq + PartialOrd + Ord + core::hash::Hash { + /// Returns the [`BlockId`] that the associated blockchain data is "anchored" in. + fn anchor_block(&self) -> BlockId; + + /// Get the upper bound of the chain data's confirmation height. + /// + /// The default definition gives a pessimistic answer. This can be overridden by the `Anchor` + /// implementation for a more accurate value. + fn confirmation_height_upper_bound(&self) -> u32 { + self.anchor_block().height + } +} + +impl Anchor for &'static A { + fn anchor_block(&self) -> BlockId { + ::anchor_block(self) + } +} + +/// Trait that makes an object appendable. +pub trait Append { + /// Append another object of the same type onto `self`. + fn append(&mut self, other: Self); +} + +impl Append for () { + fn append(&mut self, _other: Self) {} +} + +impl Append for BTreeMap { + fn append(&mut self, mut other: Self) { + BTreeMap::append(self, &mut other) + } +} + +impl Append for BTreeSet { + fn append(&mut self, mut other: Self) { + BTreeSet::append(self, &mut other) + } +} diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 3326ac4a..cee688be 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -15,12 +15,13 @@ //! of the changes to [`TxGraph`]. //! //! ``` +//! # use bdk_chain::BlockId; //! # 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 mut graph: TxGraph = TxGraph::default(); //! //! // preview a transaction insertion (not actually inserted) //! let additions = graph.insert_tx_preview(tx_a); @@ -34,12 +35,13 @@ //! A [`TxGraph`] can also be updated with another [`TxGraph`]. //! //! ``` +//! # use bdk_chain::BlockId; //! # 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 mut graph: TxGraph = TxGraph::default(); //! let update = TxGraph::new(vec![tx_a, tx_b]); //! //! // preview additions as the result of the update @@ -52,63 +54,141 @@ //! let additions = graph.apply_update(update); //! assert!(additions.is_empty()); //! ``` -use crate::{collections::*, ForEachTxOut}; + +use crate::{ + collections::*, Anchor, Append, BlockId, ChainOracle, ForEachTxOut, FullTxOut, ObservedAs, +}; use alloc::vec::Vec; use bitcoin::{OutPoint, Transaction, TxOut, Txid}; -use core::ops::RangeInclusive; +use core::{ + convert::Infallible, + ops::{Deref, RangeInclusive}, +}; /// A graph of transactions and spends. /// /// See the [module-level documentation] for more. /// /// [module-level documentation]: crate::tx_graph -#[derive(Clone, Debug, PartialEq, Default)] -pub struct TxGraph { - txs: HashMap, +#[derive(Clone, Debug, PartialEq)] +pub struct TxGraph { + // all transactions that the graph is aware of in format: `(tx_node, tx_anchors, tx_last_seen)` + txs: HashMap, u64)>, spends: BTreeMap>, + anchors: BTreeSet<(A, Txid)>, // 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, } -/// Node of a [`TxGraph`]. This can either be a whole transaction, or a partial transaction (where -/// we only have select outputs). +impl Default for TxGraph { + fn default() -> Self { + Self { + txs: Default::default(), + spends: Default::default(), + anchors: Default::default(), + empty_outspends: Default::default(), + } + } +} + +/// An outward-facing view of a (transaction) node in the [`TxGraph`]. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct TxNode<'a, T, A> { + /// Txid of the transaction. + pub txid: Txid, + /// A partial or full representation of the transaction. + pub tx: &'a T, + /// The blocks that the transaction is "anchored" in. + pub anchors: &'a BTreeSet, + /// The last-seen unix timestamp of the transaction as unconfirmed. + pub last_seen_unconfirmed: u64, +} + +impl<'a, T, A> Deref for TxNode<'a, T, A> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.tx + } +} + +/// Internal representation of a transaction 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 { +enum TxNodeInternal { Whole(Transaction), Partial(BTreeMap), } -impl Default for TxNode { +impl Default for TxNodeInternal { fn default() -> Self { Self::Partial(BTreeMap::new()) } } -impl TxGraph { +/// An outwards-facing view of a transaction that is part of the *best chain*'s history. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct CanonicalTx<'a, T, A> { + /// How the transaction is observed as (confirmed or unconfirmed). + pub observed_as: ObservedAs<&'a A>, + /// The transaction node (as part of the graph). + pub node: TxNode<'a, T, A>, +} + +impl TxGraph { /// Iterate over all tx outputs known by [`TxGraph`]. + /// + /// This includes txouts of both full transactions as well as floating transactions. pub fn all_txouts(&self) -> impl Iterator { - self.txs.iter().flat_map(|(txid, tx)| match tx { - TxNode::Whole(tx) => tx + self.txs.iter().flat_map(|(txid, (tx, _, _))| match tx { + TxNodeInternal::Whole(tx) => tx .output .iter() .enumerate() .map(|(vout, txout)| (OutPoint::new(*txid, vout as _), txout)) .collect::>(), - TxNode::Partial(txouts) => txouts + TxNodeInternal::Partial(txouts) => txouts .iter() .map(|(vout, txout)| (OutPoint::new(*txid, *vout as _), txout)) .collect::>(), }) } + /// Iterate over floating txouts known by [`TxGraph`]. + /// + /// Floating txouts are txouts that do not have the residing full transaction contained in the + /// graph. + pub fn floating_txouts(&self) -> impl Iterator { + self.txs + .iter() + .filter_map(|(txid, (tx_node, _, _))| match tx_node { + TxNodeInternal::Whole(_) => None, + TxNodeInternal::Partial(txouts) => Some( + txouts + .iter() + .map(|(&vout, txout)| (OutPoint::new(*txid, vout), txout)), + ), + }) + .flatten() + } + /// 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, - }) + pub fn full_txs(&self) -> impl Iterator> { + self.txs + .iter() + .filter_map(|(&txid, (tx, anchors, last_seen))| match tx { + TxNodeInternal::Whole(tx) => Some(TxNode { + txid, + tx, + anchors, + last_seen_unconfirmed: *last_seen, + }), + TxNodeInternal::Partial(_) => None, + }) } /// Get a transaction by txid. This only returns `Some` for full transactions. @@ -117,30 +197,42 @@ impl TxGraph { /// /// [`get_txout`]: Self::get_txout pub fn get_tx(&self, txid: Txid) -> Option<&Transaction> { - match self.txs.get(&txid)? { - TxNode::Whole(tx) => Some(tx), - TxNode::Partial(_) => None, + self.get_tx_node(txid).map(|n| n.tx) + } + + /// Get a transaction node by txid. This only returns `Some` for full transactions. + pub fn get_tx_node(&self, txid: Txid) -> Option> { + match &self.txs.get(&txid)? { + (TxNodeInternal::Whole(tx), anchors, last_seen) => Some(TxNode { + txid, + tx, + anchors, + last_seen_unconfirmed: *last_seen, + }), + _ => None, } } /// Obtains a single tx output (if any) at the specified outpoint. pub fn get_txout(&self, outpoint: OutPoint) -> Option<&TxOut> { - match self.txs.get(&outpoint.txid)? { - TxNode::Whole(tx) => tx.output.get(outpoint.vout as usize), - TxNode::Partial(txouts) => txouts.get(&outpoint.vout), + match &self.txs.get(&outpoint.txid)?.0 { + TxNodeInternal::Whole(tx) => tx.output.get(outpoint.vout as usize), + TxNodeInternal::Partial(txouts) => txouts.get(&outpoint.vout), } } + /// Returns known outputs of a given `txid`. + /// /// 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 + pub fn tx_outputs(&self, txid: Txid) -> Option> { + Some(match &self.txs.get(&txid)?.0 { + TxNodeInternal::Whole(tx) => tx .output .iter() .enumerate() .map(|(vout, txout)| (vout as u32, txout)) .collect::>(), - TxNode::Partial(txouts) => txouts + TxNodeInternal::Partial(txouts) => txouts .iter() .map(|(vout, txout)| (*vout, txout)) .collect::>(), @@ -176,145 +268,7 @@ impl TxGraph { Some(inputs_sum - outputs_sum) } -} -impl TxGraph { - /// Construct a new [`TxGraph`] from a list of transactions. - 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 - /// alleged 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: Transaction) -> 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 between `update` and `self` (transactions that - /// exist in `update` but not in `self`). - pub fn apply_update(&mut self, update: TxGraph) -> Additions { - 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.txid(); - - 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.txid(), - txid, - "old tx of the 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 between `update` and `self` (transactions that - /// exist in `update` but not in `self`). - pub fn determine_additions(&self, update: &TxGraph) -> Additions { - 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(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 exists in `self`. - pub fn insert_tx_preview(&self, tx: Transaction) -> Additions { - let mut update = Self::default(); - update.txs.insert(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 @@ -329,7 +283,7 @@ impl TxGraph { /// /// - `vout` is the provided `txid`'s outpoint that is being spent /// - `txid-set` is the set of txids spending the `vout`. - pub fn tx_outspends( + pub fn tx_spends( &self, txid: Txid, ) -> impl DoubleEndedIterator)> + '_ { @@ -343,14 +297,6 @@ impl TxGraph { .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 filters and maps descendants from the starting `txid`. /// /// The supplied closure takes in two inputs `(depth, descendant_txid)`: @@ -361,7 +307,7 @@ impl TxGraph { /// /// 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 + pub fn walk_descendants<'g, F, O>(&'g self, txid: Txid, walk_map: F) -> TxDescendants where F: FnMut(usize, Txid) -> Option + 'g, { @@ -372,7 +318,11 @@ impl TxGraph { /// 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 + pub fn walk_conflicts<'g, F, O>( + &'g self, + tx: &'g Transaction, + walk_map: F, + ) -> TxDescendants where F: FnMut(usize, Txid) -> Option + 'g, { @@ -405,6 +355,529 @@ impl TxGraph { } } +impl TxGraph { + /// Construct a new [`TxGraph`] from a list of transactions. + pub fn new(txs: impl IntoIterator) -> Self { + let mut new = Self::default(); + for tx in txs.into_iter() { + let _ = new.insert_tx(tx); + } + new + } + + /// Returns the resultant [`Additions`] if the given `txout` is inserted at `outpoint`. Does not + /// mutate `self`. + /// + /// Inserting floating txouts are useful for determining fee/feerate of transactions we care + /// about. + /// + /// 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, + ( + TxNodeInternal::Partial([(outpoint.vout, txout)].into()), + BTreeSet::new(), + 0, + ), + ); + self.determine_additions(&update) + } + + /// Inserts the given [`TxOut`] at [`OutPoint`]. + /// + /// This is equivalent to calling [`insert_txout_preview`] and [`apply_additions`] in sequence. + /// + /// [`insert_txout_preview`]: Self::insert_txout_preview + /// [`apply_additions`]: Self::apply_additions + 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 + } + + /// Returns the resultant [`Additions`] if the given transaction is inserted. Does not actually + /// mutate [`Self`]. + /// + /// The [`Additions`] result will be empty if `tx` already exists in `self`. + pub fn insert_tx_preview(&self, tx: Transaction) -> Additions { + let mut update = Self::default(); + update + .txs + .insert(tx.txid(), (TxNodeInternal::Whole(tx), BTreeSet::new(), 0)); + self.determine_additions(&update) + } + + /// Inserts the given transaction into [`TxGraph`]. + /// + /// The [`Additions`] returned will be empty if `tx` already exists. + pub fn insert_tx(&mut self, tx: Transaction) -> Additions { + let additions = self.insert_tx_preview(tx); + self.apply_additions(additions.clone()); + additions + } + + /// Returns the resultant [`Additions`] if the `txid` is set in `anchor`. + pub fn insert_anchor_preview(&self, txid: Txid, anchor: A) -> Additions { + let mut update = Self::default(); + update.anchors.insert((anchor, txid)); + self.determine_additions(&update) + } + + /// Inserts the given `anchor` into [`TxGraph`]. + /// + /// This is equivalent to calling [`insert_anchor_preview`] and [`apply_additions`] in sequence. + /// The [`Additions`] returned will be empty if graph already knows that `txid` exists in + /// `anchor`. + /// + /// [`insert_anchor_preview`]: Self::insert_anchor_preview + /// [`apply_additions`]: Self::apply_additions + pub fn insert_anchor(&mut self, txid: Txid, anchor: A) -> Additions { + let additions = self.insert_anchor_preview(txid, anchor); + self.apply_additions(additions.clone()); + additions + } + + /// Returns the resultant [`Additions`] if the `txid` is set to `seen_at`. + /// + /// Note that [`TxGraph`] only keeps track of the lastest `seen_at`. + pub fn insert_seen_at_preview(&self, txid: Txid, seen_at: u64) -> Additions { + let mut update = Self::default(); + let (_, _, update_last_seen) = update.txs.entry(txid).or_default(); + *update_last_seen = seen_at; + self.determine_additions(&update) + } + + /// Inserts the given `seen_at` into [`TxGraph`]. + /// + /// This is equivalent to calling [`insert_seen_at_preview`] and [`apply_additions`] in + /// sequence. + /// + /// [`insert_seen_at_preview`]: Self::insert_seen_at_preview + /// [`apply_additions`]: Self::apply_additions + pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> Additions { + let additions = self.insert_seen_at_preview(txid, seen_at); + 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 between `update` and `self` (transactions that + /// exist in `update` but not in `self`). + pub fn apply_update(&mut self, update: TxGraph) -> Additions { + 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.txid(); + + 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); + }); + + match self.txs.get_mut(&txid) { + Some((tx_node @ TxNodeInternal::Partial(_), _, _)) => { + *tx_node = TxNodeInternal::Whole(tx); + } + Some((TxNodeInternal::Whole(tx), _, _)) => { + debug_assert_eq!( + tx.txid(), + txid, + "tx should produce txid that is same as key" + ); + } + None => { + self.txs + .insert(txid, (TxNodeInternal::Whole(tx), BTreeSet::new(), 0)); + } + } + } + + for (outpoint, txout) in additions.txout { + let tx_entry = self + .txs + .entry(outpoint.txid) + .or_insert_with(Default::default); + + match tx_entry { + (TxNodeInternal::Whole(_), _, _) => { /* do nothing since we already have full tx */ + } + (TxNodeInternal::Partial(txouts), _, _) => { + txouts.insert(outpoint.vout, txout); + } + } + } + + for (anchor, txid) in additions.anchors { + if self.anchors.insert((anchor.clone(), txid)) { + let (_, anchors, _) = self.txs.entry(txid).or_insert_with(Default::default); + anchors.insert(anchor); + } + } + + for (txid, new_last_seen) in additions.last_seen { + let (_, _, last_seen) = self.txs.entry(txid).or_insert_with(Default::default); + if new_last_seen > *last_seen { + *last_seen = new_last_seen; + } + } + } + + /// Previews the resultant [`Additions`] when [`Self`] is updated against the `update` graph. + /// + /// The [`Additions`] would be the set difference between `update` and `self` (transactions that + /// exist in `update` but not in `self`). + pub fn determine_additions(&self, update: &TxGraph) -> Additions { + let mut additions = Additions::default(); + + for (&txid, (update_tx_node, _, update_last_seen)) in &update.txs { + let prev_last_seen: u64 = match (self.txs.get(&txid), update_tx_node) { + (None, TxNodeInternal::Whole(update_tx)) => { + additions.tx.insert(update_tx.clone()); + 0 + } + (None, TxNodeInternal::Partial(update_txos)) => { + additions.txout.extend( + update_txos + .iter() + .map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())), + ); + 0 + } + (Some((TxNodeInternal::Whole(_), _, last_seen)), _) => *last_seen, + ( + Some((TxNodeInternal::Partial(_), _, last_seen)), + TxNodeInternal::Whole(update_tx), + ) => { + additions.tx.insert(update_tx.clone()); + *last_seen + } + ( + Some((TxNodeInternal::Partial(txos), _, last_seen)), + TxNodeInternal::Partial(update_txos), + ) => { + additions.txout.extend( + update_txos + .iter() + .filter(|(vout, _)| !txos.contains_key(*vout)) + .map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())), + ); + *last_seen + } + }; + + if *update_last_seen > prev_last_seen { + additions.last_seen.insert(txid, *update_last_seen); + } + } + + additions.anchors = update.anchors.difference(&self.anchors).cloned().collect(); + + additions + } +} + +impl TxGraph { + /// Get all heights that are relevant to the graph. + pub fn relevant_heights(&self) -> impl Iterator + '_ { + let mut last_height = Option::::None; + self.anchors + .iter() + .map(|(a, _)| a.anchor_block().height) + .filter(move |&height| { + let is_unique = Some(height) != last_height; + if is_unique { + last_height = Some(height); + } + is_unique + }) + } + + /// Get the position of the transaction in `chain` with tip `chain_tip`. + /// + /// If the given transaction of `txid` does not exist in the chain of `chain_tip`, `None` is + /// returned. + /// + /// # Error + /// + /// An error will occur if the [`ChainOracle`] implementation (`chain`) fails. If the + /// [`ChainOracle`] is infallible, [`get_chain_position`] can be used instead. + /// + /// [`get_chain_position`]: Self::get_chain_position + pub fn try_get_chain_position( + &self, + chain: &C, + chain_tip: BlockId, + txid: Txid, + ) -> Result>, C::Error> { + let (tx_node, anchors, &last_seen) = match self.txs.get(&txid) { + Some((tx, anchors, last_seen)) if !(anchors.is_empty() && *last_seen == 0) => { + (tx, anchors, last_seen) + } + _ => return Ok(None), + }; + + for anchor in anchors { + match chain.is_block_in_chain(anchor.anchor_block(), chain_tip)? { + Some(true) => return Ok(Some(ObservedAs::Confirmed(anchor))), + _ => continue, + } + } + + // The tx is not anchored to a block which is in the best chain, let's check whether we can + // ignore it by checking conflicts! + let tx = match tx_node { + TxNodeInternal::Whole(tx) => tx, + TxNodeInternal::Partial(_) => { + // Partial transactions (outputs only) cannot have conflicts. + return Ok(None); + } + }; + + // If a conflicting tx is in the best chain, or has `last_seen` higher than this tx, then + // this tx cannot exist in the best chain + for conflicting_tx in self.walk_conflicts(tx, |_, txid| self.get_tx_node(txid)) { + for block in conflicting_tx.anchors.iter().map(A::anchor_block) { + if chain.is_block_in_chain(block, chain_tip)? == Some(true) { + // conflicting tx is in best chain, so the current tx cannot be in best chain! + return Ok(None); + } + } + if conflicting_tx.last_seen_unconfirmed > last_seen { + return Ok(None); + } + } + + Ok(Some(ObservedAs::Unconfirmed(last_seen))) + } + + /// Get the position of the transaction in `chain` with tip `chain_tip`. + /// + /// This is the infallible version of [`try_get_chain_position`]. + /// + /// [`try_get_chain_position`]: Self::try_get_chain_position + pub fn get_chain_position>( + &self, + chain: &C, + chain_tip: BlockId, + txid: Txid, + ) -> Option> { + self.try_get_chain_position(chain, chain_tip, txid) + .expect("error is infallible") + } + + /// Get the txid of the spending transaction and where the spending transaction is observed in + /// the `chain` of `chain_tip`. + /// + /// If no in-chain transaction spends `outpoint`, `None` will be returned. + /// + /// # Error + /// + /// An error will occur only if the [`ChainOracle`] implementation (`chain`) fails. + /// + /// If the [`ChainOracle`] is infallible, [`get_chain_spend`] can be used instead. + /// + /// [`get_chain_spend`]: Self::get_chain_spend + pub fn try_get_chain_spend( + &self, + chain: &C, + chain_tip: BlockId, + outpoint: OutPoint, + ) -> Result, Txid)>, C::Error> { + if self + .try_get_chain_position(chain, chain_tip, outpoint.txid)? + .is_none() + { + return Ok(None); + } + if let Some(spends) = self.spends.get(&outpoint) { + for &txid in spends { + if let Some(observed_at) = self.try_get_chain_position(chain, chain_tip, txid)? { + return Ok(Some((observed_at, txid))); + } + } + } + Ok(None) + } + + /// Get the txid of the spending transaction and where the spending transaction is observed in + /// the `chain` of `chain_tip`. + /// + /// This is the infallible version of [`try_get_chain_spend`] + /// + /// [`try_get_chain_spend`]: Self::try_get_chain_spend + pub fn get_chain_spend>( + &self, + chain: &C, + static_block: BlockId, + outpoint: OutPoint, + ) -> Option<(ObservedAs<&A>, Txid)> { + self.try_get_chain_spend(chain, static_block, outpoint) + .expect("error is infallible") + } + + /// List graph transactions that are in `chain` with `chain_tip`. + /// + /// Each transaction is represented as a [`CanonicalTx`] that contains where the transaction is + /// observed in-chain, and the [`TxNode`]. + /// + /// # Error + /// + /// If the [`ChainOracle`] implementation (`chain`) fails, an error will be returned with the + /// returned item. + /// + /// If the [`ChainOracle`] is infallible, [`list_chain_txs`] can be used instead. + /// + /// [`list_chain_txs`]: Self::list_chain_txs + pub fn try_list_chain_txs<'a, C: ChainOracle + 'a>( + &'a self, + chain: &'a C, + chain_tip: BlockId, + ) -> impl Iterator, C::Error>> { + self.full_txs().filter_map(move |tx| { + self.try_get_chain_position(chain, chain_tip, tx.txid) + .map(|v| { + v.map(|observed_in| CanonicalTx { + observed_as: observed_in, + node: tx, + }) + }) + .transpose() + }) + } + + /// List graph transactions that are in `chain` with `chain_tip`. + /// + /// This is the infallible version of [`try_list_chain_txs`]. + /// + /// [`try_list_chain_txs`]: Self::try_list_chain_txs + pub fn list_chain_txs<'a, C: ChainOracle + 'a>( + &'a self, + chain: &'a C, + chain_tip: BlockId, + ) -> impl Iterator> { + self.try_list_chain_txs(chain, chain_tip) + .map(|r| r.expect("oracle is infallible")) + } + + /// List outputs that are in `chain` with `chain_tip`. + /// + /// Floating ouputs are not iterated over. + /// + /// The `filter_predicate` should return true for outputs that we wish to iterate over. + /// + /// # Error + /// + /// A returned item can error if the [`ChainOracle`] implementation (`chain`) fails. + /// + /// If the [`ChainOracle`] is infallible, [`list_chain_txouts`] can be used instead. + /// + /// [`list_chain_txouts`]: Self::list_chain_txouts + pub fn try_list_chain_txouts<'a, C: ChainOracle + 'a>( + &'a self, + chain: &'a C, + chain_tip: BlockId, + ) -> impl Iterator>, C::Error>> + 'a { + self.try_list_chain_txs(chain, chain_tip) + .flat_map(move |tx_res| match tx_res { + Ok(canonical_tx) => canonical_tx + .node + .output + .iter() + .enumerate() + .map(|(vout, txout)| { + let outpoint = OutPoint::new(canonical_tx.node.txid, vout as _); + Ok((outpoint, txout.clone(), canonical_tx.clone())) + }) + .collect::>(), + Err(err) => vec![Err(err)], + }) + .map(move |res| -> Result<_, C::Error> { + let ( + outpoint, + txout, + CanonicalTx { + observed_as, + node: tx_node, + }, + ) = res?; + let chain_position = observed_as.cloned(); + let spent_by = self + .try_get_chain_spend(chain, chain_tip, outpoint)? + .map(|(obs_as, txid)| (obs_as.cloned(), txid)); + let is_on_coinbase = tx_node.tx.is_coin_base(); + Ok(FullTxOut { + outpoint, + txout, + chain_position, + spent_by, + is_on_coinbase, + }) + }) + } + + /// List outputs that are in `chain` with `chain_tip`. + /// + /// This is the infallible version of [`try_list_chain_txouts`]. + /// + /// [`try_list_chain_txouts`]: Self::try_list_chain_txouts + pub fn list_chain_txouts<'a, C: ChainOracle + 'a>( + &'a self, + chain: &'a C, + chain_tip: BlockId, + ) -> impl Iterator>> + 'a { + self.try_list_chain_txouts(chain, chain_tip) + .map(|r| r.expect("error in infallible")) + } + + /// List unspent outputs (UTXOs) that are in `chain` with `chain_tip`. + /// + /// Floating outputs are not iterated over. + /// + /// # Error + /// + /// An item can be an error if the [`ChainOracle`] implementation fails. If the oracle is + /// infallible, [`list_chain_unspents`] can be used instead. + /// + /// [`list_chain_unspents`]: Self::list_chain_unspents + pub fn try_list_chain_unspents<'a, C: ChainOracle + 'a>( + &'a self, + chain: &'a C, + chain_tip: BlockId, + ) -> impl Iterator>, C::Error>> + 'a { + self.try_list_chain_txouts(chain, chain_tip) + .filter(|r| matches!(r, Ok(txo) if txo.spent_by.is_none())) + } + + /// List unspent outputs (UTXOs) that are in `chain` with `chain_tip`. + /// + /// This is the infallible version of [`try_list_chain_unspents`]. + /// + /// [`try_list_chain_unspents`]: Self::try_list_chain_unspents + pub fn list_chain_unspents<'a, C: ChainOracle + 'a>( + &'a self, + chain: &'a C, + static_block: BlockId, + ) -> impl Iterator>> + 'a { + self.try_list_chain_unspents(chain, static_block) + .map(|r| r.expect("error is infallible")) + } +} + /// A structure that represents changes to a [`TxGraph`]. /// /// It is named "additions" because [`TxGraph`] is monotone, so transactions can only be added and @@ -413,19 +886,38 @@ impl TxGraph { /// Refer to [module-level documentation] for more. /// /// [module-level documentation]: crate::tx_graph -#[derive(Debug, Clone, PartialEq, Default)] +#[derive(Debug, Clone, PartialEq)] #[cfg_attr( feature = "serde", derive(serde::Deserialize, serde::Serialize), - serde(crate = "serde_crate") + serde( + crate = "serde_crate", + bound( + deserialize = "A: Ord + serde::Deserialize<'de>", + serialize = "A: Ord + serde::Serialize", + ) + ) )] #[must_use] -pub struct Additions { +pub struct Additions { pub tx: BTreeSet, pub txout: BTreeMap, + pub anchors: BTreeSet<(A, Txid)>, + pub last_seen: BTreeMap, } -impl Additions { +impl Default for Additions { + fn default() -> Self { + Self { + tx: Default::default(), + txout: Default::default(), + anchors: Default::default(), + last_seen: Default::default(), + } + } +} + +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() @@ -443,28 +935,38 @@ impl Additions { }) .chain(self.txout.iter().map(|(op, txout)| (*op, txout))) } +} - /// Appends the changes in `other` into self such that applying `self` afterward has the same - /// effect as sequentially applying the original `self` and `other`. - pub fn append(&mut self, mut other: Additions) { +impl Append for Additions { + fn append(&mut self, mut other: Self) { self.tx.append(&mut other.tx); self.txout.append(&mut other.txout); + self.anchors.append(&mut other.anchors); + + // last_seen timestamps should only increase + self.last_seen.extend( + other + .last_seen + .into_iter() + .filter(|(txid, update_ls)| self.last_seen.get(txid) < Some(update_ls)) + .collect::>(), + ); } } -impl AsRef for TxGraph { - fn as_ref(&self) -> &TxGraph { +impl AsRef> for TxGraph { + fn as_ref(&self) -> &TxGraph { self } } -impl ForEachTxOut for Additions { +impl ForEachTxOut for Additions { fn for_each_txout(&self, f: impl FnMut((OutPoint, &TxOut))) { self.txouts().for_each(f) } } -impl ForEachTxOut for TxGraph { +impl ForEachTxOut for TxGraph { fn for_each_txout(&self, f: impl FnMut((OutPoint, &TxOut))) { self.all_txouts().for_each(f) } @@ -475,17 +977,17 @@ impl ForEachTxOut for TxGraph { /// This `struct` is created by the [`walk_descendants`] method of [`TxGraph`]. /// /// [`walk_descendants`]: TxGraph::walk_descendants -pub struct TxDescendants<'g, F> { - graph: &'g TxGraph, +pub struct TxDescendants<'g, A, F> { + graph: &'g TxGraph, visited: HashSet, stack: Vec<(usize, Txid)>, filter_map: F, } -impl<'g, F> TxDescendants<'g, F> { +impl<'g, A, F> TxDescendants<'g, A, F> { /// 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 { + pub(crate) fn new_include_root(graph: &'g TxGraph, txid: Txid, filter_map: F) -> Self { Self { graph, visited: Default::default(), @@ -495,7 +997,7 @@ impl<'g, F> TxDescendants<'g, F> { } /// 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 { + pub(crate) fn new_exclude_root(graph: &'g TxGraph, txid: Txid, filter_map: F) -> Self { let mut descendants = Self { graph, visited: Default::default(), @@ -508,7 +1010,11 @@ impl<'g, F> TxDescendants<'g, F> { /// Creates a `TxDescendants` from multiple starting transactions that include the starting /// `txid`s when iterating. - pub(crate) fn from_multiple_include_root(graph: &'g TxGraph, txids: I, filter_map: F) -> Self + pub(crate) fn from_multiple_include_root( + graph: &'g TxGraph, + txids: I, + filter_map: F, + ) -> Self where I: IntoIterator, { @@ -523,7 +1029,11 @@ impl<'g, F> TxDescendants<'g, F> { /// 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 + pub(crate) fn from_multiple_exclude_root( + graph: &'g TxGraph, + txids: I, + filter_map: F, + ) -> Self where I: IntoIterator, { @@ -540,7 +1050,7 @@ impl<'g, F> TxDescendants<'g, F> { } } -impl<'g, F> TxDescendants<'g, F> { +impl<'g, A, F> TxDescendants<'g, A, F> { fn populate_stack(&mut self, depth: usize, txid: Txid) { let spend_paths = self .graph @@ -552,7 +1062,7 @@ impl<'g, F> TxDescendants<'g, F> { } } -impl<'g, F, O> Iterator for TxDescendants<'g, F> +impl<'g, A, F, O> Iterator for TxDescendants<'g, A, F> where F: FnMut(usize, Txid) -> Option, { diff --git a/crates/chain/tests/common/mod.rs b/crates/chain/tests/common/mod.rs index e9b7a101..7d7288bd 100644 --- a/crates/chain/tests/common/mod.rs +++ b/crates/chain/tests/common/mod.rs @@ -5,6 +5,14 @@ macro_rules! h { }}; } +#[allow(unused_macros)] +macro_rules! local_chain { + [ $(($height:expr, $block_hash:expr)), * ] => {{ + #[allow(unused_mut)] + bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*]) + }}; +} + #[allow(unused_macros)] macro_rules! chain { ($([$($tt:tt)*]),*) => { chain!( checkpoints: [$([$($tt)*]),*] ) }; diff --git a/crates/chain/tests/test_chain_graph.rs b/crates/chain/tests/test_chain_graph.rs index 68f50b8f..b5cbf5b9 100644 --- a/crates/chain/tests/test_chain_graph.rs +++ b/crates/chain/tests/test_chain_graph.rs @@ -136,6 +136,7 @@ fn update_evicts_conflicting_tx() { graph: tx_graph::Additions { tx: [tx_b2.clone()].into(), txout: [].into(), + ..Default::default() }, }; assert_eq!( @@ -215,6 +216,7 @@ fn update_evicts_conflicting_tx() { graph: tx_graph::Additions { tx: [tx_b2].into(), txout: [].into(), + ..Default::default() }, }; assert_eq!( @@ -363,7 +365,7 @@ fn test_get_tx_in_chain() { let _ = cg.insert_tx(tx.clone(), TxHeight::Unconfirmed).unwrap(); assert_eq!( cg.get_tx_in_chain(tx.txid()), - Some((&TxHeight::Unconfirmed, &tx)) + Some((&TxHeight::Unconfirmed, &tx,)) ); } @@ -395,9 +397,9 @@ fn test_iterate_transactions() { assert_eq!( cg.transactions_in_chain().collect::>(), vec![ - (&TxHeight::Confirmed(0), &txs[2]), - (&TxHeight::Confirmed(1), &txs[0]), - (&TxHeight::Unconfirmed, &txs[1]), + (&TxHeight::Confirmed(0), &txs[2],), + (&TxHeight::Confirmed(1), &txs[0],), + (&TxHeight::Unconfirmed, &txs[1],), ] ); } diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs new file mode 100644 index 00000000..4e847aec --- /dev/null +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -0,0 +1,540 @@ +#[macro_use] +mod common; + +use std::collections::{BTreeMap, BTreeSet}; + +use bdk_chain::{ + indexed_tx_graph::{IndexedAdditions, IndexedTxGraph}, + keychain::{Balance, DerivationAdditions, KeychainTxOutIndex}, + tx_graph::Additions, + BlockId, ObservedAs, +}; +use bitcoin::{secp256k1::Secp256k1, BlockHash, OutPoint, Script, Transaction, TxIn, TxOut}; +use miniscript::Descriptor; + +/// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented +/// in topological order. +/// +/// Given 3 transactions (A, B, C), where A has 2 owned outputs. B and C spends an output each of A. +/// Typically, we would only know whether B and C are relevant if we have indexed A (A's outpoints +/// are associated with owned spks in the index). Ensure insertion and indexing is topological- +/// agnostic. +#[test] +fn insert_relevant_txs() { + const DESCRIPTOR: &str = "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)"; + let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTOR) + .expect("must be valid"); + let spk_0 = descriptor.at_derivation_index(0).script_pubkey(); + let spk_1 = descriptor.at_derivation_index(9).script_pubkey(); + + let mut graph = IndexedTxGraph::>::default(); + graph.index.add_keychain((), descriptor); + graph.index.set_lookahead(&(), 10); + + let tx_a = Transaction { + output: vec![ + TxOut { + value: 10_000, + script_pubkey: spk_0, + }, + TxOut { + value: 20_000, + script_pubkey: spk_1, + }, + ], + ..common::new_tx(0) + }; + + let tx_b = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_a.txid(), 0), + ..Default::default() + }], + ..common::new_tx(1) + }; + + let tx_c = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_a.txid(), 1), + ..Default::default() + }], + ..common::new_tx(2) + }; + + let txs = [tx_c, tx_b, tx_a]; + + assert_eq!( + graph.insert_relevant_txs(txs.iter().map(|tx| (tx, None)), None), + IndexedAdditions { + graph_additions: Additions { + tx: txs.into(), + ..Default::default() + }, + index_additions: DerivationAdditions([((), 9_u32)].into()), + } + ) +} + +#[test] +fn test_list_owned_txouts() { + let mut local_chain = local_chain![ + (0, h!("Block 0")), + (1, h!("Block 1")), + (2, h!("Block 2")), + (3, h!("Block 3")) + ]; + + let desc_1 : &str = "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)"; + let (desc_1, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), desc_1).unwrap(); + let desc_2 : &str = "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/1/*)"; + let (desc_2, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), desc_2).unwrap(); + + let mut graph = IndexedTxGraph::>::default(); + + graph.index.add_keychain("keychain_1".into(), desc_1); + graph.index.add_keychain("keychain_2".into(), desc_2); + + graph.index.set_lookahead_for_all(10); + + let mut trusted_spks = Vec::new(); + let mut untrusted_spks = Vec::new(); + + { + for _ in 0..10 { + let ((_, script), _) = graph.index.reveal_next_spk(&"keychain_1".to_string()); + trusted_spks.push(script.clone()); + } + } + + { + for _ in 0..10 { + let ((_, script), _) = graph.index.reveal_next_spk(&"keychain_2".to_string()); + untrusted_spks.push(script.clone()); + } + } + + let trust_predicate = |spk: &Script| trusted_spks.contains(spk); + + // tx1 is coinbase transaction received at trusted keychain at block 0. + let tx1 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::null(), + ..Default::default() + }], + output: vec![TxOut { + value: 70000, + script_pubkey: trusted_spks[0].clone(), + }], + ..common::new_tx(0) + }; + + // tx2 is an incoming transaction received at untrusted keychain at block 1. + let tx2 = Transaction { + output: vec![TxOut { + value: 30000, + script_pubkey: untrusted_spks[0].clone(), + }], + ..common::new_tx(0) + }; + + // tx3 spends tx2 and gives a change back in trusted keychain. Confirmed at Block 2. + let tx3 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx2.txid(), 0), + ..Default::default() + }], + output: vec![TxOut { + value: 10000, + script_pubkey: trusted_spks[1].clone(), + }], + ..common::new_tx(0) + }; + + // tx4 is an external transaction receiving at untrusted keychain, unconfirmed. + let tx4 = Transaction { + output: vec![TxOut { + value: 20000, + script_pubkey: untrusted_spks[1].clone(), + }], + ..common::new_tx(0) + }; + + // tx5 is spending tx3 and receiving change at trusted keychain, unconfirmed. + let tx5 = Transaction { + output: vec![TxOut { + value: 15000, + script_pubkey: trusted_spks[2].clone(), + }], + ..common::new_tx(0) + }; + + // tx6 is an unrelated transaction confirmed at 3. + let tx6 = common::new_tx(0); + + let _ = graph.insert_relevant_txs( + [&tx1, &tx2, &tx3, &tx6] + .iter() + .enumerate() + .map(|(i, tx)| (*tx, [local_chain.get_block(i as u32).unwrap()])), + None, + ); + + let _ = graph.insert_relevant_txs([&tx4, &tx5].iter().map(|tx| (*tx, None)), Some(100)); + + // AT Block 0 + { + let txouts = graph + .list_owned_txouts(&local_chain, local_chain.get_block(0).unwrap()) + .collect::>(); + + let utxos = graph + .list_owned_unspents(&local_chain, local_chain.get_block(0).unwrap()) + .collect::>(); + + let balance = graph.balance( + &local_chain, + local_chain.get_block(0).unwrap(), + trust_predicate, + ); + + let confirmed_txouts_txid = txouts + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let unconfirmed_txout_txid = txouts + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let confirmed_utxos_txid = utxos + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let unconfirmed_utxos_txid = utxos + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + assert_eq!(txouts.len(), 5); + assert_eq!(utxos.len(), 4); + + assert_eq!(confirmed_txouts_txid, [tx1.txid()].into()); + assert_eq!( + unconfirmed_txout_txid, + [tx2.txid(), tx3.txid(), tx4.txid(), tx5.txid()].into() + ); + + assert_eq!(confirmed_utxos_txid, [tx1.txid()].into()); + assert_eq!( + unconfirmed_utxos_txid, + [tx3.txid(), tx4.txid(), tx5.txid()].into() + ); + + assert_eq!( + balance, + Balance { + immature: 70000, // immature coinbase + trusted_pending: 25000, // tx3 + tx5 + untrusted_pending: 20000, // tx4 + confirmed: 0 // Nothing is confirmed yet + } + ); + } + + // AT Block 1 + { + let txouts = graph + .list_owned_txouts(&local_chain, local_chain.get_block(1).unwrap()) + .collect::>(); + + let utxos = graph + .list_owned_unspents(&local_chain, local_chain.get_block(1).unwrap()) + .collect::>(); + + let balance = graph.balance( + &local_chain, + local_chain.get_block(1).unwrap(), + trust_predicate, + ); + + let confirmed_txouts_txid = txouts + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let unconfirmed_txout_txid = txouts + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let confirmed_utxos_txid = utxos + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let unconfirmed_utxos_txid = utxos + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + assert_eq!(txouts.len(), 5); + assert_eq!(utxos.len(), 4); + + // tx2 gets into confirmed txout set + assert_eq!(confirmed_txouts_txid, [tx1.txid(), tx2.txid()].into()); + assert_eq!( + unconfirmed_txout_txid, + [tx3.txid(), tx4.txid(), tx5.txid()].into() + ); + + // tx2 doesn't get into confirmed utxos set + assert_eq!(confirmed_utxos_txid, [tx1.txid()].into()); + assert_eq!( + unconfirmed_utxos_txid, + [tx3.txid(), tx4.txid(), tx5.txid()].into() + ); + + // Balance breakup remains same + assert_eq!( + balance, + Balance { + immature: 70000, // immature coinbase + trusted_pending: 25000, // tx3 + tx5 + untrusted_pending: 20000, // tx4 + confirmed: 0 // Nothing is confirmed yet + } + ); + } + + // AT Block 2 + { + let txouts = graph + .list_owned_txouts(&local_chain, local_chain.get_block(2).unwrap()) + .collect::>(); + + let utxos = graph + .list_owned_unspents(&local_chain, local_chain.get_block(2).unwrap()) + .collect::>(); + + let balance = graph.balance( + &local_chain, + local_chain.get_block(2).unwrap(), + trust_predicate, + ); + + let confirmed_txouts_txid = txouts + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let unconfirmed_txout_txid = txouts + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let confirmed_utxos_txid = utxos + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let unconfirmed_utxos_txid = utxos + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + assert_eq!(txouts.len(), 5); + assert_eq!(utxos.len(), 4); + + // tx3 now gets into the confirmed txout set + assert_eq!( + confirmed_txouts_txid, + [tx1.txid(), tx2.txid(), tx3.txid()].into() + ); + assert_eq!(unconfirmed_txout_txid, [tx4.txid(), tx5.txid()].into()); + + // tx3 also gets into confirmed utxo set + assert_eq!(confirmed_utxos_txid, [tx1.txid(), tx3.txid()].into()); + assert_eq!(unconfirmed_utxos_txid, [tx4.txid(), tx5.txid()].into()); + + assert_eq!( + balance, + Balance { + immature: 70000, // immature coinbase + trusted_pending: 15000, // tx5 + untrusted_pending: 20000, // tx4 + confirmed: 10000 // tx3 got confirmed + } + ); + } + + // AT Block 110 + { + let mut local_chain_extension = (4..150) + .map(|i| (i as u32, h!("random"))) + .collect::>(); + + local_chain_extension.insert(3, h!("Block 3")); + + local_chain + .apply_update(local_chain_extension.into()) + .unwrap(); + + let txouts = graph + .list_owned_txouts(&local_chain, local_chain.get_block(110).unwrap()) + .collect::>(); + + let utxos = graph + .list_owned_unspents(&local_chain, local_chain.get_block(110).unwrap()) + .collect::>(); + + let balance = graph.balance( + &local_chain, + local_chain.get_block(110).unwrap(), + trust_predicate, + ); + + let confirmed_txouts_txid = txouts + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let unconfirmed_txout_txid = txouts + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let confirmed_utxos_txid = utxos + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let unconfirmed_utxos_txid = utxos + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + println!("TxOuts : {:#?}", txouts); + println!("UTXOS {:#?}", utxos); + println!("{:#?}", balance); + + assert_eq!(txouts.len(), 5); + assert_eq!(utxos.len(), 4); + + assert_eq!( + confirmed_txouts_txid, + [tx1.txid(), tx2.txid(), tx3.txid()].into() + ); + assert_eq!(unconfirmed_txout_txid, [tx4.txid(), tx5.txid()].into()); + + assert_eq!(confirmed_utxos_txid, [tx1.txid(), tx3.txid()].into()); + assert_eq!(unconfirmed_utxos_txid, [tx4.txid(), tx5.txid()].into()); + + assert_eq!( + balance, + Balance { + immature: 0, // immature coinbase + trusted_pending: 15000, // tx5 + untrusted_pending: 20000, // tx4 + confirmed: 80000 // tx1 got matured + } + ); + } +} diff --git a/crates/chain/tests/test_keychain_tracker.rs b/crates/chain/tests/test_keychain_tracker.rs index 3bf0a1d5..bd8c6e03 100644 --- a/crates/chain/tests/test_keychain_tracker.rs +++ b/crates/chain/tests/test_keychain_tracker.rs @@ -1,6 +1,7 @@ #![cfg(feature = "miniscript")] #[macro_use] mod common; + use bdk_chain::{ keychain::{Balance, KeychainTracker}, miniscript::{ @@ -40,7 +41,7 @@ fn test_insert_tx() { .chain_graph() .transactions_in_chain() .collect::>(), - vec![(&ConfirmationTime::Unconfirmed, &tx)] + vec![(&ConfirmationTime::Unconfirmed, &tx,)] ); assert_eq!( @@ -66,7 +67,7 @@ fn test_balance() { One, Two, } - let mut tracker = KeychainTracker::::default(); + 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); diff --git a/crates/chain/tests/test_keychain_txout_index.rs b/crates/chain/tests/test_keychain_txout_index.rs index 07c7f48d..5f586584 100644 --- a/crates/chain/tests/test_keychain_txout_index.rs +++ b/crates/chain/tests/test_keychain_txout_index.rs @@ -293,7 +293,6 @@ fn test_wildcard_derivations() { let _ = txout_index.reveal_to_target(&TestKeychain::External, 25); (0..=15) - .into_iter() .chain(vec![17, 20, 23].into_iter()) .for_each(|index| assert!(txout_index.mark_used(&TestKeychain::External, index))); @@ -310,7 +309,7 @@ fn test_wildcard_derivations() { // - Use all the derived till 26. // - next_unused() = ((27, ), DerivationAdditions) - (0..=26).into_iter().for_each(|index| { + (0..=26).for_each(|index| { txout_index.mark_used(&TestKeychain::External, index); }); diff --git a/crates/chain/tests/test_local_chain.rs b/crates/chain/tests/test_local_chain.rs new file mode 100644 index 00000000..1aea9850 --- /dev/null +++ b/crates/chain/tests/test_local_chain.rs @@ -0,0 +1,167 @@ +use bdk_chain::local_chain::{LocalChain, UpdateNotConnectedError}; + +#[macro_use] +mod common; + +#[test] +fn add_first_tip() { + let chain = LocalChain::default(); + assert_eq!( + chain.determine_changeset(&local_chain![(0, h!("A"))]), + Ok([(0, Some(h!("A")))].into()), + "add first tip" + ); +} + +#[test] +fn add_second_tip() { + let chain = local_chain![(0, h!("A"))]; + assert_eq!( + chain.determine_changeset(&local_chain![(0, h!("A")), (1, h!("B"))]), + Ok([(1, Some(h!("B")))].into()) + ); +} + +#[test] +fn two_disjoint_chains_cannot_merge() { + let chain1 = local_chain![(0, h!("A"))]; + let chain2 = local_chain![(1, h!("B"))]; + assert_eq!( + chain1.determine_changeset(&chain2), + Err(UpdateNotConnectedError(0)) + ); +} + +#[test] +fn duplicate_chains_should_merge() { + let chain1 = local_chain![(0, h!("A"))]; + let chain2 = local_chain![(0, h!("A"))]; + assert_eq!(chain1.determine_changeset(&chain2), Ok(Default::default())); +} + +#[test] +fn can_introduce_older_checkpoints() { + let chain1 = local_chain![(2, h!("C")), (3, h!("D"))]; + let chain2 = local_chain![(1, h!("B")), (2, h!("C"))]; + + assert_eq!( + chain1.determine_changeset(&chain2), + Ok([(1, Some(h!("B")))].into()) + ); +} + +#[test] +fn fix_blockhash_before_agreement_point() { + let chain1 = local_chain![(0, h!("im-wrong")), (1, h!("we-agree"))]; + let chain2 = local_chain![(0, h!("fix")), (1, h!("we-agree"))]; + + assert_eq!( + chain1.determine_changeset(&chain2), + Ok([(0, Some(h!("fix")))].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 = local_chain![(1, h!("B")), (2, h!("C"))]; + let chain2 = local_chain![(0, h!("A")), (1, h!("B")), (2, h!("C")), (3, h!("D"))]; + + assert_eq!( + chain1.determine_changeset(&chain2), + Ok([(0, Some(h!("A"))), (3, Some(h!("D")))].into()), + ); +} + +/// 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 = local_chain![(1, h!("B")), (2, h!("C"))]; + let chain2 = local_chain![(0, h!("A")), (1, h!("B")), (3, h!("D"))]; + + assert_eq!( + chain1.determine_changeset(&chain2), + Err(UpdateNotConnectedError(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. +#[test] +fn transitive_invalidation_applies_to_checkpoints_higher_than_invalidation() { + let chain1 = local_chain![(0, h!("A")), (2, h!("B")), (3, h!("C")), (5, h!("E"))]; + let chain2 = local_chain![(0, h!("A")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))]; + + assert_eq!( + chain1.determine_changeset(&chain2), + Ok([ + (2, Some(h!("B'"))), + (3, Some(h!("C'"))), + (4, Some(h!("D"))), + (5, None), + ] + .into()) + ); +} + +/// 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 = local_chain![(1, h!("B")), (2, h!("C")), (4, h!("E"))]; + let chain2 = local_chain![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))]; + + assert_eq!( + chain1.determine_changeset(&chain2), + Ok([ + (1, Some(h!("B'"))), + (2, Some(h!("C'"))), + (3, Some(h!("D"))), + (4, None) + ] + .into()) + ) +} + +/// 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 = local_chain![(0, h!("A")), (1, h!("B")), (2, h!("C")), (4, h!("E"))]; + let chain2 = local_chain![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))]; + + assert_eq!( + chain1.determine_changeset(&chain2), + Err(UpdateNotConnectedError(0)) + ) +} diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 04974bf3..41b2ae02 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -2,13 +2,19 @@ mod common; use bdk_chain::{ collections::*, + local_chain::LocalChain, tx_graph::{Additions, TxGraph}, + Append, BlockId, ObservedAs, +}; +use bitcoin::{ + hashes::Hash, BlockHash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid, }; -use bitcoin::{hashes::Hash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid}; use core::iter; +use std::vec; #[test] fn insert_txouts() { + // 2 (Outpoint, TxOut) tupples that denotes original data in the graph, as partial transactions. let original_ops = [ ( OutPoint::new(h!("tx1"), 1), @@ -26,6 +32,7 @@ fn insert_txouts() { ), ]; + // Another (OutPoint, TxOut) tupple to be used as update as partial transaction. let update_ops = [( OutPoint::new(h!("tx2"), 0), TxOut { @@ -34,8 +41,32 @@ fn insert_txouts() { }, )]; + // One full transaction to be included in the update + let update_txs = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![TxIn { + previous_output: OutPoint::null(), + ..Default::default() + }], + output: vec![TxOut { + value: 30_000, + script_pubkey: Script::new(), + }], + }; + + // Conf anchor used to mark the full transaction as confirmed. + let conf_anchor = ObservedAs::Confirmed(BlockId { + height: 100, + hash: h!("random blockhash"), + }); + + // Unconfirmed anchor to mark the partial transactions as unconfirmed + let unconf_anchor = ObservedAs::::Unconfirmed(1000000); + + // Make the original graph let mut graph = { - let mut graph = TxGraph::default(); + let mut graph = TxGraph::>::default(); for (outpoint, txout) in &original_ops { assert_eq!( graph.insert_txout(*outpoint, txout.clone()), @@ -48,9 +79,11 @@ fn insert_txouts() { graph }; + // Make the update graph let update = { let mut graph = TxGraph::default(); for (outpoint, txout) in &update_ops { + // Insert partials transactions assert_eq!( graph.insert_txout(*outpoint, txout.clone()), Additions { @@ -58,24 +91,101 @@ fn insert_txouts() { ..Default::default() } ); + // Mark them unconfirmed. + assert_eq!( + graph.insert_anchor(outpoint.txid, unconf_anchor), + Additions { + tx: [].into(), + txout: [].into(), + anchors: [(unconf_anchor, outpoint.txid)].into(), + last_seen: [].into() + } + ); + // Mark them last seen at. + assert_eq!( + graph.insert_seen_at(outpoint.txid, 1000000), + Additions { + tx: [].into(), + txout: [].into(), + anchors: [].into(), + last_seen: [(outpoint.txid, 1000000)].into() + } + ); } + // Insert the full transaction + assert_eq!( + graph.insert_tx(update_txs.clone()), + Additions { + tx: [update_txs.clone()].into(), + ..Default::default() + } + ); + + // Mark it as confirmed. + assert_eq!( + graph.insert_anchor(update_txs.txid(), conf_anchor), + Additions { + tx: [].into(), + txout: [].into(), + anchors: [(conf_anchor, update_txs.txid())].into(), + last_seen: [].into() + } + ); graph }; + // Check the resulting addition. let additions = graph.determine_additions(&update); assert_eq!( additions, Additions { - tx: [].into(), + tx: [update_txs.clone()].into(), txout: update_ops.into(), + anchors: [(conf_anchor, update_txs.txid()), (unconf_anchor, h!("tx2"))].into(), + last_seen: [(h!("tx2"), 1000000)].into() } ); + // Apply addition and check the new graph counts. 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); + assert_eq!(graph.all_txouts().count(), 4); + assert_eq!(graph.full_txs().count(), 1); + assert_eq!(graph.floating_txouts().count(), 3); + + // Check TxOuts are fetched correctly from the graph. + assert_eq!( + graph.tx_outputs(h!("tx1")).expect("should exists"), + [ + ( + 1u32, + &TxOut { + value: 10_000, + script_pubkey: Script::new(), + } + ), + ( + 2u32, + &TxOut { + value: 20_000, + script_pubkey: Script::new(), + } + ) + ] + .into() + ); + + assert_eq!( + graph.tx_outputs(update_txs.txid()).expect("should exists"), + [( + 0u32, + &TxOut { + value: 30_000, + script_pubkey: Script::new() + } + )] + .into() + ); } #[test] @@ -90,10 +200,10 @@ fn insert_tx_graph_doesnt_count_coinbase_as_spent() { output: vec![], }; - let mut graph = TxGraph::default(); + 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()); + assert!(graph.tx_spends(Txid::all_zeros()).next().is_none()); } #[test] @@ -120,8 +230,8 @@ fn insert_tx_graph_keeps_track_of_spend() { output: vec![], }; - let mut graph1 = TxGraph::default(); - let mut graph2 = TxGraph::default(); + let mut graph1 = TxGraph::<()>::default(); + let mut graph2 = TxGraph::<()>::default(); // insert in different order let _ = graph1.insert_tx(tx1.clone()); @@ -149,14 +259,14 @@ fn insert_tx_can_retrieve_full_tx_from_graph() { output: vec![TxOut::default()], }; - let mut graph = TxGraph::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 mut tx_graph = TxGraph::<()>::default(); let tx = Transaction { version: 0x01, lock_time: PackedLockTime(0), @@ -212,7 +322,7 @@ fn insert_tx_displaces_txouts() { #[test] fn insert_txout_does_not_displace_tx() { - let mut tx_graph = TxGraph::default(); + let mut tx_graph = TxGraph::<()>::default(); let tx = Transaction { version: 0x01, lock_time: PackedLockTime(0), @@ -268,7 +378,7 @@ fn insert_txout_does_not_displace_tx() { #[test] fn test_calculate_fee() { - let mut graph = TxGraph::default(); + let mut graph = TxGraph::<()>::default(); let intx1 = Transaction { version: 0x01, lock_time: PackedLockTime(0), @@ -362,7 +472,7 @@ fn test_calculate_fee_on_coinbase() { output: vec![TxOut::default()], }; - let graph = TxGraph::default(); + let graph = TxGraph::<()>::default(); assert_eq!(graph.calculate_fee(&tx), Some(0)); } @@ -404,7 +514,7 @@ fn test_conflicting_descendants() { let txid_a = tx_a.txid(); let txid_b = tx_b.txid(); - let mut graph = TxGraph::default(); + let mut graph = TxGraph::<()>::default(); let _ = graph.insert_tx(tx_a); let _ = graph.insert_tx(tx_b); @@ -480,7 +590,7 @@ fn test_descendants_no_repeat() { }) .collect::>(); - let mut graph = TxGraph::default(); + let mut graph = TxGraph::<()>::default(); let mut expected_txids = BTreeSet::new(); // these are NOT descendants of `tx_a` @@ -510,3 +620,263 @@ fn test_descendants_no_repeat() { } assert!(expected_txids.is_empty()); } + +#[test] +fn test_chain_spends() { + let local_chain: LocalChain = (0..=100) + .map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes()))) + .collect::>() + .into(); + let tip = local_chain.tip().expect("must have tip"); + + // The parent tx contains 2 outputs. Which are spent by one confirmed and one unconfirmed tx. + // The parent tx is confirmed at block 95. + let tx_0 = Transaction { + input: vec![], + output: vec![ + TxOut { + value: 10_000, + script_pubkey: Script::new(), + }, + TxOut { + value: 20_000, + script_pubkey: Script::new(), + }, + ], + ..common::new_tx(0) + }; + + // The first confirmed transaction spends vout: 0. And is confirmed at block 98. + let tx_1 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_0.txid(), 0), + ..TxIn::default() + }], + output: vec![ + TxOut { + value: 5_000, + script_pubkey: Script::new(), + }, + TxOut { + value: 5_000, + script_pubkey: Script::new(), + }, + ], + ..common::new_tx(0) + }; + + // The second transactions spends vout:1, and is unconfirmed. + let tx_2 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_0.txid(), 1), + ..TxIn::default() + }], + output: vec![ + TxOut { + value: 10_000, + script_pubkey: Script::new(), + }, + TxOut { + value: 10_000, + script_pubkey: Script::new(), + }, + ], + ..common::new_tx(0) + }; + + let mut graph = TxGraph::::default(); + + let _ = graph.insert_tx(tx_0.clone()); + let _ = graph.insert_tx(tx_1.clone()); + let _ = graph.insert_tx(tx_2.clone()); + + [95, 98] + .iter() + .zip([&tx_0, &tx_1].into_iter()) + .for_each(|(ht, tx)| { + let block_id = local_chain.get_block(*ht).expect("block expected"); + let _ = graph.insert_anchor(tx.txid(), block_id); + }); + + // Assert that confirmed spends are returned correctly. + assert_eq!( + graph + .get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 0)) + .unwrap(), + ( + ObservedAs::Confirmed(&local_chain.get_block(98).expect("block expected")), + tx_1.txid() + ) + ); + + // Check if chain position is returned correctly. + assert_eq!( + graph + .get_chain_position(&local_chain, tip, tx_0.txid()) + .expect("position expected"), + ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected")) + ); + + // As long the unconfirmed tx isn't marked as seen, chain_spend will return None. + assert!(graph + .get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1)) + .is_none()); + + // Mark the unconfirmed as seen and check correct ObservedAs status is returned. + let _ = graph.insert_seen_at(tx_2.txid(), 1234567); + + // Check chain spend returned correctly. + assert_eq!( + graph + .get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1)) + .unwrap(), + (ObservedAs::Unconfirmed(1234567), tx_2.txid()) + ); + + // A conflicting transaction that conflicts with tx_1. + let tx_1_conflict = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_0.txid(), 0), + ..Default::default() + }], + ..common::new_tx(0) + }; + let _ = graph.insert_tx(tx_1_conflict.clone()); + + // Because this tx conflicts with an already confirmed transaction, chain position should return none. + assert!(graph + .get_chain_position(&local_chain, tip, tx_1_conflict.txid()) + .is_none()); + + // Another conflicting tx that conflicts with tx_2. + let tx_2_conflict = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_0.txid(), 1), + ..Default::default() + }], + ..common::new_tx(0) + }; + + // Insert in graph and mark it as seen. + let _ = graph.insert_tx(tx_2_conflict.clone()); + let _ = graph.insert_seen_at(tx_2_conflict.txid(), 1234568); + + // This should return a valid observation with correct last seen. + assert_eq!( + graph + .get_chain_position(&local_chain, tip, tx_2_conflict.txid()) + .expect("position expected"), + ObservedAs::Unconfirmed(1234568) + ); + + // Chain_spend now catches the new transaction as the spend. + assert_eq!( + graph + .get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1)) + .expect("expect observation"), + (ObservedAs::Unconfirmed(1234568), tx_2_conflict.txid()) + ); + + // Chain position of the `tx_2` is now none, as it is older than `tx_2_conflict` + assert!(graph + .get_chain_position(&local_chain, tip, tx_2.txid()) + .is_none()); +} + +#[test] +fn test_relevant_heights() { + let mut graph = TxGraph::::default(); + + let tx1 = common::new_tx(1); + let tx2 = common::new_tx(2); + + let _ = graph.insert_tx(tx1.clone()); + assert_eq!( + graph.relevant_heights().collect::>(), + vec![], + "no anchors in graph" + ); + + let _ = graph.insert_anchor( + tx1.txid(), + BlockId { + height: 3, + hash: h!("3a"), + }, + ); + assert_eq!( + graph.relevant_heights().collect::>(), + vec![3], + "one anchor at height 3" + ); + + let _ = graph.insert_anchor( + tx1.txid(), + BlockId { + height: 3, + hash: h!("3b"), + }, + ); + assert_eq!( + graph.relevant_heights().collect::>(), + vec![3], + "introducing duplicate anchor at height 3, must not iterate over duplicate heights" + ); + + let _ = graph.insert_anchor( + tx1.txid(), + BlockId { + height: 4, + hash: h!("4a"), + }, + ); + assert_eq!( + graph.relevant_heights().collect::>(), + vec![3, 4], + "anchors in height 3 and now 4" + ); + + let _ = graph.insert_anchor( + tx2.txid(), + BlockId { + height: 5, + hash: h!("5a"), + }, + ); + assert_eq!( + graph.relevant_heights().collect::>(), + vec![3, 4, 5], + "anchor for non-existant tx is inserted at height 5, must still be in relevant heights", + ); +} + +/// Ensure that `last_seen` values only increase during [`Append::append`]. +#[test] +fn test_additions_last_seen_append() { + let txid: Txid = h!("test txid"); + + let test_cases: &[(Option, Option)] = &[ + (Some(5), Some(6)), + (Some(5), Some(5)), + (Some(6), Some(5)), + (None, Some(5)), + (Some(5), None), + ]; + + for (original_ls, update_ls) in test_cases { + let mut original = Additions::<()> { + last_seen: original_ls.map(|ls| (txid, ls)).into_iter().collect(), + ..Default::default() + }; + let update = Additions::<()> { + last_seen: update_ls.map(|ls| (txid, ls)).into_iter().collect(), + ..Default::default() + }; + + original.append(update); + assert_eq!( + &original.last_seen.get(&txid).cloned(), + Ord::max(original_ls, update_ls), + ); + } +} diff --git a/example-crates/keychain_tracker_example_cli/src/lib.rs b/example-crates/keychain_tracker_example_cli/src/lib.rs index df42df1a..702cc2a2 100644 --- a/example-crates/keychain_tracker_example_cli/src/lib.rs +++ b/example-crates/keychain_tracker_example_cli/src/lib.rs @@ -13,7 +13,7 @@ use bdk_chain::{ Descriptor, DescriptorPublicKey, }, sparse_chain::{self, ChainPosition}, - DescriptorExt, FullTxOut, + Append, DescriptorExt, FullTxOut, }; use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue}; use bdk_file_store::KeychainStore;