diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index aa1e74d4..d8ce8cda 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -2,16 +2,19 @@ use bitcoin::{hashes::Hash, BlockHash, OutPoint, TxOut, Txid}; use crate::{ sparse_chain::{self, ChainPosition}, - BlockAnchor, COINBASE_MATURITY, + Anchor, ConfirmationHeight, COINBASE_MATURITY, }; /// Represents an observation of some chain data. /// -/// The generic `A` should be a [`BlockAnchor`] implementation. +/// 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 assumed to be confirmed, because a transaction that spends it is anchored + /// by `A`. + ConfirmedImplicit(A), /// The chain data is seen in mempool at this given timestamp. Unconfirmed(u64), } @@ -20,6 +23,7 @@ impl ObservedAs<&A> { pub fn cloned(self) -> ObservedAs { match self { ObservedAs::Confirmed(a) => ObservedAs::Confirmed(a.clone()), + ObservedAs::ConfirmedImplicit(a) => ObservedAs::ConfirmedImplicit(a.clone()), ObservedAs::Unconfirmed(last_seen) => ObservedAs::Unconfirmed(last_seen), } } @@ -160,7 +164,7 @@ impl Default for BlockId { } } -impl BlockAnchor for BlockId { +impl Anchor for BlockId { fn anchor_block(&self) -> BlockId { *self } @@ -241,20 +245,23 @@ impl FullTxOut

