//! 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 {
chain: SparseChain ,
graph: TxGraph Default for ChainGraph {
fn default() -> Self {
Self {
chain: Default::default(),
graph: Default::default(),
}
}
}
impl AsRef {
fn as_ref(&self) -> &SparseChain {
&self.chain
}
}
impl AsRef {
fn as_ref(&self) -> &TxGraph AsRef {
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 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 `][`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 >` 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 > {
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 {
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 > {
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 > {
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 ,
) -> Result >
where
T2: IntoOwned ,
) -> 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:: ) {
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 ,
) -> Result > {
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 {
pub chain: sparse_chain::ChangeSet ,
pub graph: tx_graph::Additions 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 {
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:: {}
/// 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 {
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 {
fn from(inner: sparse_chain::InsertTxError ) -> Self {
Self::Chain(inner)
}
}
#[cfg(feature = "std")]
impl {}
/// 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 {
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 {
fn from(inner: sparse_chain::UpdateError ) -> Self {
Self::Chain(inner)
}
}
#[cfg(feature = "std")]
impl {}
/// 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 {
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 {
fn from(inner: UnresolvableConflict ) -> Self {
Self::UnresolvableConflict(inner)
}
}
impl From {
fn from(inner: UnresolvableConflict ) -> Self {
Self::UnresolvableConflict(inner)
}
}
#[cfg(feature = "std")]
impl {}