BlockAnchor, COINBASE_MATURITY,
};
+/// Represents an observation of some chain data.
+#[derive(Debug, Clone, Copy)]
+pub enum Observation<A> {
+ /// 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(
collections::BTreeMap,
sparse_chain::ChainPosition,
tx_graph::TxGraph,
- ForEachTxOut,
+ ForEachTxOut, TxIndexAdditions,
};
#[cfg(feature = "miniscript")]
}
}
+impl<K: Ord> TxIndexAdditions for DerivationAdditions<K> {
+ fn append_additions(&mut self, other: Self) {
+ self.append(other)
+ }
+}
+
impl<K> Default for DerivationAdditions<K> {
fn default() -> Self {
Self(Default::default())
use crate::{
collections::*,
miniscript::{Descriptor, DescriptorPublicKey},
- ForEachTxOut, SpkTxOutIndex,
+ ForEachTxOut, SpkTxOutIndex, TxIndex,
};
use alloc::{borrow::Cow, vec::Vec};
use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, TxOut};
}
}
+impl<K: Clone + Ord + Debug> TxIndex for KeychainTxOutIndex<K> {
+ type Additions = DerivationAdditions<K>;
+
+ 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<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// Scans an object for relevant outpoints, which are stored and indexed internally.
///
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
#[cfg(feature = "std")]
impl<P: core::fmt::Debug> std::error::Error for UpdateError<P> {}
+impl<P: ChainPosition> ChainOracle for SparseChain<P> {
+ type Error = ();
+
+ fn get_block_in_best_chain(&self, height: u32) -> Result<Option<BlockHash>, Self::Error> {
+ Ok(self.checkpoint_at(height).map(|b| b.hash))
+ }
+}
+
impl<P: ChainPosition> SparseChain<P> {
/// Creates a new chain from a list of block hashes and heights. The caller must guarantee they
/// are in the same chain.
use crate::{
collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap},
- ForEachTxOut,
+ ForEachTxOut, TxIndex,
};
use bitcoin::{self, OutPoint, Script, Transaction, TxOut, Txid};
}
}
+impl<I: Clone + Ord> TxIndex for SpkTxOutIndex<I> {
+ type Additions = BTreeSet<I>;
+
+ 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.
+use alloc::collections::BTreeSet;
use bitcoin::{Block, BlockHash, OutPoint, Transaction, TxOut};
use crate::BlockId;
fn anchor_block(&self) -> BlockId;
}
+impl<A: BlockAnchor> BlockAnchor for &'static A {
+ fn anchor_block(&self) -> BlockId {
+ <A as BlockAnchor>::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<Option<BlockHash>, Self::Error>;
+
+ /// Determines whether the block of [`BlockId`] exists in the best chain.
+ fn is_block_in_best_chain(&self, block_id: BlockId) -> Result<bool, Self::Error> {
+ Ok(matches!(self.get_block_in_best_chain(block_id.height)?, Some(h) if h == block_id.hash))
+ }
+}
+
+impl<C: ChainOracle> ChainOracle for &C {
+ type Error = C::Error;
+
+ fn get_block_in_best_chain(&self, height: u32) -> Result<Option<BlockHash>, Self::Error> {
+ <C as ChainOracle>::get_block_in_best_chain(self, height)
+ }
+
+ fn is_block_in_best_chain(&self, block_id: BlockId) -> Result<bool, Self::Error> {
+ <C as ChainOracle>::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<I: Ord> TxIndexAdditions for BTreeSet<I> {
+ 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;
+}
//! 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};
})
}
+ pub fn get_anchors_and_last_seen(&self, txid: Txid) -> Option<(&BTreeSet<A>, 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
*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<C>(&self, chain: C, txid: Txid) -> Result<bool, C::Error>
+ 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<C>(&self, chain: C, outpoint: OutPoint) -> Result<bool, C::Error>
+ 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<A> TxGraph<A> {
}
}
+pub struct IndexedAdditions<A, D> {
+ pub graph_additions: Additions<A>,
+ pub index_delta: D,
+}
+
+impl<A, D: Default> Default for IndexedAdditions<A, D> {
+ fn default() -> Self {
+ Self {
+ graph_additions: Default::default(),
+ index_delta: Default::default(),
+ }
+ }
+}
+
+impl<A: BlockAnchor, D: TxIndexAdditions> TxIndexAdditions for IndexedAdditions<A, D> {
+ 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<A, I> {
+ graph: TxGraph<A>,
+ index: I,
+}
+
+impl<A, I: Default> Default for IndexedTxGraph<A, I> {
+ fn default() -> Self {
+ Self {
+ graph: Default::default(),
+ index: Default::default(),
+ }
+ }
+}
+
+impl<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
+ pub fn insert_txout(
+ &mut self,
+ outpoint: OutPoint,
+ txout: &TxOut,
+ observation: Observation<A>,
+ ) -> IndexedAdditions<A, I::Additions> {
+ 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: <I as TxIndex>::index_txout(&mut self.index, outpoint, txout),
+ }
+ }
+
+ pub fn insert_tx(
+ &mut self,
+ tx: &Transaction,
+ observation: Observation<A>,
+ ) -> IndexedAdditions<A, I::Additions> {
+ 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: <I as TxIndex>::index_tx(&mut self.index, tx),
+ }
+ }
+
+ pub fn filter_and_insert_txs<'t, T>(
+ &mut self,
+ txs: T,
+ observation: Observation<A>,
+ ) -> IndexedAdditions<A, I::Additions>
+ where
+ T: Iterator<Item = &'t Transaction>,
+ {
+ 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