[bdk_chain_redesign] Fix Anchor definition + docs

Previously, I have misunderstood the definition of anchor. If a tx is
anchored in a block, it does not necessarily mean it is confirmed in
that block. The tx can be confirmed in an ancestor block of the anchor
block.

With this new definition, we need a new trait `ConfirmationHeight` that
has one method `confirmation_height`. This trait can be used to extend
`Anchor` for those implementations that can give us the exact
conirmation height of a tx (which is useful in most cases).

Another change is to add another variant to the `ObservedAs` enum;
`ObservedAs::ConfirmedImplicit(A)`. If a tx does not have an anchor, but
another tx that spends it has an anchor that in in the best chain, we
can assume that tx is also in the best chain. The logic of
`TxGraph::try_get_chain_position` is also changed to reflect this.

Some methods from `IndexedTxGraph` have been moved to `TxGraph` as they
do not require the `Indexer`. Some `TxGraph` methods have been renamed
for clarity and consistency.

Also more docs are added.
This commit is contained in:
志宇 2023-04-17 23:25:57 +08:00
parent 001efdd1cb
commit 81436fcd72
No known key found for this signature in database
GPG Key ID: F6345C9837C2BDE8
8 changed files with 474 additions and 226 deletions

View File

@ -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<A> {
/// 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<A: Clone> ObservedAs<&A> {
pub fn cloned(self) -> ObservedAs<A> {
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<P: ChainPosition> FullTxOut<P> {
}
}
impl<A: BlockAnchor> FullTxOut<ObservedAs<A>> {
impl<A: Anchor + ConfirmationHeight> FullTxOut<ObservedAs<A>> {
/// Whether the `txout` is considered mature.
///
/// This is the alternative version of [`is_mature`] which depends on `chain_position` being a
/// [`ObservedAs<A>`] where `A` implements [`BlockAnchor`].
/// [`ObservedAs<A>`] 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<A: BlockAnchor> FullTxOut<ObservedAs<A>> {
/// 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<A>`] where `A` implements [`BlockAnchor`].
/// being a [`ObservedAs<A>`] 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 {

View File

@ -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<A, I> {
/// Transaction index.
pub index: I,
@ -23,7 +26,7 @@ impl<A, I: Default> Default for IndexedTxGraph<A, I> {
}
}
impl<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I> {
/// Get a reference of the internal transaction graph.
pub fn graph(&self) -> &TxGraph<A> {
&self.graph
@ -47,195 +50,191 @@ impl<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
self.graph.apply_additions(graph_additions);
}
}
/// Insert a `txout` that exists in `outpoint` with the given `observation`.
impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I>
where
I::Additions: Default + Append,
{
/// Apply an `update` directly.
///
/// `update` is a [`TxGraph<A>`] and the resultant changes is returned as [`IndexedAdditions`].
pub fn apply_update(&mut self, update: TxGraph<A>) -> IndexedAdditions<A, I::Additions> {
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<A>,
) -> IndexedAdditions<A, I::Additions> {
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: <I as TxIndex>::index_txout(&mut self.index, outpoint, txout),
}
let mut update = TxGraph::<A>::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<A>,
anchors: impl IntoIterator<Item = A>,
seen_at: Option<u64>,
) -> 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 {
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: <I as TxIndex>::index_tx(&mut self.index, tx),
let mut update = TxGraph::<A>::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<A>,
anchors: impl IntoIterator<Item = A> + Clone,
seen_at: Option<u64>,
) -> IndexedAdditions<A, I::Additions>
where
T: Iterator<Item = &'t Transaction>,
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<A: Anchor, I: OwnedIndexer> IndexedTxGraph<A, I> {
pub fn try_list_owned_txs<'a, C>(
&'a self,
chain: &'a C,
static_block: BlockId,
chain_tip: BlockId,
) -> impl Iterator<Item = Result<CanonicalTx<'a, Transaction, A>, 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<Item = CanonicalTx<'a, Transaction, A>>
where
C: ChainOracle<Error = Infallible> + '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<Item = Result<FullTxOut<ObservedAs<A>>, 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<Result<_, C::Error>> {
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<Item = FullTxOut<ObservedAs<A>>> + 'a
where
C: ChainOracle<Error = Infallible> + '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<Item = Result<FullTxOut<ObservedAs<A>>, 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<Item = FullTxOut<ObservedAs<A>>> + 'a
where
C: ChainOracle<Error = Infallible> + '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<A: Anchor + ConfirmationHeight, I: OwnedIndexer> IndexedTxGraph<A, I> {
pub fn try_balance<C, F>(
&self,
chain: &C,
static_block: BlockId,
chain_tip: BlockId,
tip: u32,
mut should_trust: F,
) -> Result<Balance, C::Error>
@ -248,13 +247,13 @@ impl<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
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<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
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<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
pub struct IndexedAdditions<A, IA> {
/// [`TxGraph`] additions.
pub graph_additions: Additions<A>,
/// [`TxIndex`] additions.
/// [`Indexer`] additions.
pub index_additions: IA,
}
@ -352,24 +354,15 @@ impl<A, IA: Default> Default for IndexedAdditions<A, IA> {
}
}
impl<A: BlockAnchor, IA: Append> Append for IndexedAdditions<A, IA> {
impl<A: Anchor, IA: Append> Append for IndexedAdditions<A, IA> {
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<A, I>(
graph: &TxGraph<A>,
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))
}

View File

@ -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<K> Deref for KeychainTxOutIndex<K> {
}
}
impl<K: Clone + Ord + Debug + 'static> TxIndex for KeychainTxOutIndex<K> {
impl<K: Clone + Ord + Debug + 'static> Indexer for KeychainTxOutIndex<K> {
type Additions = DerivationAdditions<K>;
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions {
@ -106,10 +106,6 @@ impl<K: Clone + Ord + Debug + 'static> TxIndex for KeychainTxOutIndex<K> {
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)
}

