diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index 64f55fc46..ab03d58e1 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -1,12 +1,12 @@ use bitcoin::{hashes::Hash, BlockHash, OutPoint, TxOut, Txid}; -use crate::{Anchor, BlockTime, COINBASE_MATURITY}; +use crate::{BlockTime, COINBASE_MATURITY}; /// Represents the observed position of some chain data. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)] pub enum ChainPosition { /// The chain data is seen as confirmed, and in anchored by `Anchor`. - Confirmed((Anchor, AM)), + Confirmed(BlockId, AM), /// The chain data is not confirmed and last seen in the mempool at this timestamp. Unconfirmed(u64), } @@ -14,7 +14,7 @@ pub enum ChainPosition { impl ChainPosition { /// Returns whether [`ChainPosition`] is confirmed or not. pub fn is_confirmed(&self) -> bool { - matches!(self, Self::Confirmed(_)) + matches!(self, Self::Confirmed(_, _)) } } @@ -22,7 +22,9 @@ impl ChainPosition { /// Maps a [`ChainPosition`] into a [`ChainPosition`] by cloning the contents. pub fn cloned(self) -> ChainPosition { match self { - ChainPosition::Confirmed(a) => ChainPosition::Confirmed(a), + ChainPosition::Confirmed(bid, anchor_meta) => { + ChainPosition::Confirmed(bid, anchor_meta.clone()) + } ChainPosition::Unconfirmed(last_seen) => ChainPosition::Unconfirmed(last_seen), } } @@ -65,8 +67,8 @@ impl ConfirmationTime { impl From> for ConfirmationTime { fn from(observed_as: ChainPosition) -> Self { match observed_as { - ChainPosition::Confirmed(((_txid, blockid), anchor_meta)) => Self::Confirmed { - height: blockid.height, + ChainPosition::Confirmed(bid, anchor_meta) => Self::Confirmed { + height: bid.height, time: *anchor_meta.as_ref() as u64, }, ChainPosition::Unconfirmed(last_seen) => Self::Unconfirmed { last_seen }, @@ -147,7 +149,7 @@ impl FullTxOut { pub fn is_mature(&self, tip: u32) -> bool { if self.is_on_coinbase { let tx_height = match &self.chain_position { - ChainPosition::Confirmed(((_, blockid), _)) => blockid.height, + ChainPosition::Confirmed(bid, _) => bid.height, ChainPosition::Unconfirmed(_) => { debug_assert!(false, "coinbase tx can never be unconfirmed"); return false; @@ -177,7 +179,7 @@ impl FullTxOut { } let confirmation_height = match &self.chain_position { - ChainPosition::Confirmed(((_, blockid), _)) => blockid.height, + ChainPosition::Confirmed(bid, _) => bid.height, ChainPosition::Unconfirmed(_) => return false, }; if confirmation_height > tip { @@ -185,8 +187,8 @@ impl FullTxOut { } // if the spending tx is confirmed within tip height, the txout is no longer spendable - if let Some((ChainPosition::Confirmed(((_, spending_blockid), _)), _)) = &self.spent_by { - if spending_blockid.height <= tip { + if let Some((ChainPosition::Confirmed(spending_bid, _), _)) = &self.spent_by { + if spending_bid.height <= tip { return false; } } diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 94a1f148b..b2a6d88bd 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -110,9 +110,8 @@ use core::{ #[derive(Clone, Debug, PartialEq)] pub struct TxGraph { // all transactions that the graph is aware of in format: `(tx_node, tx_anchors)` - txs: HashMap)>, + txs: HashMap)>, spends: BTreeMap>, - anchors: BTreeMap, last_seen: HashMap, // This atrocity exists so that `TxGraph::outspends()` can return a reference. @@ -125,7 +124,6 @@ impl Default for TxGraph { Self { txs: Default::default(), spends: Default::default(), - anchors: Default::default(), last_seen: Default::default(), empty_outspends: Default::default(), } @@ -140,7 +138,7 @@ pub struct TxNode<'a, T, AM> { /// A partial or full representation of the transaction. pub tx: T, /// The blocks that the transaction is "anchored" in. - pub anchors: &'a BTreeMap, + pub anchors: &'a BTreeMap, /// The last-seen unix timestamp of the transaction as unconfirmed. pub last_seen_unconfirmed: Option, } @@ -470,8 +468,12 @@ impl TxGraph { } /// Get all transaction anchors known by [`TxGraph`]. - pub fn all_anchors(&self) -> &BTreeMap { - &self.anchors + pub fn all_anchors(&self) -> impl Iterator { + self.txs.iter().flat_map(|(&txid, (_, blocks))| { + blocks + .iter() + .map(move |(&bid, anchor_data)| ((txid, bid), anchor_data)) + }) } /// Whether the graph has any transactions or outputs in it. @@ -547,7 +549,11 @@ impl TxGraph { /// `anchor`. pub fn insert_anchor(&mut self, anchor: Anchor, anchor_meta: AM) -> ChangeSet { let mut update = Self::default(); - update.anchors.insert(anchor, anchor_meta); + let (txid, bid) = anchor; + update.txs.insert( + txid, + (TxNodeInternal::default(), [(bid, anchor_meta)].into()), + ); self.apply_update(update) } @@ -684,15 +690,12 @@ impl TxGraph { } } - for ((txid, blockid), anchor_meta) in changeset.anchors { - if self - .anchors - .insert((txid, blockid), anchor_meta.clone()) - .is_none() - { - let (_, anchors) = self.txs.entry(txid).or_default(); - anchors.insert((txid, blockid), anchor_meta); - } + for ((txid, bid), anchor_data) in changeset.anchors { + let (_, blocks) = self + .txs + .entry(txid) + .or_insert((TxNodeInternal::default(), BTreeMap::new())); + blocks.insert(bid, anchor_data); } for (txid, new_last_seen) in changeset.last_seen { @@ -740,6 +743,12 @@ impl TxGraph { } } + changeset.anchors = update + .all_anchors() + .filter(|&(anchor, anchor_data)| self.anchor_data(anchor) != Some(anchor_data)) + .map(|(anchor, anchor_data)| (anchor, anchor_data.clone())) + .collect(); + for (txid, update_last_seen) in update.last_seen { let prev_last_seen = self.last_seen.get(&txid).copied(); if Some(update_last_seen) > prev_last_seen { @@ -747,15 +756,15 @@ impl TxGraph { } } - changeset.anchors = update - .anchors - .iter() - .filter(|(k, _)| !self.anchors.contains_key(k)) - .map(|(k, v)| (*k, v.clone())) - .collect::>(); - changeset } + + /// Get data for a given `anchor`. + pub fn anchor_data(&self, anchor: Anchor) -> Option<&AM> { + let (txid, bid) = anchor; + let (_, blocks) = self.txs.get(&txid)?; + blocks.get(&bid) + } } impl TxGraph { @@ -788,19 +797,14 @@ impl TxGraph { chain_tip: BlockId, txid: Txid, ) -> Result>, C::Error> { - let (tx_node, anchors) = match self.txs.get(&txid) { + let (tx_node, blocks) = match self.txs.get(&txid) { Some(v) => v, None => return Ok(None), }; - for (anchor, anchor_meta) in anchors { - match chain.is_block_in_chain(anchor.1, chain_tip)? { - Some(true) => { - return Ok(Some(ChainPosition::Confirmed(( - *anchor, - anchor_meta.clone(), - )))) - } + for (&bid, anchor_meta) in blocks { + match chain.is_block_in_chain(bid, chain_tip)? { + Some(true) => return Ok(Some(ChainPosition::Confirmed(bid, anchor_meta.clone()))), _ => continue, } } @@ -843,8 +847,8 @@ impl TxGraph { let tx_node = self.get_tx_node(ancestor_tx.as_ref().compute_txid())?; // We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in // the best chain) - for (_, block) in tx_node.anchors.keys() { - match chain.is_block_in_chain(*block, chain_tip) { + for bid in tx_node.anchors.keys() { + match chain.is_block_in_chain(*bid, chain_tip) { Ok(Some(true)) => return None, Err(e) => return Some(Err(e)), _ => continue, @@ -863,8 +867,8 @@ impl TxGraph { let tx_node = self.get_tx_node(descendant_txid)?; // We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in // the best chain) - for (_, block) in tx_node.anchors.keys() { - match chain.is_block_in_chain(*block, chain_tip) { + for bid in tx_node.anchors.keys() { + match chain.is_block_in_chain(*bid, chain_tip) { Ok(Some(true)) => return None, Err(e) => return Some(Err(e)), _ => continue, @@ -890,8 +894,8 @@ impl TxGraph { // If a conflicting tx is in the best chain, or has `last_seen` higher than this ancestor, then // this tx cannot exist in the best chain for conflicting_tx in conflicting_txs { - for (_, block) in conflicting_tx.anchors.keys() { - if chain.is_block_in_chain(*block, chain_tip)? == Some(true) { + for bid in conflicting_tx.anchors.keys() { + if chain.is_block_in_chain(*bid, chain_tip)? == Some(true) { return Ok(None); } } @@ -1174,7 +1178,7 @@ impl TxGraph { let (spk_i, txout) = res?; match &txout.chain_position { - ChainPosition::Confirmed(_) => { + ChainPosition::Confirmed(_, _) => { if txout.is_confirmed_and_spendable(chain_tip.height) { confirmed += txout.txout.value; } else if !txout.is_mature(chain_tip.height) { diff --git a/crates/wallet/src/wallet/export.rs b/crates/wallet/src/wallet/export.rs index 69c00bf15..8a27a9f09 100644 --- a/crates/wallet/src/wallet/export.rs +++ b/crates/wallet/src/wallet/export.rs @@ -128,7 +128,7 @@ impl FullyNodedExport { let blockheight = if include_blockheight { wallet.transactions().next().map_or(0, |canonical_tx| { match canonical_tx.chain_position { - bdk_chain::ChainPosition::Confirmed(((_, blockid), _)) => blockid.height, + bdk_chain::ChainPosition::Confirmed(bid, _) => bid.height, bdk_chain::ChainPosition::Unconfirmed(_) => 0, } }) diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index b9ae07ef6..64ad91db9 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -1547,7 +1547,7 @@ impl Wallet { let pos = graph .get_chain_position(&self.chain, chain_tip, txid) .ok_or(BuildFeeBumpError::TransactionNotFound(txid))?; - if let ChainPosition::Confirmed(_) = pos { + if let ChainPosition::Confirmed(_, _) = pos { return Err(BuildFeeBumpError::TransactionConfirmed(txid)); } @@ -1799,7 +1799,7 @@ impl Wallet { .graph() .get_chain_position(&self.chain, chain_tip, input.previous_output.txid) .map(|chain_position| match chain_position { - ChainPosition::Confirmed(((_, blockid), _)) => blockid.height, + ChainPosition::Confirmed(bid, _) => bid.height, ChainPosition::Unconfirmed(_) => u32::MAX, }); let current_height = sign_options