diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index ec76dbb7db..147ce24027 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -5,6 +5,15 @@ use crate::{ BlockAnchor, COINBASE_MATURITY, }; +/// Represents an observation of some chain data. +#[derive(Debug, Clone, Copy)] +pub enum Observation { + /// 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), +} + /// Represents the height at which a transaction is confirmed. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr( diff --git a/crates/chain/src/keychain.rs b/crates/chain/src/keychain.rs index 92d72841fa..dd419db564 100644 --- a/crates/chain/src/keychain.rs +++ b/crates/chain/src/keychain.rs @@ -19,7 +19,7 @@ use crate::{ collections::BTreeMap, sparse_chain::ChainPosition, tx_graph::TxGraph, - ForEachTxOut, + ForEachTxOut, TxIndexAdditions, }; #[cfg(feature = "miniscript")] @@ -85,6 +85,12 @@ impl DerivationAdditions { } } +impl TxIndexAdditions for DerivationAdditions { + fn append_additions(&mut self, other: Self) { + self.append(other) + } +} + impl Default for DerivationAdditions { fn default() -> Self { Self(Default::default()) diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs index feb71edb45..b60e0584c2 100644 --- a/crates/chain/src/keychain/txout_index.rs +++ b/crates/chain/src/keychain/txout_index.rs @@ -1,7 +1,7 @@ use crate::{ collections::*, miniscript::{Descriptor, DescriptorPublicKey}, - ForEachTxOut, SpkTxOutIndex, + ForEachTxOut, SpkTxOutIndex, TxIndex, }; use alloc::{borrow::Cow, vec::Vec}; use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, TxOut}; @@ -88,6 +88,22 @@ impl Deref for KeychainTxOutIndex { } } +impl TxIndex 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 is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool { + self.is_relevant(tx) + } +} + impl KeychainTxOutIndex { /// Scans an object for relevant outpoints, which are stored and indexed internally. /// diff --git a/crates/chain/src/sparse_chain.rs b/crates/chain/src/sparse_chain.rs index a449638d89..eb6e3e2ade 100644 --- a/crates/chain/src/sparse_chain.rs +++ b/crates/chain/src/sparse_chain.rs @@ -311,7 +311,7 @@ use core::{ ops::{Bound, RangeBounds}, }; -use crate::{collections::*, tx_graph::TxGraph, BlockId, FullTxOut, TxHeight}; +use crate::{collections::*, tx_graph::TxGraph, BlockId, ChainOracle, FullTxOut, TxHeight}; use bitcoin::{hashes::Hash, BlockHash, OutPoint, Txid}; /// This is a non-monotone structure that tracks relevant [`Txid`]s that are ordered by chain @@ -456,6 +456,14 @@ impl core::fmt::Display for UpdateError

