diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index 6c1c2c3a..85f9107c 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -6,49 +6,21 @@ use crate::{ }; /// 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 { - /// The chain data is seen in a block identified by `A`. - Block(A), +pub enum ObservedAs { + /// 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 ObservedIn<&A> { - pub fn into_owned(self) -> ObservedIn { +impl ObservedAs<&A> { + pub fn cloned(self) -> ObservedAs { match self { - ObservedIn::Block(a) => ObservedIn::Block(a.clone()), - ObservedIn::Mempool(last_seen) => ObservedIn::Mempool(last_seen), - } - } -} - -impl ChainPosition for ObservedIn { - 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), } } } @@ -217,20 +189,20 @@ impl From<(&u32, &BlockHash)> for BlockId { /// A `TxOut` with as much data as we can retrieve about it #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct FullTxOut { +pub struct FullTxOut

{ /// 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 FullTxOut { +impl FullTxOut

{ /// 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 @@ -269,15 +241,63 @@ impl FullTxOut { } } -impl FullTxOut> { - pub fn into_owned(self) -> FullTxOut> { - 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 FullTxOut> { + /// Whether the `txout` is considered mature. + /// + /// This is the alternative version of [`is_mature`] which depends on `chain_position` being a + /// [`ObservedAs`] 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`] 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 } } diff --git a/crates/chain/src/chain_graph.rs b/crates/chain/src/chain_graph.rs index 8c954f8d..0e3e3439 100644 --- a/crates/chain/src/chain_graph.rs +++ b/crates/chain/src/chain_graph.rs @@ -151,7 +151,7 @@ where 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 => { @@ -212,8 +212,8 @@ where /// 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 diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index a3996c13..e2d71af1 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -4,16 +4,15 @@ use bitcoin::{OutPoint, Script, Transaction, TxOut}; 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>, } @@ -140,19 +139,23 @@ impl IndexedTxGraph { &mut self, outpoint: OutPoint, txout: &TxOut, - observation: ObservedIn, + observation: ObservedAs, ) -> IndexedAdditions { 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) } }); @@ -166,21 +169,23 @@ impl IndexedTxGraph { pub fn insert_tx( &mut self, tx: &Transaction, - observation: ObservedIn, + observation: ObservedAs, ) -> IndexedAdditions { 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 }, @@ -192,7 +197,7 @@ impl IndexedTxGraph { pub fn filter_and_insert_txs<'t, T>( &mut self, txs: T, - observation: ObservedIn, + observation: ObservedAs, ) -> IndexedAdditions where T: Iterator, @@ -220,7 +225,7 @@ impl IndexedTxGraph { pub fn try_list_chain_txs<'a, C>( &'a self, chain: C, - ) -> impl Iterator, C::Error>> + ) -> impl Iterator, C::Error>> where C: ChainOracle + 'a, { @@ -230,7 +235,12 @@ impl IndexedTxGraph { .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() }) } @@ -238,7 +248,7 @@ impl IndexedTxGraph { pub fn list_chain_txs<'a, C>( &'a self, chain: C, - ) -> impl Iterator> + ) -> impl Iterator> where C: ChainOracle + 'a, { @@ -249,10 +259,9 @@ impl IndexedTxGraph { pub fn try_list_chain_txouts<'a, C>( &'a self, chain: C, - ) -> impl Iterator>, C::Error>> + 'a + ) -> impl Iterator>, C::Error>> + 'a where C: ChainOracle + 'a, - ObservedIn: ChainPosition, { self.graph .all_txouts() @@ -263,13 +272,13 @@ impl IndexedTxGraph { 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)), }; @@ -289,10 +298,9 @@ impl IndexedTxGraph { pub fn list_chain_txouts<'a, C>( &'a self, chain: C, - ) -> impl Iterator>> + 'a + ) -> impl Iterator>> + 'a where C: ChainOracle + 'a, - ObservedIn: ChainPosition, { self.try_list_chain_txouts(chain) .map(|r| r.expect("error in infallible")) @@ -302,10 +310,9 @@ impl IndexedTxGraph { pub fn try_list_chain_utxos<'a, C>( &'a self, chain: C, - ) -> impl Iterator>, C::Error>> + 'a + ) -> impl Iterator>, C::Error>> + 'a where C: ChainOracle + 'a, - ObservedIn: ChainPosition, { self.try_list_chain_txouts(chain) .filter(|r| !matches!(r, Ok(txo) if txo.spent_by.is_none())) @@ -314,10 +321,9 @@ impl IndexedTxGraph { pub fn list_chain_utxos<'a, C>( &'a self, chain: C, - ) -> impl Iterator>> + 'a + ) -> impl Iterator>> + 'a where C: ChainOracle + 'a, - ObservedIn: ChainPosition, { self.try_list_chain_utxos(chain) .map(|r| r.expect("error is infallible")) @@ -331,7 +337,6 @@ impl IndexedTxGraph { ) -> Result where C: ChainOracle, - ObservedIn: ChainPosition + Clone, F: FnMut(&Script) -> bool, { let mut immature = 0; @@ -343,16 +348,16 @@ impl IndexedTxGraph { 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 { @@ -373,7 +378,6 @@ impl IndexedTxGraph { pub fn balance(&self, chain: C, tip: u32, should_trust: F) -> Balance where C: ChainOracle, - ObservedIn: ChainPosition + Clone, F: FnMut(&Script) -> bool, { self.try_balance(chain, tip, should_trust) @@ -383,12 +387,11 @@ impl IndexedTxGraph { pub fn try_balance_at(&self, chain: C, height: u32) -> Result where C: ChainOracle, - ObservedIn: 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; } } @@ -398,7 +401,6 @@ impl IndexedTxGraph { pub fn balance_at(&self, chain: C, height: u32) -> u64 where C: ChainOracle, - ObservedIn: ChainPosition + Clone, { self.try_balance_at(chain, height) .expect("error is infallible") diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index a1ca921b..fb7d008d 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -123,7 +123,7 @@ impl LocalChain { /// 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 diff --git a/crates/chain/src/sparse_chain.rs b/crates/chain/src/sparse_chain.rs index 7f0b67e5..b615f4aa 100644 --- a/crates/chain/src/sparse_chain.rs +++ b/crates/chain/src/sparse_chain.rs @@ -457,7 +457,7 @@ impl core::fmt::Display for UpdateError

{ #[cfg(feature = "std")] impl std::error::Error for UpdateError

{} -impl ChainOracle for SparseChain

{ +impl

ChainOracle for SparseChain

{ type Error = Infallible; fn get_tip_in_best_chain(&self) -> Result, Self::Error> { @@ -473,7 +473,7 @@ impl ChainOracle for SparseChain

{ } } -impl SparseChain

{ +impl

SparseChain

{ /// 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(checkpoints: C) -> Self @@ -504,13 +504,6 @@ impl SparseChain

{ .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 { &self.checkpoints @@ -526,6 +519,47 @@ impl SparseChain

{ .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 { + 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) { + self.checkpoint_limit = limit; + self.prune_checkpoints(); + } + + fn prune_checkpoints(&mut self) -> Option> { + 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 SparseChain

{ + /// 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 @@ -936,24 +970,6 @@ impl SparseChain

{ }) } - /// 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 { - 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) { - 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, @@ -969,20 +985,6 @@ impl SparseChain

{ .map(|(&txid, _)| txid) } - fn prune_checkpoints(&mut self) -> Option> { - 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. diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 893060ae..620a2dc3 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -55,7 +55,7 @@ //! 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::{ @@ -91,10 +91,7 @@ impl Default for TxGraph { } } -// pub type InChainTx<'a, T, A> = (ObservedIn<&'a A>, TxInGraph<'a, T, A>); -// pub type InChainTxOut<'a, I, A> = (&'a I, FullTxOut>); - -/// 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. @@ -601,7 +598,7 @@ impl TxGraph { &self, chain: C, txid: Txid, - ) -> Result>, C::Error> + ) -> Result>, C::Error> where C: ChainOracle, { @@ -614,7 +611,7 @@ impl TxGraph { 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))); } } @@ -643,10 +640,10 @@ impl TxGraph { } } - Ok(Some(ObservedIn::Mempool(last_seen))) + Ok(Some(ObservedAs::Unconfirmed(last_seen))) } - pub fn get_chain_position(&self, chain: C, txid: Txid) -> Option> + pub fn get_chain_position(&self, chain: C, txid: Txid) -> Option> where C: ChainOracle, { @@ -658,7 +655,7 @@ impl TxGraph { &self, chain: C, outpoint: OutPoint, - ) -> Result, Txid)>, C::Error> + ) -> Result, Txid)>, C::Error> where C: ChainOracle, { @@ -678,7 +675,7 @@ impl TxGraph { Ok(None) } - pub fn get_chain_spend(&self, chain: C, outpoint: OutPoint) -> Option<(ObservedIn<&A>, Txid)> + pub fn get_chain_spend(&self, chain: C, outpoint: OutPoint) -> Option<(ObservedAs<&A>, Txid)> where C: ChainOracle, {