//! Module for structures that combine the features of [`sparse_chain`] and [`tx_graph`]. use crate::{ collections::HashSet, sparse_chain::{self, ChainPosition, SparseChain}, tx_graph::{self, TxGraph}, AsTransaction, BlockId, ForEachTxOut, FullTxOut, IntoOwned, TxHeight, }; use alloc::{borrow::Cow, string::ToString, vec::Vec}; use bitcoin::{OutPoint, Transaction, TxOut, Txid}; use core::fmt::Debug; /// A consistent combination of a [`SparseChain

`] and a [`TxGraph`]. /// /// `SparseChain` only keeps track of transaction ids and their position in the chain but you often /// want to store the full transactions as well. Additionally you want to make sure that everything /// in the chain is consistent with the full transaction data. `ChainGraph` enforces these two /// invariants: /// /// 1. Every transaction that is in the chain is also in the graph (you always have the full /// transaction). /// 2. No transactions in the chain conflict with each other i.e. they don't double spend each /// other or have ancestors that double spend each other. /// /// Note that the `ChainGraph` guarantees a 1:1 mapping between transactions in the `chain` and /// `graph` but not the other way around. Transactions may fall out of the *chain* (via re-org or /// mempool eviction) but will remain in the *graph*. #[derive(Clone, Debug, PartialEq)] pub struct ChainGraph

{ chain: SparseChain

, graph: TxGraph, } impl Default for ChainGraph { fn default() -> Self { Self { chain: Default::default(), graph: Default::default(), } } } impl AsRef> for ChainGraph { fn as_ref(&self) -> &SparseChain

{ &self.chain } } impl AsRef> for ChainGraph { fn as_ref(&self) -> &TxGraph { &self.graph } } impl AsRef> for ChainGraph { fn as_ref(&self) -> &ChainGraph { self } } impl ChainGraph { /// Returns a reference to the internal [`SparseChain`]. pub fn chain(&self) -> &SparseChain

{ &self.chain } /// Returns a reference to the internal [`TxGraph`]. pub fn graph(&self) -> &TxGraph { &self.graph } } impl ChainGraph where P: ChainPosition, T: AsTransaction + Clone + Ord, { /// Create a new chain graph from a `chain` and a `graph`. /// /// There are two reasons this can return an `Err`: /// /// 1. There is a transaction in the `chain` that does not have its corresponding full /// transaction in `graph`. /// 2. The `chain` has two transactions that allegedly in it but they conflict in the `graph` /// (so could not possibly be in the same chain). pub fn new(chain: SparseChain

, graph: TxGraph) -> Result> { let mut missing = HashSet::default(); for (pos, txid) in chain.txids() { if let Some(tx) = graph.get_tx(*txid) { let conflict = graph .walk_conflicts(tx.as_tx(), |_, txid| { Some((chain.tx_position(txid)?.clone(), txid)) }) .next(); if let Some((conflict_pos, conflict)) = conflict { return Err(NewError::Conflict { a: (pos.clone(), *txid), b: (conflict_pos, conflict), }); } } else { missing.insert(*txid); } } if !missing.is_empty() { return Err(NewError::Missing(missing)); } Ok(Self { chain, graph }) } /// Take an update in the form of a [`SparseChain