{ #[cfg(feature = "std")] impl std::error::Error for UpdateError

{} +impl ChainOracle for SparseChain

{ + type Error = (); + + fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error> { + Ok(self.checkpoint_at(height).map(|b| b.hash)) + } +} + impl SparseChain

{ /// Creates a new chain from a list of block hashes and heights. The caller must guarantee they /// are in the same chain. diff --git a/crates/chain/src/spk_txout_index.rs b/crates/chain/src/spk_txout_index.rs index 7f46604fc3..3ce6c06c8c 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}, - ForEachTxOut, + ForEachTxOut, TxIndex, }; use bitcoin::{self, OutPoint, Script, Transaction, TxOut, Txid}; @@ -52,6 +52,25 @@ impl Default for SpkTxOutIndex { } } +impl TxIndex for SpkTxOutIndex { + type Additions = BTreeSet; + + fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions { + self.scan_txout(outpoint, txout) + .cloned() + .into_iter() + .collect() + } + + fn index_tx(&mut self, tx: &Transaction) -> Self::Additions { + self.scan(tx) + } + + fn is_tx_relevant(&self, tx: &Transaction) -> bool { + self.is_relevant(tx) + } +} + /// 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 9b9facabeb..43ce487e06 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -1,3 +1,4 @@ +use alloc::collections::BTreeSet; use bitcoin::{Block, BlockHash, OutPoint, Transaction, TxOut}; use crate::BlockId; @@ -44,8 +45,79 @@ pub trait BlockAnchor: fn anchor_block(&self) -> BlockId; } +impl BlockAnchor for &'static A { + fn anchor_block(&self) -> BlockId { + ::anchor_block(self) + } +} + impl BlockAnchor for (u32, BlockHash) { fn anchor_block(&self) -> BlockId { (*self).into() } } + +/// Represents a service that tracks the best chain history. +pub trait ChainOracle { + /// Error type. + type Error: core::fmt::Debug; + + /// Returns the block hash (if any) of the given `height`. + fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error>; + + /// Determines whether the block of [`BlockId`] exists in the best chain. + fn is_block_in_best_chain(&self, block_id: BlockId) -> Result { + Ok(matches!(self.get_block_in_best_chain(block_id.height)?, Some(h) if h == block_id.hash)) + } +} + +impl ChainOracle for &C { + type Error = C::Error; + + fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error> { + ::get_block_in_best_chain(self, height) + } + + fn is_block_in_best_chain(&self, block_id: BlockId) -> Result { + ::is_block_in_best_chain(self, block_id) + } +} + +/// Represents changes to a [`TxIndex`] implementation. +pub trait TxIndexAdditions: Default { + /// Append `other` on top of `self`. + fn append_additions(&mut self, other: Self); +} + +impl TxIndexAdditions for BTreeSet { + fn append_additions(&mut self, mut other: Self) { + self.append(&mut other); + } +} + +/// Represents an index of transaction data. +pub trait TxIndex { + /// The resultant "additions" when new transaction data is indexed. + type Additions: TxIndexAdditions; + + /// 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 { + let txid = tx.txid(); + tx.output + .iter() + .enumerate() + .map(|(vout, txout)| self.index_txout(OutPoint::new(txid, vout as _), txout)) + .reduce(|mut acc, other| { + acc.append_additions(other); + acc + }) + .unwrap_or_default() + } + + /// 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; +} diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 824b68e207..c8d697085d 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -55,7 +55,10 @@ //! assert!(additions.is_empty()); //! ``` -use crate::{collections::*, BlockAnchor, BlockId, ForEachTxOut}; +use crate::{ + collections::*, BlockAnchor, BlockId, ChainOracle, ForEachTxOut, Observation, TxIndex, + TxIndexAdditions, +}; use alloc::vec::Vec; use bitcoin::{OutPoint, Transaction, TxOut, Txid}; use core::ops::{Deref, RangeInclusive}; @@ -209,6 +212,12 @@ 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 @@ -462,6 +471,75 @@ impl TxGraph { *update_last_seen = seen_at; self.determine_additions(&update) } + + /// 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 + where + C: ChainOracle, + { + 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(false), + }; + + for block_id in anchors.iter().map(A::anchor_block) { + if chain.is_block_in_best_chain(block_id)? { + return Ok(true); + } + } + + // 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 { + TxNode::Whole(tx) => tx, + 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); + } + }; + + // [TODO] Is this logic correct? I do not think so, but it should be good enough for now! + let mut latest_last_seen = 0_u64; + for conflicting_tx in self.walk_conflicts(tx, |_, txid| self.get_tx(txid)) { + 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); + } + } + if conflicting_tx.last_seen > latest_last_seen { + latest_last_seen = conflicting_tx.last_seen; + } + } + if last_seen >= latest_last_seen { + Ok(true) + } else { + Ok(false) + } + } + + /// Return true if `outpoint` exists in best chain and is unspent. + pub fn is_unspent(&self, chain: C, outpoint: OutPoint) -> Result + where + C: ChainOracle, + { + if !self.is_txid_in_best_chain(&chain, outpoint.txid)? { + return Ok(false); + } + if let Some(spends) = self.spends.get(&outpoint) { + for &txid in spends { + if self.is_txid_in_best_chain(&chain, txid)? { + return Ok(false); + } + } + } + Ok(true) + } } impl TxGraph { @@ -568,6 +646,108 @@ impl TxGraph { } } +pub struct IndexedAdditions { + pub graph_additions: Additions, + pub index_delta: D, +} + +impl Default for IndexedAdditions { + fn default() -> Self { + Self { + graph_additions: Default::default(), + index_delta: Default::default(), + } + } +} + +impl TxIndexAdditions for IndexedAdditions { + fn append_additions(&mut self, other: Self) { + let Self { + graph_additions, + index_delta, + } = other; + self.graph_additions.append(graph_additions); + self.index_delta.append_additions(index_delta); + } +} + +pub struct IndexedTxGraph { + graph: TxGraph, + index: I, +} + +impl Default for IndexedTxGraph { + fn default() -> Self { + Self { + graph: Default::default(), + index: Default::default(), + } + } +} + +impl IndexedTxGraph { + pub fn insert_txout( + &mut self, + outpoint: OutPoint, + txout: &TxOut, + observation: Observation, + ) -> 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) => { + self.graph.insert_seen_at(outpoint.txid, seen_at) + } + }); + graph_additions + }, + index_delta: ::index_txout(&mut self.index, outpoint, txout), + } + } + + pub fn insert_tx( + &mut self, + tx: &Transaction, + observation: Observation, + ) -> 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), + }); + graph_additions + }, + index_delta: ::index_tx(&mut self.index, tx), + } + } + + pub fn filter_and_insert_txs<'t, T>( + &mut self, + txs: T, + observation: Observation, + ) -> IndexedAdditions + where + T: Iterator, + { + txs.filter_map(|tx| { + if self.index.is_tx_relevant(tx) { + Some(self.insert_tx(tx, observation.clone())) + } else { + None + } + }) + .fold(IndexedAdditions::default(), |mut acc, other| { + acc.append_additions(other); + acc + }) + } +} + /// A structure that represents changes to a [`TxGraph`]. /// /// It is named "additions" because [`TxGraph`] is monotone, so transactions can only be added and