diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 65d3008b7..194c6c901 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -24,7 +24,7 @@ use bdk_chain::{ chain_graph, keychain::{persist, KeychainChangeSet, KeychainScan, KeychainTracker}, sparse_chain, - tx_graph::GraphedTx, + tx_graph::TxInGraph, BlockId, ConfirmationTime, }; use bitcoin::consensus::encode::serialize; @@ -524,7 +524,7 @@ impl Wallet { /// unconfirmed transactions last. pub fn transactions( &self, - ) -> impl DoubleEndedIterator)> + '_ + ) -> impl DoubleEndedIterator)> + '_ { self.keychain_tracker .chain_graph() diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index 147ce2402..43eb64f6e 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -6,12 +6,41 @@ use crate::{ }; /// Represents an observation of some chain data. -#[derive(Debug, Clone, Copy)] -pub enum Observation { +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)] +pub enum ObservedIn { /// The chain data is seen in a block identified by `A`. - InBlock(A), - /// The chain data is seen at this given unix timestamp. - SeenAt(u64), + Block(A), + /// The chain data is seen in mempool at this given timestamp. + Mempool(u64), +} + +impl ChainPosition for ObservedIn { + fn height(&self) -> TxHeight { + match self { + ObservedIn::Block(block_id) => TxHeight::Confirmed(block_id.height), + ObservedIn::Mempool(_) => TxHeight::Unconfirmed, + } + } + + fn max_ord_of_height(height: TxHeight) -> Self { + match height { + TxHeight::Confirmed(height) => ObservedIn::Block(BlockId { + height, + hash: Hash::from_inner([u8::MAX; 32]), + }), + TxHeight::Unconfirmed => Self::Mempool(u64::MAX), + } + } + + fn min_ord_of_height(height: TxHeight) -> Self { + match height { + TxHeight::Confirmed(height) => ObservedIn::Block(BlockId { + height, + hash: Hash::from_inner([u8::MIN; 32]), + }), + TxHeight::Unconfirmed => Self::Mempool(u64::MIN), + } + } } /// Represents the height at which a transaction is confirmed. @@ -177,7 +206,7 @@ impl From<(&u32, &BlockHash)> for BlockId { } /// A `TxOut` with as much data as we can retrieve about it -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct FullTxOut { /// The location of the `TxOut`. pub outpoint: OutPoint, diff --git a/crates/chain/src/chain_graph.rs b/crates/chain/src/chain_graph.rs index 1a6ccb1e0..fcb980433 100644 --- a/crates/chain/src/chain_graph.rs +++ b/crates/chain/src/chain_graph.rs @@ -2,7 +2,7 @@ use crate::{ collections::HashSet, sparse_chain::{self, ChainPosition, SparseChain}, - tx_graph::{self, GraphedTx, TxGraph}, + tx_graph::{self, TxGraph, TxInGraph}, BlockAnchor, BlockId, ForEachTxOut, FullTxOut, TxHeight, }; use alloc::{string::ToString, vec::Vec}; @@ -213,7 +213,7 @@ where /// /// This does not necessarily mean that it is *confirmed* in the blockchain; it might just be in /// the unconfirmed transaction list within the [`SparseChain`]. - pub fn get_tx_in_chain(&self, txid: Txid) -> Option<(&P, GraphedTx<'_, Transaction, A>)> { + pub fn get_tx_in_chain(&self, txid: Txid) -> Option<(&P, TxInGraph<'_, Transaction, A>)> { let position = self.chain.tx_position(txid)?; let graphed_tx = self.graph.get_tx(txid).expect("must exist"); Some((position, graphed_tx)) @@ -441,7 +441,7 @@ where /// in ascending order. pub fn transactions_in_chain( &self, - ) -> impl DoubleEndedIterator)> { + ) -> impl DoubleEndedIterator)> { self.chain .txids() .map(move |(pos, txid)| (pos, self.graph.get_tx(*txid).expect("must exist"))) diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs index b60e0584c..176254b4a 100644 --- a/crates/chain/src/keychain/txout_index.rs +++ b/crates/chain/src/keychain/txout_index.rs @@ -88,9 +88,11 @@ impl Deref for KeychainTxOutIndex { } } -impl TxIndex for KeychainTxOutIndex { +impl TxIndex for KeychainTxOutIndex { type Additions = DerivationAdditions; + type SpkIndex = (K, u32); + fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions { self.scan_txout(outpoint, txout) } @@ -102,6 +104,10 @@ impl TxIndex for KeychainTxOutIndex { fn is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool { self.is_relevant(tx) } + + fn relevant_txouts(&self) -> &BTreeMap { + self.inner.relevant_txouts() + } } impl KeychainTxOutIndex { diff --git a/crates/chain/src/spk_txout_index.rs b/crates/chain/src/spk_txout_index.rs index 3ce6c06c8..3d2f783e3 100644 --- a/crates/chain/src/spk_txout_index.rs +++ b/crates/chain/src/spk_txout_index.rs @@ -52,9 +52,11 @@ impl Default for SpkTxOutIndex { } } -impl TxIndex for SpkTxOutIndex { +impl TxIndex for SpkTxOutIndex { type Additions = BTreeSet; + type SpkIndex = I; + fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions { self.scan_txout(outpoint, txout) .cloned() @@ -69,6 +71,10 @@ impl TxIndex for SpkTxOutIndex { fn is_tx_relevant(&self, tx: &Transaction) -> bool { self.is_relevant(tx) } + + fn relevant_txouts(&self) -> &BTreeMap { + &self.txouts + } } /// This macro is used instead of a member function of `SpkTxOutIndex`, which would result in a diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 43ce487e0..f412f4529 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -1,4 +1,4 @@ -use alloc::collections::BTreeSet; +use alloc::collections::{BTreeMap, BTreeSet}; use bitcoin::{Block, BlockHash, OutPoint, Transaction, TxOut}; use crate::BlockId; @@ -100,6 +100,8 @@ pub trait TxIndex { /// The resultant "additions" when new transaction data is indexed. type Additions: TxIndexAdditions; + type SpkIndex: Ord; + /// Scan and index the given `outpoint` and `txout`. fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions; @@ -120,4 +122,7 @@ pub trait TxIndex { /// A transaction is relevant if it contains a txout with a script_pubkey that we own, or if it /// spends an already-indexed outpoint that we have previously indexed. fn is_tx_relevant(&self, tx: &Transaction) -> bool; + + /// Lists all relevant txouts known by the index. + fn relevant_txouts(&self) -> &BTreeMap; } diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index daa7e1ba8..3181ed2a7 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -56,8 +56,8 @@ //! ``` use crate::{ - collections::*, BlockAnchor, BlockId, ChainOracle, ForEachTxOut, Observation, TxIndex, - TxIndexAdditions, + collections::*, sparse_chain::ChainPosition, BlockAnchor, BlockId, ChainOracle, ForEachTxOut, + FullTxOut, ObservedIn, TxIndex, TxIndexAdditions, }; use alloc::vec::Vec; use bitcoin::{OutPoint, Transaction, TxOut, Txid}; @@ -91,9 +91,12 @@ impl Default for TxGraph { } } +// pub type InChainTx<'a, T, A> = (ObservedIn<&'a A>, TxInGraph<'a, T, A>); +// pub type InChainTxOut<'a, I, A> = (&'a I, FullTxOut>); + /// An outward-facing view of a transaction that resides in a [`TxGraph`]. -#[derive(Clone, Debug, PartialEq)] -pub struct GraphedTx<'a, T, A> { +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct TxInGraph<'a, T, A> { /// Txid of the transaction. pub txid: Txid, /// A partial or full representation of the transaction. @@ -104,7 +107,7 @@ pub struct GraphedTx<'a, T, A> { pub last_seen: u64, } -impl<'a, T, A> Deref for GraphedTx<'a, T, A> { +impl<'a, T, A> Deref for TxInGraph<'a, T, A> { type Target = T; fn deref(&self) -> &Self::Target { @@ -112,7 +115,7 @@ impl<'a, T, A> Deref for GraphedTx<'a, T, A> { } } -impl<'a, A> GraphedTx<'a, Transaction, A> { +impl<'a, A> TxInGraph<'a, Transaction, A> { pub fn from_tx(tx: &'a Transaction, anchors: &'a BTreeSet) -> Self { Self { txid: tx.txid(), @@ -123,6 +126,18 @@ impl<'a, A> GraphedTx<'a, Transaction, A> { } } +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct TxInChain<'a, T, A> { + pub observed_in: ObservedIn<&'a A>, + pub tx: TxInGraph<'a, T, A>, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct TxOutInChain<'a, I, A> { + pub spk_index: &'a I, + pub txout: FullTxOut>, +} + /// 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 @@ -157,11 +172,11 @@ impl TxGraph { } /// Iterate over all full transactions in the graph. - pub fn full_transactions(&self) -> impl Iterator> { + pub fn full_transactions(&self) -> impl Iterator> { self.txs .iter() .filter_map(|(&txid, (tx, anchors, last_seen))| match tx { - TxNode::Whole(tx) => Some(GraphedTx { + TxNode::Whole(tx) => Some(TxInGraph { txid, tx, anchors, @@ -176,9 +191,9 @@ impl TxGraph { /// Refer to [`get_txout`] for getting a specific [`TxOut`]. /// /// [`get_txout`]: Self::get_txout - pub fn get_tx(&self, txid: Txid) -> Option> { + pub fn get_tx(&self, txid: Txid) -> Option> { match &self.txs.get(&txid)? { - (TxNode::Whole(tx), anchors, last_seen) => Some(GraphedTx { + (TxNode::Whole(tx), anchors, last_seen) => Some(TxInGraph { txid, tx, anchors, @@ -212,12 +227,6 @@ impl TxGraph { }) } - pub fn get_anchors_and_last_seen(&self, txid: Txid) -> Option<(&BTreeSet, u64)> { - self.txs - .get(&txid) - .map(|(_, anchors, last_seen)| (anchors, *last_seen)) - } - /// Calculates the fee of a given transaction. Returns 0 if `tx` is a coinbase transaction. /// Returns `Some(_)` if we have all the `TxOut`s being spent by `tx` in the graph (either as /// the full transactions or individual txouts). If the returned value is negative, then the @@ -472,10 +481,22 @@ impl TxGraph { self.determine_additions(&update) } + /// Get all heights that are relevant to the graph. + pub fn relevant_heights(&self) -> BTreeSet { + self.anchors + .iter() + .map(|(a, _)| a.anchor_block().height) + .collect() + } + /// Determines whether a transaction of `txid` is in the best chain. /// /// TODO: Also return conflicting tx list, ordered by last_seen. - pub fn is_txid_in_best_chain(&self, chain: C, txid: Txid) -> Result + pub fn get_position_in_chain( + &self, + chain: C, + txid: Txid, + ) -> Result>, C::Error> where C: ChainOracle, { @@ -483,12 +504,12 @@ impl TxGraph { Some((tx, anchors, last_seen)) if !(anchors.is_empty() && *last_seen == 0) => { (tx, anchors, last_seen) } - _ => return Ok(false), + _ => return Ok(None), }; - for block_id in anchors.iter().map(A::anchor_block) { - if chain.is_block_in_best_chain(block_id)? { - return Ok(true); + for anchor in anchors { + if chain.is_block_in_best_chain(anchor.anchor_block())? { + return Ok(Some(ObservedIn::Block(anchor))); } } @@ -499,7 +520,7 @@ impl TxGraph { TxNode::Partial(_) => { // [TODO] Unfortunately, we can't iterate over conflicts of partial txs right now! // [TODO] So we just assume the partial tx does not exist in the best chain :/ - return Ok(false); + return Ok(None); } }; @@ -509,7 +530,7 @@ impl TxGraph { for block_id in conflicting_tx.anchors.iter().map(A::anchor_block) { if chain.is_block_in_best_chain(block_id)? { // conflicting tx is in best chain, so the current tx cannot be in best chain! - return Ok(false); + return Ok(None); } } if conflicting_tx.last_seen > latest_last_seen { @@ -517,28 +538,47 @@ impl TxGraph { } } if last_seen >= latest_last_seen { - Ok(true) + Ok(Some(ObservedIn::Mempool(last_seen))) } else { - Ok(false) + Ok(None) } } - /// Return true if `outpoint` exists in best chain and is unspent. - pub fn is_unspent(&self, chain: C, outpoint: OutPoint) -> Result + pub fn get_spend_in_chain( + &self, + chain: C, + outpoint: OutPoint, + ) -> Result, Txid)>, C::Error> where C: ChainOracle, { - if !self.is_txid_in_best_chain(&chain, outpoint.txid)? { - return Ok(false); + if self.get_position_in_chain(&chain, outpoint.txid)?.is_none() { + return Ok(None); } if let Some(spends) = self.spends.get(&outpoint) { for &txid in spends { - if self.is_txid_in_best_chain(&chain, txid)? { - return Ok(false); + if let Some(observed_at) = self.get_position_in_chain(&chain, txid)? { + return Ok(Some((observed_at, txid))); } } } - Ok(true) + Ok(None) + } + + pub fn transactions_in_chain( + &self, + chain: C, + ) -> Result>, C::Error> + where + C: ChainOracle, + { + self.full_transactions() + .filter_map(|tx| { + self.get_position_in_chain(&chain, tx.txid) + .map(|v| v.map(|observed_in| TxInChain { observed_in, tx })) + .transpose() + }) + .collect() } } @@ -574,12 +614,12 @@ impl TxGraph { /// Iterate over all partial transactions (outputs only) in the graph. pub fn partial_transactions( &self, - ) -> impl Iterator, A>> { + ) -> impl Iterator, A>> { self.txs .iter() .filter_map(|(&txid, (tx, anchors, last_seen))| match tx { TxNode::Whole(_) => None, - TxNode::Partial(partial) => Some(GraphedTx { + TxNode::Partial(partial) => Some(TxInGraph { txid, tx: partial, anchors, @@ -686,18 +726,29 @@ impl Default for IndexedTxGraph { } impl IndexedTxGraph { + /// Get a reference of the internal transaction graph. + pub fn graph(&self) -> &TxGraph { + &self.graph + } + + /// Get a reference of the internal transaction index. + pub fn index(&self) -> &I { + &self.index + } + + /// Insert a `txout` that exists in `outpoint` with the given `observation`. pub fn insert_txout( &mut self, outpoint: OutPoint, txout: &TxOut, - observation: Observation, + observation: ObservedIn, ) -> IndexedAdditions { IndexedAdditions { graph_additions: { let mut graph_additions = self.graph.insert_txout(outpoint, txout.clone()); graph_additions.append(match observation { - Observation::InBlock(anchor) => self.graph.insert_anchor(outpoint.txid, anchor), - Observation::SeenAt(seen_at) => { + ObservedIn::Block(anchor) => self.graph.insert_anchor(outpoint.txid, anchor), + ObservedIn::Mempool(seen_at) => { self.graph.insert_seen_at(outpoint.txid, seen_at) } }); @@ -710,15 +761,15 @@ impl IndexedTxGraph { pub fn insert_tx( &mut self, tx: &Transaction, - observation: Observation, + observation: ObservedIn, ) -> IndexedAdditions { let txid = tx.txid(); IndexedAdditions { graph_additions: { let mut graph_additions = self.graph.insert_tx(tx.clone()); graph_additions.append(match observation { - Observation::InBlock(anchor) => self.graph.insert_anchor(txid, anchor), - Observation::SeenAt(seen_at) => self.graph.insert_seen_at(txid, seen_at), + ObservedIn::Block(anchor) => self.graph.insert_anchor(txid, anchor), + ObservedIn::Mempool(seen_at) => self.graph.insert_seen_at(txid, seen_at), }); graph_additions }, @@ -729,7 +780,7 @@ impl IndexedTxGraph { pub fn filter_and_insert_txs<'t, T>( &mut self, txs: T, - observation: Observation, + observation: ObservedIn, ) -> IndexedAdditions where T: Iterator, @@ -746,6 +797,81 @@ impl IndexedTxGraph { acc }) } + + pub fn relevant_heights(&self) -> BTreeSet { + self.graph.relevant_heights() + } + + pub fn txs_in_chain( + &self, + chain: C, + ) -> Result>, C::Error> + where + C: ChainOracle, + { + let mut tx_set = self.graph.transactions_in_chain(chain)?; + tx_set.retain(|tx| self.index.is_tx_relevant(&tx.tx)); + Ok(tx_set) + } + + pub fn txouts_in_chain( + &self, + chain: C, + ) -> Result>, C::Error> + where + C: ChainOracle, + ObservedIn: ChainPosition, + { + self.index + .relevant_txouts() + .iter() + .filter_map(|(op, (spk_i, txout))| -> Option> { + let graph_tx = self.graph.get_tx(op.txid)?; + + let is_on_coinbase = graph_tx.is_coin_base(); + + let chain_position = match self.graph.get_position_in_chain(&chain, op.txid) { + Ok(Some(observed_at)) => observed_at, + Ok(None) => return None, + Err(err) => return Some(Err(err)), + }; + + let spent_by = match self.graph.get_spend_in_chain(&chain, *op) { + Ok(spent_by) => spent_by, + Err(err) => return Some(Err(err)), + }; + + let full_txout = FullTxOut { + outpoint: *op, + txout: txout.clone(), + chain_position, + spent_by, + is_on_coinbase, + }; + + let txout_in_chain = TxOutInChain { + spk_index: spk_i, + txout: full_txout, + }; + + Some(Ok(txout_in_chain)) + }) + .collect() + } + + /// Return relevant unspents. + pub fn utxos_in_chain( + &self, + chain: C, + ) -> Result>, C::Error> + where + C: ChainOracle, + ObservedIn: ChainPosition, + { + let mut txouts = self.txouts_in_chain(chain)?; + txouts.retain(|txo| txo.txout.spent_by.is_none()); + Ok(txouts) + } } /// A structure that represents changes to a [`TxGraph`]. diff --git a/crates/chain/tests/test_chain_graph.rs b/crates/chain/tests/test_chain_graph.rs index cd2a28943..f7b39d2b0 100644 --- a/crates/chain/tests/test_chain_graph.rs +++ b/crates/chain/tests/test_chain_graph.rs @@ -7,7 +7,7 @@ use bdk_chain::{ chain_graph::*, collections::HashSet, sparse_chain, - tx_graph::{self, GraphedTx, TxGraph}, + tx_graph::{self, TxGraph, TxInGraph}, BlockId, TxHeight, }; use bitcoin::{ @@ -371,7 +371,7 @@ fn test_get_tx_in_chain() { cg.get_tx_in_chain(tx.txid()), Some(( &TxHeight::Unconfirmed, - GraphedTx { + TxInGraph { txid: tx.txid(), tx: &tx, anchors: &BTreeSet::new(), @@ -411,15 +411,15 @@ fn test_iterate_transactions() { vec![ ( &TxHeight::Confirmed(0), - GraphedTx::from_tx(&txs[2], &BTreeSet::new()) + TxInGraph::from_tx(&txs[2], &BTreeSet::new()) ), ( &TxHeight::Confirmed(1), - GraphedTx::from_tx(&txs[0], &BTreeSet::new()) + TxInGraph::from_tx(&txs[0], &BTreeSet::new()) ), ( &TxHeight::Unconfirmed, - GraphedTx::from_tx(&txs[1], &BTreeSet::new()) + TxInGraph::from_tx(&txs[1], &BTreeSet::new()) ), ] ); diff --git a/crates/chain/tests/test_keychain_tracker.rs b/crates/chain/tests/test_keychain_tracker.rs index 1c5e07956..b4e51d850 100644 --- a/crates/chain/tests/test_keychain_tracker.rs +++ b/crates/chain/tests/test_keychain_tracker.rs @@ -9,7 +9,7 @@ use bdk_chain::{ bitcoin::{secp256k1::Secp256k1, OutPoint, PackedLockTime, Transaction, TxOut}, Descriptor, }, - tx_graph::GraphedTx, + tx_graph::TxInGraph, BlockId, ConfirmationTime, TxHeight, }; use bitcoin::{BlockHash, TxIn}; @@ -45,7 +45,7 @@ fn test_insert_tx() { .collect::>(), vec![( &ConfirmationTime::Unconfirmed, - GraphedTx::from_tx(&tx, &BTreeSet::new()) + TxInGraph::from_tx(&tx, &BTreeSet::new()) )] ); diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 2550d5568..107e106d5 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -2,7 +2,7 @@ mod common; use bdk_chain::{ collections::*, - tx_graph::{Additions, GraphedTx, TxGraph}, + tx_graph::{Additions, TxGraph, TxInGraph}, BlockId, }; use bitcoin::{ @@ -157,7 +157,7 @@ fn insert_tx_can_retrieve_full_tx_from_graph() { let _ = graph.insert_tx(tx.clone()); assert_eq!( graph.get_tx(tx.txid()), - Some(GraphedTx::from_tx(&tx, &BTreeSet::new())) + Some(TxInGraph::from_tx(&tx, &BTreeSet::new())) ); }