{ } } -impl FullTxOut> { +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 [`BlockAnchor`]. + /// [`ObservedAs`] where `A` implements [`Anchor`]. /// /// [`is_mature`]: Self::is_mature - pub fn is_observed_as_confirmed_and_mature(&self, tip: u32) -> bool { + pub fn is_observed_as_mature(&self, tip: u32) -> bool { if !self.is_on_coinbase { return false; } let tx_height = match &self.chain_position { - ObservedAs::Confirmed(anchor) => anchor.anchor_block().height, + ObservedAs::Confirmed(anchor) => anchor.confirmation_height(), + // although we do not know the exact confirm height, the returned height here is the + // "upper bound" so only false-negatives are possible + ObservedAs::ConfirmedImplicit(anchor) => anchor.confirmation_height(), ObservedAs::Unconfirmed(_) => { debug_assert!(false, "coinbase tx can never be unconfirmed"); return false; @@ -274,22 +281,24 @@ impl FullTxOut> { /// Currently 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 [`BlockAnchor`]. + /// being a [`ObservedAs`] where `A` implements [`Anchor`]. /// /// [`is_spendable_at`]: Self::is_spendable_at pub fn is_observed_as_confirmed_and_spendable(&self, tip: u32) -> bool { - if !self.is_observed_as_confirmed_and_mature(tip) { + if !self.is_observed_as_mature(tip) { return false; } - match &self.chain_position { - ObservedAs::Confirmed(anchor) => { - if anchor.anchor_block().height > tip { - return false; - } - } + let confirmation_height = match &self.chain_position { + ObservedAs::Confirmed(anchor) => anchor.confirmation_height(), + // although we do not know the exact confirm height, the returned height here is the + // "upper bound" so only false-negatives are possible + ObservedAs::ConfirmedImplicit(anchor) => anchor.confirmation_height(), 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 { diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 0cee62ed..00137c41 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -1,13 +1,16 @@ use core::convert::Infallible; -use bitcoin::{OutPoint, Script, Transaction, TxOut}; +use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid}; use crate::{ keychain::Balance, - tx_graph::{Additions, TxGraph, TxNode}, - Append, BlockAnchor, BlockId, ChainOracle, FullTxOut, ObservedAs, + tx_graph::{Additions, CanonicalTx, TxGraph}, + Anchor, Append, BlockId, ChainOracle, ConfirmationHeight, 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, @@ -23,7 +26,7 @@ impl Default for IndexedTxGraph { } } -impl IndexedTxGraph { +impl IndexedTxGraph { /// Get a reference of the internal transaction graph. pub fn graph(&self) -> &TxGraph { &self.graph @@ -47,195 +50,191 @@ impl IndexedTxGraph { self.graph.apply_additions(graph_additions); } +} - /// Insert a `txout` that exists in `outpoint` with the given `observation`. +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, - observation: ObservedAs, ) -> IndexedAdditions { - IndexedAdditions { - graph_additions: { - let mut graph_additions = self.graph.insert_txout(outpoint, txout.clone()); - graph_additions.append(match observation { - ObservedAs::Confirmed(anchor) => { - self.graph.insert_anchor(outpoint.txid, anchor) - } - ObservedAs::Unconfirmed(seen_at) => { - self.graph.insert_seen_at(outpoint.txid, seen_at) - } - }); - graph_additions - }, - index_additions: ::index_txout(&mut self.index, outpoint, txout), - } + 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, - observation: ObservedAs, + anchors: impl IntoIterator, + seen_at: Option, ) -> IndexedAdditions { let txid = tx.txid(); - IndexedAdditions { - graph_additions: { - let mut graph_additions = self.graph.insert_tx(tx.clone()); - graph_additions.append(match observation { - ObservedAs::Confirmed(anchor) => self.graph.insert_anchor(txid, anchor), - ObservedAs::Unconfirmed(seen_at) => self.graph.insert_seen_at(txid, seen_at), - }); - graph_additions - }, - index_additions: ::index_tx(&mut self.index, tx), + 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. + /// + /// `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, T>( &mut self, txs: T, - observation: ObservedAs, + anchors: impl IntoIterator + Clone, + seen_at: Option, ) -> IndexedAdditions where T: Iterator, - I::Additions: Default + Append, { - txs.filter_map(|tx| { - if self.index.is_tx_relevant(tx) { - Some(self.insert_tx(tx, observation.clone())) - } else { - None - } + txs.filter_map(|tx| match self.index.is_tx_relevant(tx) { + true => Some(self.insert_tx(tx, anchors.clone(), seen_at)), + false => None, }) - .fold(IndexedAdditions::default(), |mut acc, other| { + .fold(Default::default(), |mut acc, other| { acc.append(other); acc }) } +} - // [TODO] Have to methods, one for relevant-only, and one for any. Have one in `TxGraph`. - pub fn try_list_chain_txs<'a, C>( +impl IndexedTxGraph { + pub fn try_list_owned_txs<'a, C>( &'a self, chain: &'a C, - static_block: BlockId, + chain_tip: BlockId, ) -> impl Iterator, C::Error>> where C: ChainOracle + 'a, { self.graph - .full_transactions() - .filter(|tx| self.index.is_tx_relevant(tx)) - .filter_map(move |tx| { + .full_txs() + .filter(|node| tx_alters_owned_utxo_set(&self.graph, &self.index, node.txid, node.tx)) + .filter_map(move |tx_node| { self.graph - .try_get_chain_position(chain, static_block, tx.txid) + .try_get_chain_position(chain, chain_tip, tx_node.txid) .map(|v| { - v.map(|observed_in| CanonicalTx { - observed_as: observed_in, - tx, + v.map(|observed_as| CanonicalTx { + observed_as, + node: tx_node, }) }) .transpose() }) } - pub fn list_chain_txs<'a, C>( + pub fn list_owned_txs<'a, C>( &'a self, chain: &'a C, - static_block: BlockId, + chain_tip: BlockId, ) -> impl Iterator> where C: ChainOracle + 'a, { - self.try_list_chain_txs(chain, static_block) - .map(|r| r.expect("error is infallible")) + self.try_list_owned_txs(chain, chain_tip) + .map(|r| r.expect("chain oracle is infallible")) } - pub fn try_list_chain_txouts<'a, C>( + pub fn try_list_owned_txouts<'a, C>( &'a self, chain: &'a C, - static_block: BlockId, + chain_tip: BlockId, ) -> impl Iterator>, C::Error>> + 'a where C: ChainOracle + 'a, { - self.graph - .all_txouts() - .filter(|&(op, txo)| self.index.is_txout_relevant(op, txo)) - .filter_map(move |(op, 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 - .try_get_chain_position(chain, static_block, op.txid) - { - Ok(Some(observed_at)) => observed_at.cloned(), - Ok(None) => return None, - Err(err) => return Some(Err(err)), - }; - - let spent_by = match self.graph.try_get_spend_in_chain(chain, static_block, op) { - Ok(Some((obs, txid))) => Some((obs.cloned(), txid)), - Ok(None) => None, - Err(err) => return Some(Err(err)), - }; - - let full_txout = FullTxOut { - outpoint: op, - txout: txout.clone(), - chain_position, - spent_by, - is_on_coinbase, - }; - - Some(Ok(full_txout)) + self.graph() + .try_list_chain_txouts(chain, chain_tip, |_, txout| { + self.index.is_spk_owned(&txout.script_pubkey) }) } - pub fn list_chain_txouts<'a, C>( + pub fn list_owned_txouts<'a, C>( &'a self, chain: &'a C, - static_block: BlockId, + chain_tip: BlockId, ) -> impl Iterator>> + 'a where - C: ChainOracle + 'a, + C: ChainOracle + 'a, { - self.try_list_chain_txouts(chain, static_block) - .map(|r| r.expect("error in infallible")) + self.try_list_owned_txouts(chain, chain_tip) + .map(|r| r.expect("oracle is infallible")) } - /// Return relevant unspents. - pub fn try_list_chain_utxos<'a, C>( + pub fn try_list_owned_unspents<'a, C>( &'a self, chain: &'a C, - static_block: BlockId, + chain_tip: BlockId, ) -> impl Iterator>, C::Error>> + 'a where C: ChainOracle + 'a, { - self.try_list_chain_txouts(chain, static_block) - .filter(|r| !matches!(r, Ok(txo) if txo.spent_by.is_none())) + self.graph() + .try_list_chain_unspents(chain, chain_tip, |_, txout| { + self.index.is_spk_owned(&txout.script_pubkey) + }) } - pub fn list_chain_utxos<'a, C>( + pub fn list_owned_unspents<'a, C>( &'a self, chain: &'a C, - static_block: BlockId, + chain_tip: BlockId, ) -> impl Iterator>> + 'a where - C: ChainOracle + 'a, + C: ChainOracle + 'a, { - self.try_list_chain_utxos(chain, static_block) - .map(|r| r.expect("error is infallible")) + self.try_list_owned_unspents(chain, chain_tip) + .map(|r| r.expect("oracle is infallible")) } +} +impl IndexedTxGraph { pub fn try_balance( &self, chain: &C, - static_block: BlockId, + chain_tip: BlockId, tip: u32, mut should_trust: F, ) -> Result @@ -248,13 +247,13 @@ impl IndexedTxGraph { let mut untrusted_pending = 0; let mut confirmed = 0; - for res in self.try_list_chain_txouts(chain, static_block) { + for res in self.try_list_owned_txouts(chain, chain_tip) { let txout = res?; match &txout.chain_position { - ObservedAs::Confirmed(_) => { + ObservedAs::Confirmed(_) | ObservedAs::ConfirmedImplicit(_) => { if txout.is_on_coinbase { - if txout.is_observed_as_confirmed_and_mature(tip) { + if txout.is_observed_as_mature(tip) { confirmed += txout.txout.value; } else { immature += txout.txout.value; @@ -304,7 +303,10 @@ impl IndexedTxGraph { C: ChainOracle, { let mut sum = 0; - for txo_res in self.try_list_chain_txouts(chain, static_block) { + for txo_res in self + .graph() + .try_list_chain_txouts(chain, static_block, |_, _| true) + { let txo = txo_res?; if txo.is_observed_as_confirmed_and_spendable(height) { sum += txo.txout.value; @@ -339,7 +341,7 @@ impl IndexedTxGraph { pub struct IndexedAdditions { /// [`TxGraph`] additions. pub graph_additions: Additions, - /// [`TxIndex`] additions. + /// [`Indexer`] additions. pub index_additions: IA, } @@ -352,24 +354,15 @@ impl Default for IndexedAdditions { } } -impl Append for IndexedAdditions { +impl Append for IndexedAdditions { fn append(&mut self, other: Self) { self.graph_additions.append(other.graph_additions); self.index_additions.append(other.index_additions); } } -/// 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> { - /// Where the transaction is observed (in a block or in mempool). - pub observed_as: ObservedAs<&'a A>, - /// The transaction with anchors and last seen timestamp. - pub tx: TxNode<'a, T, A>, -} - -/// Represents an index of transaction data. -pub trait TxIndex { +/// Represents a structure that can index transaction data. +pub trait Indexer { /// The resultant "additions" when new transaction data is indexed. type Additions; @@ -382,11 +375,30 @@ pub trait TxIndex { /// Apply additions to itself. fn apply_additions(&mut self, additions: Self::Additions); - /// Returns whether the txout is marked as relevant in the index. - fn is_txout_relevant(&self, outpoint: OutPoint, txout: &TxOut) -> bool; - - /// Returns whether the transaction is marked as relevant in the index. + /// Determines whether the transaction should be included in the index. fn is_tx_relevant(&self, tx: &Transaction) -> bool; } -pub trait SpkIndex: TxIndex {} +/// 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; +} + +fn tx_alters_owned_utxo_set( + graph: &TxGraph, + index: &I, + txid: Txid, + tx: &Transaction, +) -> bool +where + A: Anchor, + I: OwnedIndexer, +{ + let prev_spends = (0..tx.input.len() as u32) + .map(|vout| OutPoint { txid, vout }) + .filter_map(|op| graph.get_txout(op)); + prev_spends + .chain(&tx.output) + .any(|txout| index.is_spk_owned(&txout.script_pubkey)) +} diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs index 43623a3e..e4ac3ef4 100644 --- a/crates/chain/src/keychain/txout_index.rs +++ b/crates/chain/src/keychain/txout_index.rs @@ -1,6 +1,6 @@ use crate::{ collections::*, - indexed_tx_graph::TxIndex, + indexed_tx_graph::Indexer, miniscript::{Descriptor, DescriptorPublicKey}, ForEachTxOut, SpkTxOutIndex, }; @@ -91,7 +91,7 @@ impl Deref for KeychainTxOutIndex { } } -impl TxIndex for KeychainTxOutIndex { +impl Indexer for KeychainTxOutIndex { type Additions = DerivationAdditions; fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions { @@ -106,10 +106,6 @@ impl TxIndex for KeychainTxOutIndex { self.apply_additions(additions) } - fn is_txout_relevant(&self, _outpoint: OutPoint, txout: &TxOut) -> bool { - self.index_of_spk(&txout.script_pubkey).is_some() - } - fn is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool { self.is_relevant(tx) } diff --git a/crates/chain/src/sparse_chain.rs b/crates/chain/src/sparse_chain.rs index acc61601..fbcdcaa5 100644 --- a/crates/chain/src/sparse_chain.rs +++ b/crates/chain/src/sparse_chain.rs @@ -997,7 +997,7 @@ impl SparseChain

{ /// `chain` for this to return `Some`. pub fn spent_by(&self, graph: &TxGraph, outpoint: OutPoint) -> Option<(&P, Txid)> { graph - .outspends(outpoint) + .output_spends(outpoint) .iter() .find_map(|&txid| Some((self.tx_position(txid)?, txid))) } diff --git a/crates/chain/src/spk_txout_index.rs b/crates/chain/src/spk_txout_index.rs index 03b7b6a7..3e89ba39 100644 --- a/crates/chain/src/spk_txout_index.rs +++ b/crates/chain/src/spk_txout_index.rs @@ -2,7 +2,7 @@ use core::ops::RangeBounds; use crate::{ collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap}, - indexed_tx_graph::TxIndex, + indexed_tx_graph::Indexer, ForEachTxOut, }; use bitcoin::{self, OutPoint, Script, Transaction, TxOut, Txid}; @@ -53,7 +53,7 @@ impl Default for SpkTxOutIndex { } } -impl TxIndex for SpkTxOutIndex { +impl Indexer for SpkTxOutIndex { type Additions = (); fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions { @@ -70,10 +70,6 @@ impl TxIndex for SpkTxOutIndex { // This applies nothing. } - fn is_txout_relevant(&self, _outpoint: OutPoint, txout: &TxOut) -> bool { - self.index_of_spk(&txout.script_pubkey).is_some() - } - fn is_tx_relevant(&self, tx: &Transaction) -> bool { self.is_relevant(tx) } diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 401b00f9..9d41c549 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -40,25 +40,34 @@ impl ForEachTxOut for Transaction { /// 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 BlockAnchor: +pub trait Anchor: core::fmt::Debug + Clone + Eq + PartialOrd + Ord + core::hash::Hash + Send + Sync + 'static { /// Returns the [`BlockId`] that the associated blockchain data is "anchored" in. fn anchor_block(&self) -> BlockId; } -impl BlockAnchor for &'static A { +impl Anchor for &'static A { fn anchor_block(&self) -> BlockId { - ::anchor_block(self) + ::anchor_block(self) } } -impl BlockAnchor for (u32, BlockHash) { +impl Anchor for (u32, BlockHash) { fn anchor_block(&self) -> BlockId { (*self).into() } } +/// A trait that returns a confirmation height. +/// +/// This is typically used to provide an [`Anchor`] implementation the exact confirmation height of +/// the data being anchored. +pub trait ConfirmationHeight { + /// Returns the confirmation height. + fn confirmation_height(&self) -> u32; +} + /// Trait that makes an object appendable. pub trait Append { /// Append another object of the same type onto `self`. diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index e3afce0e..7c32606a 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -55,7 +55,7 @@ //! assert!(additions.is_empty()); //! ``` -use crate::{collections::*, BlockAnchor, BlockId, ChainOracle, ForEachTxOut, ObservedAs}; +use crate::{collections::*, Anchor, BlockId, ChainOracle, ForEachTxOut, FullTxOut, ObservedAs}; use alloc::vec::Vec; use bitcoin::{OutPoint, Transaction, TxOut, Txid}; use core::{ @@ -91,7 +91,7 @@ impl Default for TxGraph { } } -/// An outward-facing representation of a (transaction) node in the [`TxGraph`]. +/// 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. @@ -139,8 +139,19 @@ impl Default for TxNodeInternal { } } +/// 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 { TxNodeInternal::Whole(tx) => tx @@ -156,8 +167,26 @@ impl TxGraph { }) } + /// 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> { + pub fn full_txs(&self) -> impl Iterator> { self.txs .iter() .filter_map(|(&txid, (tx, anchors, last_seen))| match tx { @@ -201,8 +230,10 @@ impl TxGraph { } } + /// 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> { + pub fn tx_outputs(&self, txid: Txid) -> Option> { Some(match &self.txs.get(&txid)?.0 { TxNodeInternal::Whole(tx) => tx .output @@ -251,7 +282,7 @@ impl TxGraph { /// /// `TxGraph` allows conflicting transactions within the graph. Obviously the transactions in /// the returned set will never be in the same active-chain. - pub fn outspends(&self, outpoint: OutPoint) -> &HashSet { + pub fn output_spends(&self, outpoint: OutPoint) -> &HashSet { self.spends.get(&outpoint).unwrap_or(&self.empty_outspends) } @@ -261,7 +292,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)> + '_ { @@ -275,23 +306,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, A>> { - self.txs - .iter() - .filter_map(|(&txid, (tx, anchors, last_seen))| match tx { - TxNodeInternal::Whole(_) => None, - TxNodeInternal::Partial(partial) => Some(TxNode { - txid, - tx: partial, - anchors, - last_seen_unconfirmed: *last_seen, - }), - }) - } - /// Creates an iterator that filters and maps descendants from the starting `txid`. /// /// The supplied closure takes in two inputs `(depth, descendant_txid)`: @@ -363,6 +377,9 @@ impl TxGraph { /// 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 { @@ -380,8 +397,10 @@ impl TxGraph { /// 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!). + /// 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()); @@ -581,7 +600,7 @@ impl TxGraph { } } -impl TxGraph { +impl TxGraph { /// Get all heights that are relevant to the graph. pub fn relevant_heights(&self) -> impl Iterator + '_ { let mut visited = HashSet::new(); @@ -591,13 +610,21 @@ impl TxGraph { .filter(move |&h| visited.insert(h)) } - /// Determines whether a transaction of `txid` is in the best chain. + /// Get the position of the transaction in `chain` with tip `chain_tip`. /// - /// TODO: Also return conflicting tx list, ordered by last_seen. + /// 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, - static_block: BlockId, + chain_tip: BlockId, txid: Txid, ) -> Result>, C::Error> where @@ -611,28 +638,27 @@ impl TxGraph { }; for anchor in anchors { - match chain.is_block_in_chain(anchor.anchor_block(), static_block)? { + match chain.is_block_in_chain(anchor.anchor_block(), chain_tip)? { Some(true) => return Ok(Some(ObservedAs::Confirmed(anchor))), - Some(false) => continue, - // if we cannot determine whether block is in the best chain, we can check whether - // a spending transaction is confirmed in best chain, and if so, it is guaranteed - // that the tx being spent (this tx) is in the best chain - None => { - let spending_anchors = self - .spends - .range(OutPoint::new(txid, u32::MIN)..=OutPoint::new(txid, u32::MAX)) - .flat_map(|(_, spending_txids)| spending_txids) - .filter_map(|spending_txid| self.txs.get(spending_txid)) - .flat_map(|(_, spending_anchors, _)| spending_anchors); - for spending_anchor in spending_anchors { - match chain - .is_block_in_chain(spending_anchor.anchor_block(), static_block)? - { - Some(true) => return Ok(Some(ObservedAs::Confirmed(anchor))), - _ => continue, - } - } - } + _ => continue, + } + } + + // If we cannot determine whether tx is in best chain, we can check whether a spending tx is + // confirmed and in best chain, and if so, it is guaranteed that this tx is in the best + // chain. + // + // [TODO] This logic is incomplete as we do not check spends of spends. + let spending_anchors = self + .spends + .range(OutPoint::new(txid, u32::MIN)..=OutPoint::new(txid, u32::MAX)) + .flat_map(|(_, spending_txids)| spending_txids) + .filter_map(|spending_txid| self.txs.get(spending_txid)) + .flat_map(|(_, spending_anchors, _)| spending_anchors); + for spending_anchor in spending_anchors { + match chain.is_block_in_chain(spending_anchor.anchor_block(), chain_tip)? { + Some(true) => return Ok(Some(ObservedAs::ConfirmedImplicit(spending_anchor))), + _ => continue, } } @@ -650,7 +676,7 @@ impl TxGraph { // 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, static_block)? == Some(true) { + 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); } @@ -663,37 +689,54 @@ impl TxGraph { 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, - static_block: BlockId, + chain_tip: BlockId, txid: Txid, ) -> Option> where C: ChainOracle, { - self.try_get_chain_position(chain, static_block, txid) + self.try_get_chain_position(chain, chain_tip, txid) .expect("error is infallible") } - pub fn try_get_spend_in_chain( + /// 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, - static_block: BlockId, + chain_tip: BlockId, outpoint: OutPoint, ) -> Result, Txid)>, C::Error> where C: ChainOracle, { if self - .try_get_chain_position(chain, static_block, outpoint.txid)? + .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, static_block, txid)? { + if let Some(observed_at) = self.try_get_chain_position(chain, chain_tip, txid)? { return Ok(Some((observed_at, txid))); } } @@ -701,6 +744,12 @@ impl TxGraph { 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, @@ -710,9 +759,186 @@ impl TxGraph { where C: ChainOracle, { - self.try_get_spend_in_chain(chain, static_block, outpoint) + 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>( + &'a self, + chain: &'a C, + chain_tip: BlockId, + ) -> impl Iterator, C::Error>> + where + C: ChainOracle + 'a, + { + 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>( + &'a self, + chain: &'a C, + chain_tip: BlockId, + ) -> impl Iterator> + where + C: ChainOracle + 'a, + { + 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, P>( + &'a self, + chain: &'a C, + chain_tip: BlockId, + mut filter_predicate: P, + ) -> impl Iterator>, C::Error>> + 'a + where + C: ChainOracle + 'a, + P: FnMut(OutPoint, &TxOut) -> bool + '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() + .filter_map(|(vout, txout)| { + let outpoint = OutPoint::new(canonical_tx.node.txid, vout as _); + if filter_predicate(outpoint, txout) { + Some(Ok((outpoint, txout.clone(), canonical_tx.clone()))) + } else { + None + } + }) + .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, P>( + &'a self, + chain: &'a C, + chain_tip: BlockId, + filter_predicate: P, + ) -> impl Iterator>> + 'a + where + C: ChainOracle + 'a, + P: FnMut(OutPoint, &TxOut) -> bool + 'a, + { + self.try_list_chain_txouts(chain, chain_tip, filter_predicate) + .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, P>( + &'a self, + chain: &'a C, + chain_tip: BlockId, + filter_txout: P, + ) -> impl Iterator>, C::Error>> + 'a + where + C: ChainOracle + 'a, + P: FnMut(OutPoint, &TxOut) -> bool + 'a, + { + self.try_list_chain_txouts(chain, chain_tip, filter_txout) + .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, P>( + &'a self, + chain: &'a C, + static_block: BlockId, + filter_txout: P, + ) -> impl Iterator>> + 'a + where + C: ChainOracle + 'a, + P: FnMut(OutPoint, &TxOut) -> bool + 'a, + { + self.try_list_chain_unspents(chain, static_block, filter_txout) + .map(|r| r.expect("error is infallible")) + } } /// A structure that represents changes to a [`TxGraph`]. diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index e3bc0ac8..965bb259 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -149,12 +149,12 @@ fn insert_txouts() { // Apply addition and check the new graph counts. graph.apply_additions(additions); assert_eq!(graph.all_txouts().count(), 4); - assert_eq!(graph.full_transactions().count(), 1); - assert_eq!(graph.partial_transactions().count(), 2); + 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.txouts(h!("tx1")).expect("should exists"), + graph.tx_outputs(h!("tx1")).expect("should exists"), [ ( 1u32, @@ -175,7 +175,7 @@ fn insert_txouts() { ); assert_eq!( - graph.txouts(update_txs.txid()).expect("should exists"), + graph.tx_outputs(update_txs.txid()).expect("should exists"), [( 0u32, &TxOut { @@ -201,8 +201,8 @@ fn insert_tx_graph_doesnt_count_coinbase_as_spent() { 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.output_spends(OutPoint::null()).is_empty()); + assert!(graph.tx_spends(Txid::all_zeros()).next().is_none()); } #[test] @@ -240,10 +240,10 @@ fn insert_tx_graph_keeps_track_of_spend() { let _ = graph2.insert_tx(tx1); assert_eq!( - graph1.outspends(op), + graph1.output_spends(op), &iter::once(tx2.txid()).collect::>() ); - assert_eq!(graph2.outspends(op), graph1.outspends(op)); + assert_eq!(graph2.output_spends(op), graph1.output_spends(op)); } #[test]