};
/// Represents an observation of some chain data.
+///
+/// The generic `A` should be a [`BlockAnchor`] implementation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)]
-pub enum ObservedIn<A> {
- /// The chain data is seen in a block identified by `A`.
- Block(A),
+pub enum ObservedAs<A> {
+ /// The chain data is seen as confirmed, and in anchored by `A`.
+ Confirmed(A),
/// The chain data is seen in mempool at this given timestamp.
- /// TODO: Call this `Unconfirmed`.
- Mempool(u64),
+ Unconfirmed(u64),
}
-impl<A: Clone> ObservedIn<&A> {
- pub fn into_owned(self) -> ObservedIn<A> {
+impl<A: Clone> ObservedAs<&A> {
+ pub fn cloned(self) -> ObservedAs<A> {
match self {
- ObservedIn::Block(a) => ObservedIn::Block(a.clone()),
- ObservedIn::Mempool(last_seen) => ObservedIn::Mempool(last_seen),
- }
- }
-}
-
-impl ChainPosition for ObservedIn<BlockId> {
- fn height(&self) -> TxHeight {
- match self {
- ObservedIn::Block(block_id) => TxHeight::Confirmed(block_id.height),
- ObservedIn::Mempool(_) => TxHeight::Unconfirmed,
- }
- }
-
- fn max_ord_of_height(height: TxHeight) -> Self {
- match height {
- TxHeight::Confirmed(height) => ObservedIn::Block(BlockId {
- height,
- hash: Hash::from_inner([u8::MAX; 32]),
- }),
- TxHeight::Unconfirmed => Self::Mempool(u64::MAX),
- }
- }
-
- fn min_ord_of_height(height: TxHeight) -> Self {
- match height {
- TxHeight::Confirmed(height) => ObservedIn::Block(BlockId {
- height,
- hash: Hash::from_inner([u8::MIN; 32]),
- }),
- TxHeight::Unconfirmed => Self::Mempool(u64::MIN),
+ ObservedAs::Confirmed(a) => ObservedAs::Confirmed(a.clone()),
+ ObservedAs::Unconfirmed(last_seen) => ObservedAs::Unconfirmed(last_seen),
}
}
}
/// A `TxOut` with as much data as we can retrieve about it
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
-pub struct FullTxOut<I> {
+pub struct FullTxOut<P> {
/// The location of the `TxOut`.
pub outpoint: OutPoint,
/// The `TxOut`.
pub txout: TxOut,
/// The position of the transaction in `outpoint` in the overall chain.
- pub chain_position: I,
+ pub chain_position: P,
/// The txid and chain position of the transaction (if any) that has spent this output.
- pub spent_by: Option<(I, Txid)>,
+ pub spent_by: Option<(P, Txid)>,
/// Whether this output is on a coinbase transaction.
pub is_on_coinbase: bool,
}
-impl<I: ChainPosition> FullTxOut<I> {
+impl<P: ChainPosition> FullTxOut<P> {
/// Whether the utxo is/was/will be spendable at `height`.
///
/// It is spendable if it is not an immature coinbase output and no spending tx has been
}
}
-impl<A: Clone> FullTxOut<ObservedIn<&A>> {
- pub fn into_owned(self) -> FullTxOut<ObservedIn<A>> {
- FullTxOut {
- outpoint: self.outpoint,
- txout: self.txout,
- chain_position: self.chain_position.into_owned(),
- spent_by: self.spent_by.map(|(o, txid)| (o.into_owned(), txid)),
- is_on_coinbase: self.is_on_coinbase,
+impl<A: BlockAnchor> 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`].
+ ///
+ /// [`is_mature`]: Self::is_mature
+ 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::Unconfirmed(_) => {
+ debug_assert!(false, "coinbase tx can never be unconfirmed");
+ return false;
+ }
+ };
+
+ let age = tip.saturating_sub(tx_height);
+ if age + 1 < COINBASE_MATURITY {
+ return false;
+ }
+
+ true
+ }
+
+ /// Whether the utxo is/was/will be spendable with chain `tip`.
+ ///
+ /// This is the alternative version of [`is_spendable_at`] which depends on `chain_position`
+ /// being a [`ObservedAs<A>`] where `A` implements [`BlockAnchor`].
+ ///
+ /// [`is_spendable_at`]: Self::is_spendable_at
+ pub fn is_observed_as_spendable(&self, tip: u32) -> bool {
+ if !self.is_observed_as_mature(tip) {
+ return false;
+ }
+
+ match &self.chain_position {
+ ObservedAs::Confirmed(anchor) => {
+ if anchor.anchor_block().height > tip {
+ return false;
+ }
+ }
+ // [TODO] Why are unconfirmed txs always considered unspendable here?
+ ObservedAs::Unconfirmed(_) => 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 {
+ if spending_anchor.anchor_block().height <= tip {
+ return false;
+ }
+ }
+
+ true
}
}
let _ = inflated_chain
.insert_tx(*txid, pos.clone())
.expect("must insert since this was already in update");
- let _ = inflated_graph.insert_tx(tx.clone());
+ let _ = inflated_graph.insert_tx(tx);
}
}
None => {
/// the unconfirmed transaction list within the [`SparseChain`].
pub fn get_tx_in_chain(&self, txid: Txid) -> Option<(&P, &Transaction)> {
let position = self.chain.tx_position(txid)?;
- let tx = self.graph.get_tx(txid).expect("must exist");
- Some((position, tx))
+ 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
use crate::{
keychain::Balance,
- sparse_chain::ChainPosition,
tx_graph::{Additions, TxGraph, TxNode},
- BlockAnchor, ChainOracle, FullTxOut, ObservedIn, TxIndex,
+ BlockAnchor, ChainOracle, FullTxOut, ObservedAs, TxIndex,
};
/// 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 TxInChain<'a, T, A> {
+pub struct CanonicalTx<'a, T, A> {
/// Where the transaction is observed (in a block or in mempool).
- pub observed_in: ObservedIn<&'a A>,
+ pub observed_as: ObservedAs<&'a A>,
/// The transaction with anchors and last seen timestamp.
pub tx: TxNode<'a, T, A>,
}
&mut self,
outpoint: OutPoint,
txout: &TxOut,
- observation: ObservedIn<A>,
+ observation: ObservedAs<A>,
) -> IndexedAdditions<A, I::Additions> {
let last_height = match &observation {
- ObservedIn::Block(anchor) => self.insert_height_internal(anchor.anchor_block().height),
- ObservedIn::Mempool(_) => None,
+ ObservedAs::Confirmed(anchor) => {
+ self.insert_height_internal(anchor.anchor_block().height)
+ }
+ ObservedAs::Unconfirmed(_) => None,
};
IndexedAdditions {
graph_additions: {
let mut graph_additions = self.graph.insert_txout(outpoint, txout.clone());
graph_additions.append(match observation {
- ObservedIn::Block(anchor) => self.graph.insert_anchor(outpoint.txid, anchor),
- ObservedIn::Mempool(seen_at) => {
+ ObservedAs::Confirmed(anchor) => {
+ self.graph.insert_anchor(outpoint.txid, anchor)
+ }
+ ObservedAs::Unconfirmed(seen_at) => {
self.graph.insert_seen_at(outpoint.txid, seen_at)
}
});
pub fn insert_tx(
&mut self,
tx: &Transaction,
- observation: ObservedIn<A>,
+ observation: ObservedAs<A>,
) -> IndexedAdditions<A, I::Additions> {
let txid = tx.txid();
let last_height = match &observation {
- ObservedIn::Block(anchor) => self.insert_height_internal(anchor.anchor_block().height),
- ObservedIn::Mempool(_) => None,
+ ObservedAs::Confirmed(anchor) => {
+ self.insert_height_internal(anchor.anchor_block().height)
+ }
+ ObservedAs::Unconfirmed(_) => None,
};
IndexedAdditions {
graph_additions: {
let mut graph_additions = self.graph.insert_tx(tx.clone());
graph_additions.append(match observation {
- ObservedIn::Block(anchor) => self.graph.insert_anchor(txid, anchor),
- ObservedIn::Mempool(seen_at) => self.graph.insert_seen_at(txid, seen_at),
+ ObservedAs::Confirmed(anchor) => self.graph.insert_anchor(txid, anchor),
+ ObservedAs::Unconfirmed(seen_at) => self.graph.insert_seen_at(txid, seen_at),
});
graph_additions
},
pub fn filter_and_insert_txs<'t, T>(
&mut self,
txs: T,
- observation: ObservedIn<A>,
+ observation: ObservedAs<A>,
) -> IndexedAdditions<A, I::Additions>
where
T: Iterator<Item = &'t Transaction>,
pub fn try_list_chain_txs<'a, C>(
&'a self,
chain: C,
- ) -> impl Iterator<Item = Result<TxInChain<'a, Transaction, A>, C::Error>>
+ ) -> impl Iterator<Item = Result<CanonicalTx<'a, Transaction, A>, C::Error>>
where
C: ChainOracle + 'a,
{
.filter_map(move |tx| {
self.graph
.try_get_chain_position(&chain, tx.txid)
- .map(|v| v.map(|observed_in| TxInChain { observed_in, tx }))
+ .map(|v| {
+ v.map(|observed_in| CanonicalTx {
+ observed_as: observed_in,
+ tx,
+ })
+ })
.transpose()
})
}
pub fn list_chain_txs<'a, C>(
&'a self,
chain: C,
- ) -> impl Iterator<Item = TxInChain<'a, Transaction, A>>
+ ) -> impl Iterator<Item = CanonicalTx<'a, Transaction, A>>
where
C: ChainOracle<Error = Infallible> + 'a,
{
pub fn try_list_chain_txouts<'a, C>(
&'a self,
chain: C,
- ) -> impl Iterator<Item = Result<FullTxOut<ObservedIn<A>>, C::Error>> + 'a
+ ) -> impl Iterator<Item = Result<FullTxOut<ObservedAs<A>>, C::Error>> + 'a
where
C: ChainOracle + 'a,
- ObservedIn<A>: ChainPosition,
{
self.graph
.all_txouts()
let is_on_coinbase = graph_tx.is_coin_base();
let chain_position = match self.graph.try_get_chain_position(&chain, op.txid) {
- Ok(Some(observed_at)) => observed_at.into_owned(),
+ 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, op) {
- Ok(Some((obs, txid))) => Some((obs.into_owned(), txid)),
+ Ok(Some((obs, txid))) => Some((obs.cloned(), txid)),
Ok(None) => None,
Err(err) => return Some(Err(err)),
};
pub fn list_chain_txouts<'a, C>(
&'a self,
chain: C,
- ) -> impl Iterator<Item = FullTxOut<ObservedIn<A>>> + 'a
+ ) -> impl Iterator<Item = FullTxOut<ObservedAs<A>>> + 'a
where
C: ChainOracle<Error = Infallible> + 'a,
- ObservedIn<A>: ChainPosition,
{
self.try_list_chain_txouts(chain)
.map(|r| r.expect("error in infallible"))
pub fn try_list_chain_utxos<'a, C>(
&'a self,
chain: C,
- ) -> impl Iterator<Item = Result<FullTxOut<ObservedIn<A>>, C::Error>> + 'a
+ ) -> impl Iterator<Item = Result<FullTxOut<ObservedAs<A>>, C::Error>> + 'a
where
C: ChainOracle + 'a,
- ObservedIn<A>: ChainPosition,
{
self.try_list_chain_txouts(chain)
.filter(|r| !matches!(r, Ok(txo) if txo.spent_by.is_none()))
pub fn list_chain_utxos<'a, C>(
&'a self,
chain: C,
- ) -> impl Iterator<Item = FullTxOut<ObservedIn<A>>> + 'a
+ ) -> impl Iterator<Item = FullTxOut<ObservedAs<A>>> + 'a
where
C: ChainOracle<Error = Infallible> + 'a,
- ObservedIn<A>: ChainPosition,
{
self.try_list_chain_utxos(chain)
.map(|r| r.expect("error is infallible"))
) -> Result<Balance, C::Error>
where
C: ChainOracle,
- ObservedIn<A>: ChainPosition + Clone,
F: FnMut(&Script) -> bool,
{
let mut immature = 0;
let txout = res?;
match &txout.chain_position {
- ObservedIn::Block(_) => {
+ ObservedAs::Confirmed(_) => {
if txout.is_on_coinbase {
- if txout.is_mature(tip) {
+ if txout.is_observed_as_mature(tip) {
confirmed += txout.txout.value;
} else {
immature += txout.txout.value;
}
}
}
- ObservedIn::Mempool(_) => {
+ ObservedAs::Unconfirmed(_) => {
if should_trust(&txout.txout.script_pubkey) {
trusted_pending += txout.txout.value;
} else {
pub fn balance<C, F>(&self, chain: C, tip: u32, should_trust: F) -> Balance
where
C: ChainOracle<Error = Infallible>,
- ObservedIn<A>: ChainPosition + Clone,
F: FnMut(&Script) -> bool,
{
self.try_balance(chain, tip, should_trust)
pub fn try_balance_at<C>(&self, chain: C, height: u32) -> Result<u64, C::Error>
where
C: ChainOracle,
- ObservedIn<A>: ChainPosition + Clone,
{
let mut sum = 0;
for txo_res in self.try_list_chain_txouts(chain) {
let txo = txo_res?;
- if txo.is_spendable_at(height) {
+ if txo.is_observed_as_spendable(height) {
sum += txo.txout.value;
}
}
pub fn balance_at<C>(&self, chain: C, height: u32) -> u64
where
C: ChainOracle<Error = Infallible>,
- ObservedIn<A>: ChainPosition + Clone,
{
self.try_balance_at(chain, height)
.expect("error is infallible")
/// Updates [`LocalChain`] with an update [`LocalChain`].
///
- /// This is equivilant to calling [`determine_changeset`] and [`apply_changeset`] in sequence.
+ /// This is equivalent to calling [`determine_changeset`] and [`apply_changeset`] in sequence.
///
/// [`determine_changeset`]: Self::determine_changeset
/// [`apply_changeset`]: Self::apply_changeset
#[cfg(feature = "std")]
impl<P: core::fmt::Debug> std::error::Error for UpdateError<P> {}
-impl<P: ChainPosition> ChainOracle for SparseChain<P> {
+impl<P> ChainOracle for SparseChain<P> {
type Error = Infallible;
fn get_tip_in_best_chain(&self) -> Result<Option<BlockId>, Self::Error> {
}
}
-impl<P: ChainPosition> SparseChain<P> {
+impl<P> SparseChain<P> {
/// Creates a new chain from a list of block hashes and heights. The caller must guarantee they
/// are in the same chain.
pub fn from_checkpoints<C>(checkpoints: C) -> Self
.map(|&hash| BlockId { height, hash })
}
- /// Return the [`ChainPosition`] of a `txid`.
- ///
- /// This returns [`None`] if the transaction does not exist.
- pub fn tx_position(&self, txid: Txid) -> Option<&P> {
- self.txid_to_pos.get(&txid)
- }
-
/// Return a [`BTreeMap`] of all checkpoints (block hashes by height).
pub fn checkpoints(&self) -> &BTreeMap<u32, BlockHash> {
&self.checkpoints
.map(|(&height, &hash)| BlockId { height, hash })
}
+ /// Returns the value set as the checkpoint limit.
+ ///
+ /// Refer to [`set_checkpoint_limit`].
+ ///
+ /// [`set_checkpoint_limit`]: Self::set_checkpoint_limit
+ pub fn checkpoint_limit(&self) -> Option<usize> {
+ self.checkpoint_limit
+ }
+
+ /// Set the checkpoint limit.
+ ///
+ /// The checkpoint limit restricts the number of checkpoints that can be stored in [`Self`].
+ /// Oldest checkpoints are pruned first.
+ pub fn set_checkpoint_limit(&mut self, limit: Option<usize>) {
+ self.checkpoint_limit = limit;
+ self.prune_checkpoints();
+ }
+
+ fn prune_checkpoints(&mut self) -> Option<BTreeMap<u32, BlockHash>> {
+ let limit = self.checkpoint_limit?;
+
+ // find the last height to be pruned
+ let last_height = *self.checkpoints.keys().rev().nth(limit)?;
+ // first height to be kept
+ let keep_height = last_height + 1;
+
+ let mut split = self.checkpoints.split_off(&keep_height);
+ core::mem::swap(&mut self.checkpoints, &mut split);
+
+ Some(split)
+ }
+}
+
+impl<P: ChainPosition> SparseChain<P> {
+ /// Return the [`ChainPosition`] of a `txid`.
+ ///
+ /// This returns [`None`] if the transaction does not exist.
+ pub fn tx_position(&self, txid: Txid) -> Option<&P> {
+ self.txid_to_pos.get(&txid)
+ }
+
/// Preview changes of updating [`Self`] with another chain that connects to it.
///
/// If the `update` wishes to introduce confirmed transactions, it must contain a checkpoint
})
}
- /// Returns the value set as the checkpoint limit.
- ///
- /// Refer to [`set_checkpoint_limit`].
- ///
- /// [`set_checkpoint_limit`]: Self::set_checkpoint_limit
- pub fn checkpoint_limit(&self) -> Option<usize> {
- self.checkpoint_limit
- }
-
- /// Set the checkpoint limit.
- ///
- /// The checkpoint limit restricts the number of checkpoints that can be stored in [`Self`].
- /// Oldest checkpoints are pruned first.
- pub fn set_checkpoint_limit(&mut self, limit: Option<usize>) {
- self.checkpoint_limit = limit;
- self.prune_checkpoints();
- }
-
/// Return [`Txid`]s that would be added to the sparse chain if this `changeset` was applied.
pub fn changeset_additions<'a>(
&'a self,
.map(|(&txid, _)| txid)
}
- fn prune_checkpoints(&mut self) -> Option<BTreeMap<u32, BlockHash>> {
- let limit = self.checkpoint_limit?;
-
- // find the last height to be pruned
- let last_height = *self.checkpoints.keys().rev().nth(limit)?;
- // first height to be kept
- let keep_height = last_height + 1;
-
- let mut split = self.checkpoints.split_off(&keep_height);
- core::mem::swap(&mut self.checkpoints, &mut split);
-
- Some(split)
- }
-
/// Finds the transaction in the chain that spends `outpoint`.
///
/// [`TxGraph`] is used to provide the spend relationships.
//! assert!(additions.is_empty());
//! ```
-use crate::{collections::*, BlockAnchor, ChainOracle, ForEachTxOut, ObservedIn};
+use crate::{collections::*, BlockAnchor, ChainOracle, ForEachTxOut, ObservedAs};
use alloc::vec::Vec;
use bitcoin::{OutPoint, Transaction, TxOut, Txid};
use core::{
}
}
-// pub type InChainTx<'a, T, A> = (ObservedIn<&'a A>, TxInGraph<'a, T, A>);
-// pub type InChainTxOut<'a, I, A> = (&'a I, FullTxOut<ObservedIn<&'a A>>);
-
-/// An outward-facing view of a transaction node that resides in a [`TxGraph`].
+/// An outward-facing representation of a (transaction) node in the [`TxGraph`].
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct TxNode<'a, T, A> {
/// Txid of the transaction.
&self,
chain: C,
txid: Txid,
- ) -> Result<Option<ObservedIn<&A>>, C::Error>
+ ) -> Result<Option<ObservedAs<&A>>, C::Error>
where
C: ChainOracle,
{
for anchor in anchors {
if chain.is_block_in_best_chain(anchor.anchor_block())? {
- return Ok(Some(ObservedIn::Block(anchor)));
+ return Ok(Some(ObservedAs::Confirmed(anchor)));
}
}
}
}
- Ok(Some(ObservedIn::Mempool(last_seen)))
+ Ok(Some(ObservedAs::Unconfirmed(last_seen)))
}
- pub fn get_chain_position<C>(&self, chain: C, txid: Txid) -> Option<ObservedIn<&A>>
+ pub fn get_chain_position<C>(&self, chain: C, txid: Txid) -> Option<ObservedAs<&A>>
where
C: ChainOracle<Error = Infallible>,
{
&self,
chain: C,
outpoint: OutPoint,
- ) -> Result<Option<(ObservedIn<&A>, Txid)>, C::Error>
+ ) -> Result<Option<(ObservedAs<&A>, Txid)>, C::Error>
where
C: ChainOracle,
{
Ok(None)
}
- pub fn get_chain_spend<C>(&self, chain: C, outpoint: OutPoint) -> Option<(ObservedIn<&A>, Txid)>
+ pub fn get_chain_spend<C>(&self, chain: C, outpoint: OutPoint) -> Option<(ObservedAs<&A>, Txid)>
where
C: ChainOracle<Error = Infallible>,
{