View File

@ -997,7 +997,7 @@ impl<P: ChainPosition> SparseChain<P> {
/// `chain` for this to return `Some`.
pub fn spent_by<A>(&self, graph: &TxGraph<A>, outpoint: OutPoint) -> Option<(&P, Txid)> {
graph
.outspends(outpoint)
.output_spends(outpoint)
.iter()
.find_map(|&txid| Some((self.tx_position(txid)?, txid)))
}

View File

@ -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<I> Default for SpkTxOutIndex<I> {
}
}
impl<I: Clone + Ord + 'static> TxIndex for SpkTxOutIndex<I> {
impl<I: Clone + Ord + 'static> Indexer for SpkTxOutIndex<I> {
type Additions = ();
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions {
@ -70,10 +70,6 @@ impl<I: Clone + Ord + 'static> TxIndex for SpkTxOutIndex<I> {
// 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)
}

View File

@ -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<A: BlockAnchor> BlockAnchor for &'static A {
impl<A: Anchor> Anchor for &'static A {
fn anchor_block(&self) -> BlockId {
<A as BlockAnchor>::anchor_block(self)
<A as Anchor>::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`.

View File

@ -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<A> Default for TxGraph<A> {
}
}
/// 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<A> TxGraph<A> {
/// 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<Item = (OutPoint, &TxOut)> {
self.txs.iter().flat_map(|(txid, (tx, _, _))| match tx {
TxNodeInternal::Whole(tx) => tx
@ -156,8 +167,26 @@ impl<A> TxGraph<A> {
})
}
/// 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<Item = (OutPoint, &TxOut)> {
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<Item = TxNode<'_, Transaction, A>> {
pub fn full_txs(&self) -> impl Iterator<Item = TxNode<'_, Transaction, A>> {
self.txs
.iter()
.filter_map(|(&txid, (tx, anchors, last_seen))| match tx {
@ -201,8 +230,10 @@ impl<A> TxGraph<A> {
}
}
/// 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<BTreeMap<u32, &TxOut>> {
pub fn tx_outputs(&self, txid: Txid) -> Option<BTreeMap<u32, &TxOut>> {
Some(match &self.txs.get(&txid)?.0 {
TxNodeInternal::Whole(tx) => tx
.output
@ -251,7 +282,7 @@ impl<A> TxGraph<A> {
///
/// `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<Txid> {
pub fn output_spends(&self, outpoint: OutPoint) -> &HashSet<Txid> {
self.spends.get(&outpoint).unwrap_or(&self.empty_outspends)
}
@ -261,7 +292,7 @@ impl<A> TxGraph<A> {
///
/// - `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<Item = (u32, &HashSet<Txid>)> + '_ {
@ -275,23 +306,6 @@ impl<A> TxGraph<A> {
.map(|(outpoint, spends)| (outpoint.vout, spends))
}
/// Iterate over all partial transactions (outputs only) in the graph.
pub fn partial_transactions(
&self,
) -> impl Iterator<Item = TxNode<'_, BTreeMap<u32, TxOut>, 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<A: Clone + Ord> TxGraph<A> {
/// 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<A> {
@ -380,8 +397,10 @@ impl<A: Clone + Ord> TxGraph<A> {
/// 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<A> {
let additions = self.insert_txout_preview(outpoint, txout);
self.apply_additions(additions.clone());
@ -581,7 +600,7 @@ impl<A: Clone + Ord> TxGraph<A> {
}
}
impl<A: BlockAnchor> TxGraph<A> {
impl<A: Anchor> TxGraph<A> {
/// Get all heights that are relevant to the graph.
pub fn relevant_heights(&self) -> impl Iterator<Item = u32> + '_ {
let mut visited = HashSet::new();
@ -591,13 +610,21 @@ impl<A: BlockAnchor> TxGraph<A> {
.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<C>(
&self,
chain: &C,
static_block: BlockId,
chain_tip: BlockId,
txid: Txid,
) -> Result<Option<ObservedAs<&A>>, C::Error>
where
@ -611,28 +638,27 @@ impl<A: BlockAnchor> TxGraph<A> {
};
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<A: BlockAnchor> TxGraph<A> {
// 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<A: BlockAnchor> TxGraph<A> {
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<C>(
&self,
chain: &C,
static_block: BlockId,
chain_tip: BlockId,
txid: Txid,
) -> Option<ObservedAs<&A>>
where
C: ChainOracle<Error = Infallible>,
{
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<C>(
/// 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<C>(
&self,
chain: &C,
static_block: BlockId,
chain_tip: BlockId,
outpoint: OutPoint,
) -> Result<Option<(ObservedAs<&A>, 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<A: BlockAnchor> TxGraph<A> {
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<C>(
&self,
chain: &C,
@ -710,9 +759,186 @@ impl<A: BlockAnchor> TxGraph<A> {
where
C: ChainOracle<Error = Infallible>,
{
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<Item = Result<CanonicalTx<'a, Transaction, A>, 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<Item = CanonicalTx<'a, Transaction, A>>
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<Item = Result<FullTxOut<ObservedAs<A>>, 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::<Vec<_>>(),
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<Item = FullTxOut<ObservedAs<A>>> + 'a
where
C: ChainOracle<Error = Infallible> + '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<Item = Result<FullTxOut<ObservedAs<A>>, 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<Item = FullTxOut<ObservedAs<A>>> + 'a
where
C: ChainOracle<Error = Infallible> + '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`].

View File

@ -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::<HashSet<_>>()
);
assert_eq!(graph2.outspends(op), graph1.outspends(op));
assert_eq!(graph2.output_spends(op), graph1.output_spends(op));
}
#[test]