`][`SparseChain`] and attempt to turn it /// into a chain graph by filling in full transactions from `self` and from `new_txs`. This /// returns a `ChainGraph>` where the [`Cow<'a, T>`] will borrow the transaction if it /// got it from `self`. /// /// This is useful when interacting with services like an electrum server which returns a list /// of txids and heights when calling [`script_get_history`] which can easily be inserted into a /// [`SparseChain`][`SparseChain`]. From there you need to figure out which full /// transactions you are missing in your chain graph and form `new_txs`. You then use /// `inflate_update` to turn this into an update `ChainGraph>` and finally /// use [`determine_changeset`] to generate the changeset from it. /// /// [`SparseChain`]: crate::sparse_chain::SparseChain /// [`Cow<'a, T>`]: std::borrow::Cow /// [`script_get_history`]: https://docs.rs/electrum-client/latest/electrum_client/trait.ElectrumApi.html#tymethod.script_get_history /// [`determine_changeset`]: Self::determine_changeset pub fn inflate_update( &self, update: SparseChain

, new_txs: impl IntoIterator, ) -> Result>, NewError

> { let mut inflated_graph = TxGraph::default(); for (_, txid) in update.txids() { if let Some(tx) = self.graph.get_tx(*txid) { let _ = inflated_graph.insert_tx(Cow::Borrowed(tx)); } } for tx in new_txs { let _ = inflated_graph.insert_tx(Cow::Owned(tx)); } ChainGraph::new(update, inflated_graph) } /// Sets the checkpoint limit. /// /// Refer to [`SparseChain::checkpoint_limit`] for more. pub fn checkpoint_limit(&self) -> Option { self.chain.checkpoint_limit() } /// Sets the checkpoint limit. /// /// Refer to [`SparseChain::set_checkpoint_limit`] for more. pub fn set_checkpoint_limit(&mut self, limit: Option) { self.chain.set_checkpoint_limit(limit) } /// Determines the changes required to invalidate checkpoints `from_height` (inclusive) and /// above. Displaced transactions will have their positions moved to [`TxHeight::Unconfirmed`]. pub fn invalidate_checkpoints_preview(&self, from_height: u32) -> ChangeSet { ChangeSet { chain: self.chain.invalidate_checkpoints_preview(from_height), ..Default::default() } } /// Invalidate checkpoints `from_height` (inclusive) and above. Displaced transactions will be /// re-positioned to [`TxHeight::Unconfirmed`]. /// /// This is equivalent to calling [`Self::invalidate_checkpoints_preview`] and /// [`Self::apply_changeset`] in sequence. pub fn invalidate_checkpoints(&mut self, from_height: u32) -> ChangeSet where ChangeSet: Clone, { let changeset = self.invalidate_checkpoints_preview(from_height); self.apply_changeset(changeset.clone()); changeset } /// Get a transaction that is currently in the underlying [`SparseChain`]. /// /// 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, &T)> { let position = self.chain.tx_position(txid)?; let full_tx = self.graph.get_tx(txid).expect("must exist"); Some((position, full_tx)) } /// Determines the changes required to insert a transaction into the inner [`ChainGraph`] and /// [`SparseChain`] at the given `position`. /// /// If inserting it into the chain `position` will result in conflicts, the returned /// [`ChangeSet`] should evict conflicting transactions. pub fn insert_tx_preview(&self, tx: T, pos: P) -> Result, InsertTxError

> { let mut changeset = ChangeSet { chain: self.chain.insert_tx_preview(tx.as_tx().txid(), pos)?, graph: self.graph.insert_tx_preview(tx), }; self.fix_conflicts(&mut changeset)?; Ok(changeset) } /// Inserts [`Transaction`] at given chain position. /// /// This is equivalent to calling [`Self::insert_tx_preview`] and [`Self::apply_changeset`] in /// sequence. pub fn insert_tx(&mut self, tx: T, pos: P) -> Result, InsertTxError

> { let changeset = self.insert_tx_preview(tx, pos)?; self.apply_changeset(changeset.clone()); Ok(changeset) } /// Determines the changes required to insert a [`TxOut`] into the internal [`TxGraph`]. pub fn insert_txout_preview(&self, outpoint: OutPoint, txout: TxOut) -> ChangeSet { ChangeSet { chain: Default::default(), graph: self.graph.insert_txout_preview(outpoint, txout), } } /// Inserts a [`TxOut`] into the internal [`TxGraph`]. /// /// This is equivalent to calling [`Self::insert_txout_preview`] and [`Self::apply_changeset`] /// in sequence. pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet { let changeset = self.insert_txout_preview(outpoint, txout); self.apply_changeset(changeset.clone()); changeset } /// Determines the changes required to insert a `block_id` (a height and block hash) into the /// chain. /// /// If a checkpoint already exists at that height with a different hash this will return /// an error. pub fn insert_checkpoint_preview( &self, block_id: BlockId, ) -> Result, InsertCheckpointError> { self.chain .insert_checkpoint_preview(block_id) .map(|chain_changeset| ChangeSet { chain: chain_changeset, ..Default::default() }) } /// Inserts checkpoint into [`Self`]. /// /// This is equivalent to calling [`Self::insert_checkpoint_preview`] and /// [`Self::apply_changeset`] in sequence. pub fn insert_checkpoint( &mut self, block_id: BlockId, ) -> Result, InsertCheckpointError> { let changeset = self.insert_checkpoint_preview(block_id)?; self.apply_changeset(changeset.clone()); Ok(changeset) } /// Calculates the difference between self and `update` in the form of a [`ChangeSet`]. pub fn determine_changeset( &self, update: &ChainGraph, ) -> Result, UpdateError

> where T2: IntoOwned + Clone, { let chain_changeset = self .chain .determine_changeset(&update.chain) .map_err(UpdateError::Chain)?; let mut changeset = ChangeSet { chain: chain_changeset, graph: self.graph.determine_additions(&update.graph), }; self.fix_conflicts(&mut changeset)?; Ok(changeset) } /// Given a transaction, return an iterator of `txid`s that conflict with it (spends at least /// one of the same inputs). This includes all descendants of conflicting transactions. /// /// This method only returns conflicts that exist in the [`SparseChain`] as transactions that /// are not included in [`SparseChain`] are already considered as evicted. pub fn tx_conflicts_in_chain<'a>( &'a self, tx: &'a Transaction, ) -> impl Iterator + 'a { self.graph.walk_conflicts(tx, |_, conflict_txid| { self.chain .tx_position(conflict_txid) .map(|conflict_pos| (conflict_pos, conflict_txid)) }) } /// Fix changeset conflicts. /// /// **WARNING:** If there are any missing full txs, conflict resolution will not be complete. In /// debug mode, this will result in panic. fn fix_conflicts( &self, changeset: &mut ChangeSet, ) -> Result<(), UnresolvableConflict

> { let chain_conflicts = changeset .chain .txids .iter() // we want to find new txid additions by the changeset (all txid entries in the // changeset with Some(position_change)) .filter_map(|(&txid, pos_change)| pos_change.as_ref().map(|pos| (txid, pos))) // we don't care about txids that move, only newly added txids .filter(|&(txid, _)| self.chain.tx_position(txid).is_none()) // full tx should exist (either in graph, or additions) .filter_map(|(txid, pos)| { let full_tx = self .graph .get_tx(txid) .or_else(|| { changeset .graph .tx .iter() .find(|tx| tx.as_tx().txid() == txid) }) .map(|tx| (txid, tx, pos)); debug_assert!(full_tx.is_some(), "should have full tx at this point"); full_tx }) .flat_map(|(new_txid, new_tx, new_pos)| { self.tx_conflicts_in_chain(new_tx.as_tx()).map( move |(conflict_pos, conflict_txid)| { (new_pos.clone(), new_txid, conflict_pos, conflict_txid) }, ) }) .collect::>(); for (update_pos, update_txid, conflicting_pos, conflicting_txid) in chain_conflicts { // We have found a tx that conflicts with our update txid. Only allow this when the // conflicting tx will be positioned as "unconfirmed" after the update is applied. // If so, we will modify the changeset to evict the conflicting txid. // determine the position of the conflicting txid after current changeset is applied let conflicting_new_pos = changeset .chain .txids .get(&conflicting_txid) .map(Option::as_ref) .unwrap_or(Some(conflicting_pos)); match conflicting_new_pos { None => { // conflicting txid will be deleted, can ignore } Some(existing_new_pos) => match existing_new_pos.height() { TxHeight::Confirmed(_) => { // the new postion of the conflicting tx is "confirmed", therefore cannot be // evicted, return error return Err(UnresolvableConflict { already_confirmed_tx: (conflicting_pos.clone(), conflicting_txid), update_tx: (update_pos, update_txid), }); } TxHeight::Unconfirmed => { // the new position of the conflicting tx is "unconfirmed", therefore it can // be evicted changeset.chain.txids.insert(conflicting_txid, None); } }, }; } Ok(()) } /// Applies `changeset` to `self`. /// /// **Warning** this method assumes the changeset is assumed to be correctly formed. If it isn't /// then the chain graph may not behave correctly in the future and may panic unexpectedly. pub fn apply_changeset(&mut self, changeset: ChangeSet) { self.chain.apply_changeset(changeset.chain); self.graph.apply_additions(changeset.graph); } /// Applies the `update` chain graph. Note this is shorthand for calling /// [`Self::determine_changeset()`] and [`Self::apply_changeset()`] in sequence. pub fn apply_update + Clone>( &mut self, update: ChainGraph, ) -> Result, UpdateError

> { let changeset = self.determine_changeset(&update)?; self.apply_changeset(changeset.clone()); Ok(changeset) } /// Get the full transaction output at an outpoint if it exists in the chain and the graph. pub fn full_txout(&self, outpoint: OutPoint) -> Option> { self.chain.full_txout(&self.graph, outpoint) } /// Iterate over the full transactions and their position in the chain ordered by their position /// in ascending order. pub fn transactions_in_chain(&self) -> impl DoubleEndedIterator { self.chain .txids() .map(|(pos, txid)| (pos, self.graph.get_tx(*txid).expect("must exist"))) } /// Finds the transaction in the chain that spends `outpoint` given the input/output /// relationships in `graph`. Note that the transaction including `outpoint` does not need to be /// in the `graph` or the `chain` for this to return `Some(_)`. pub fn spent_by(&self, outpoint: OutPoint) -> Option<(&P, Txid)> { self.chain.spent_by(&self.graph, outpoint) } /// Whether the chain graph contains any data whatsoever. pub fn is_empty(&self) -> bool { self.chain.is_empty() && self.graph.is_empty() } } /// Represents changes to [`ChainGraph`]. /// /// This is essentially a combination of [`sparse_chain::ChangeSet`] and [`tx_graph::Additions`]. #[derive(Debug, Clone, PartialEq)] #[cfg_attr( feature = "serde", derive(serde::Deserialize, serde::Serialize), serde( crate = "serde_crate", bound( deserialize = "P: serde::Deserialize<'de>, T: Ord + serde::Deserialize<'de>", serialize = "P: serde::Serialize, T: Ord + serde::Serialize" ) ) )] #[must_use] pub struct ChangeSet { pub chain: sparse_chain::ChangeSet

, pub graph: tx_graph::Additions, } impl ChangeSet { /// Returns `true` if this [`ChangeSet`] records no changes. pub fn is_empty(&self) -> bool { self.chain.is_empty() && self.graph.is_empty() } /// Returns `true` if this [`ChangeSet`] contains transaction evictions. pub fn contains_eviction(&self) -> bool { self.chain .txids .iter() .any(|(_, new_pos)| new_pos.is_none()) } /// Appends the changes in `other` into self such that applying `self` afterwards has the same /// effect as sequentially applying the original `self` and `other`. pub fn append(&mut self, other: ChangeSet) where P: ChainPosition, T: Ord, { self.chain.append(other.chain); self.graph.append(other.graph); } } impl Default for ChangeSet { fn default() -> Self { Self { chain: Default::default(), graph: Default::default(), } } } impl ForEachTxOut for ChainGraph { fn for_each_txout(&self, f: impl FnMut((OutPoint, &TxOut))) { self.graph.for_each_txout(f) } } impl ForEachTxOut for ChangeSet { fn for_each_txout(&self, f: impl FnMut((OutPoint, &TxOut))) { self.graph.for_each_txout(f) } } /// Error that may occur when calling [`ChainGraph::new`]. #[derive(Clone, Debug, PartialEq)] pub enum NewError

{ /// Two transactions within the sparse chain conflicted with each other Conflict { a: (P, Txid), b: (P, Txid) }, /// One or more transactions in the chain were not in the graph Missing(HashSet), } impl core::fmt::Display for NewError

{ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { NewError::Conflict { a, b } => write!( f, "Unable to inflate sparse chain to chain graph since transactions {:?} and {:?}", a, b ), NewError::Missing(missing) => write!( f, "missing full transactions for {}", missing .iter() .map(|txid| txid.to_string()) .collect::>() .join(", ") ), } } } #[cfg(feature = "std")] impl std::error::Error for NewError

{} /// Error that may occur when inserting a transaction. /// /// Refer to [`ChainGraph::insert_tx_preview`] and [`ChainGraph::insert_tx`]. #[derive(Clone, Debug, PartialEq)] pub enum InsertTxError

{ Chain(sparse_chain::InsertTxError

), UnresolvableConflict(UnresolvableConflict

), } impl core::fmt::Display for InsertTxError

{ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { InsertTxError::Chain(inner) => core::fmt::Display::fmt(inner, f), InsertTxError::UnresolvableConflict(inner) => core::fmt::Display::fmt(inner, f), } } } impl

From> for InsertTxError

{ fn from(inner: sparse_chain::InsertTxError

) -> Self { Self::Chain(inner) } } #[cfg(feature = "std")] impl std::error::Error for InsertTxError

{} /// A nice alias of [`sparse_chain::InsertCheckpointError`]. pub type InsertCheckpointError = sparse_chain::InsertCheckpointError; /// Represents an update failure. #[derive(Clone, Debug, PartialEq)] pub enum UpdateError

{ /// The update chain was inconsistent with the existing chain Chain(sparse_chain::UpdateError

), /// A transaction in the update spent the same input as an already confirmed transaction UnresolvableConflict(UnresolvableConflict

), } impl core::fmt::Display for UpdateError

{ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { UpdateError::Chain(inner) => core::fmt::Display::fmt(inner, f), UpdateError::UnresolvableConflict(inner) => core::fmt::Display::fmt(inner, f), } } } impl

From> for UpdateError

{ fn from(inner: sparse_chain::UpdateError

) -> Self { Self::Chain(inner) } } #[cfg(feature = "std")] impl std::error::Error for UpdateError

{} /// Represents an unresolvable conflict between an update's transaction and an /// already-confirmed transaction. #[derive(Clone, Debug, PartialEq)] pub struct UnresolvableConflict

{ pub already_confirmed_tx: (P, Txid), pub update_tx: (P, Txid), } impl core::fmt::Display for UnresolvableConflict

{ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let Self { already_confirmed_tx, update_tx, } = self; write!(f, "update transaction {} at height {:?} conflicts with an already confirmed transaction {} at height {:?}", update_tx.1, update_tx.0, already_confirmed_tx.1, already_confirmed_tx.0) } } impl

From> for UpdateError

{ fn from(inner: UnresolvableConflict

) -> Self { Self::UnresolvableConflict(inner) } } impl

From> for InsertTxError

{ fn from(inner: UnresolvableConflict

) -> Self { Self::UnresolvableConflict(inner) } } #[cfg(feature = "std")] impl std::error::Error for UnresolvableConflict

{}