From 71a3e0e335f0d29249224558118be9edada82be6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 22 Aug 2024 08:39:08 +0000 Subject: [PATCH 1/6] feat(chain): get rid of `TxGraph::determine_changeset` Contain most of the insertion logic in `.insert_{}` methods, thus simplifying `.apply_{}` methods. We can also get rid of `.determine_changeset`. --- crates/chain/src/tx_graph.rs | 224 +++++++++++++--------------- crates/chain/tests/test_tx_graph.rs | 55 ++----- 2 files changed, 112 insertions(+), 167 deletions(-) diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 0eab93867..9ab1268b3 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -512,28 +512,66 @@ impl TxGraph { /// /// [`apply_changeset`]: Self::apply_changeset pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet { - let mut update = Self::default(); - update.txs.insert( - outpoint.txid, - ( - TxNodeInternal::Partial([(outpoint.vout, txout)].into()), - BTreeSet::new(), - ), - ); - self.apply_update(update) + let mut changeset = ChangeSet::::default(); + let (tx_node, _) = self.txs.entry(outpoint.txid).or_default(); + match tx_node { + TxNodeInternal::Whole(_) => { + // ignore this txout we have the full one already. + // NOTE: You might think putting a debug_assert! here to check the output being + // replaced was actually correct is a good idea but the tests have already been + // written assuming this never panics. + } + TxNodeInternal::Partial(partial_tx) => { + match partial_tx.insert(outpoint.vout, txout.clone()) { + Some(old_txout) => { + debug_assert_eq!( + txout, old_txout, + "txout of the same outpoint should never change" + ); + } + None => { + changeset.txouts.insert(outpoint, txout); + } + } + } + } + changeset } /// Inserts the given transaction into [`TxGraph`]. /// /// The [`ChangeSet`] returned will be empty if `tx` already exists. pub fn insert_tx>>(&mut self, tx: T) -> ChangeSet { - let tx = tx.into(); - let mut update = Self::default(); - update.txs.insert( - tx.compute_txid(), - (TxNodeInternal::Whole(tx), BTreeSet::new()), - ); - self.apply_update(update) + let tx: Arc = tx.into(); + let txid = tx.compute_txid(); + let mut changeset = ChangeSet::::default(); + + let (tx_node, _) = self.txs.entry(txid).or_default(); + match tx_node { + TxNodeInternal::Whole(existing_tx) => { + debug_assert_eq!( + existing_tx.as_ref(), + tx.as_ref(), + "tx of same txid should never change" + ); + } + partial_tx => { + for txin in &tx.input { + // this means the tx is coinbase so there is no previous output + if txin.previous_output.is_null() { + continue; + } + self.spends + .entry(txin.previous_output) + .or_default() + .insert(txid); + } + *partial_tx = TxNodeInternal::Whole(tx.clone()); + changeset.txs.insert(tx); + } + } + + changeset } /// Batch insert unconfirmed transactions. @@ -558,9 +596,17 @@ impl TxGraph { /// The [`ChangeSet`] returned will be empty if graph already knows that `txid` exists in /// `anchor`. pub fn insert_anchor(&mut self, txid: Txid, anchor: A) -> ChangeSet { - let mut update = Self::default(); - update.anchors.insert((anchor, txid)); - self.apply_update(update) + let mut changeset = ChangeSet::::default(); + if self.anchors.insert((anchor.clone(), txid)) { + let (_tx_node, anchors) = self.txs.entry(txid).or_default(); + let _inserted = anchors.insert(anchor.clone()); + debug_assert!( + _inserted, + "anchors in `.anchors` and `.txs` should be consistent" + ); + changeset.anchors.insert((anchor, txid)); + } + changeset } /// Inserts the given `seen_at` for `txid` into [`TxGraph`]. @@ -571,9 +617,13 @@ impl TxGraph { /// /// [`update_last_seen_unconfirmed`]: Self::update_last_seen_unconfirmed pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet { - let mut update = Self::default(); - update.last_seen.insert(txid, seen_at); - self.apply_update(update) + let mut changeset = ChangeSet::::default(); + let last_seen = self.last_seen.entry(txid).or_default(); + if seen_at > *last_seen { + *last_seen = seen_at; + changeset.last_seen.insert(txid, seen_at); + } + changeset } /// Update the last seen time for all unconfirmed transactions. @@ -641,124 +691,50 @@ impl TxGraph { /// The returned [`ChangeSet`] is the set difference between `update` and `self` (transactions that /// exist in `update` but not in `self`). pub fn apply_update(&mut self, update: TxGraph) -> ChangeSet { - let changeset = self.determine_changeset(update); - self.apply_changeset(changeset.clone()); + let mut changeset = ChangeSet::::default(); + for tx_node in update.full_txs() { + changeset.merge(self.insert_tx(tx_node.tx)); + } + for (outpoint, txout) in update.floating_txouts() { + changeset.merge(self.insert_txout(outpoint, txout.clone())); + } + for (anchor, txid) in &update.anchors { + changeset.merge(self.insert_anchor(*txid, anchor.clone())); + } + for (&txid, &last_seen) in &update.last_seen { + changeset.merge(self.insert_seen_at(txid, last_seen)); + } changeset } /// Determines the [`ChangeSet`] between `self` and an empty [`TxGraph`]. pub fn initial_changeset(&self) -> ChangeSet { - Self::default().determine_changeset(self.clone()) + ChangeSet { + txs: self.full_txs().map(|tx_node| tx_node.tx).collect(), + txouts: self + .floating_txouts() + .map(|(op, txout)| (op, txout.clone())) + .collect(), + anchors: self.anchors.clone(), + last_seen: self.last_seen.iter().map(|(&k, &v)| (k, v)).collect(), + } } /// Applies [`ChangeSet`] to [`TxGraph`]. pub fn apply_changeset(&mut self, changeset: ChangeSet) { - for wrapped_tx in changeset.txs { - let tx = wrapped_tx.as_ref(); - let txid = tx.compute_txid(); - - tx.input - .iter() - .map(|txin| txin.previous_output) - // coinbase spends are not to be counted - .filter(|outpoint| !outpoint.is_null()) - // record spend as this tx has spent this outpoint - .for_each(|outpoint| { - self.spends.entry(outpoint).or_default().insert(txid); - }); - - match self.txs.get_mut(&txid) { - Some((tx_node @ TxNodeInternal::Partial(_), _)) => { - *tx_node = TxNodeInternal::Whole(wrapped_tx.clone()); - } - Some((TxNodeInternal::Whole(tx), _)) => { - debug_assert_eq!( - tx.as_ref().compute_txid(), - txid, - "tx should produce txid that is same as key" - ); - } - None => { - self.txs - .insert(txid, (TxNodeInternal::Whole(wrapped_tx), BTreeSet::new())); - } - } + for tx in changeset.txs { + let _ = self.insert_tx(tx); } - for (outpoint, txout) in changeset.txouts { - let tx_entry = self.txs.entry(outpoint.txid).or_default(); - - match tx_entry { - (TxNodeInternal::Whole(_), _) => { /* do nothing since we already have full tx */ } - (TxNodeInternal::Partial(txouts), _) => { - txouts.insert(outpoint.vout, txout); - } - } + let _ = self.insert_txout(outpoint, txout); } - for (anchor, txid) in changeset.anchors { - if self.anchors.insert((anchor.clone(), txid)) { - let (_, anchors) = self.txs.entry(txid).or_default(); - anchors.insert(anchor); - } + let _ = self.insert_anchor(txid, anchor); } - - for (txid, new_last_seen) in changeset.last_seen { - let last_seen = self.last_seen.entry(txid).or_default(); - if new_last_seen > *last_seen { - *last_seen = new_last_seen; - } + for (txid, seen_at) in changeset.last_seen { + let _ = self.insert_seen_at(txid, seen_at); } } - - /// Previews the resultant [`ChangeSet`] when [`Self`] is updated against the `update` graph. - /// - /// The [`ChangeSet`] would be the set difference between `update` and `self` (transactions that - /// exist in `update` but not in `self`). - pub(crate) fn determine_changeset(&self, update: TxGraph) -> ChangeSet { - let mut changeset = ChangeSet::::default(); - - for (&txid, (update_tx_node, _)) in &update.txs { - match (self.txs.get(&txid), update_tx_node) { - (None, TxNodeInternal::Whole(update_tx)) => { - changeset.txs.insert(update_tx.clone()); - } - (None, TxNodeInternal::Partial(update_txos)) => { - changeset.txouts.extend( - update_txos - .iter() - .map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())), - ); - } - (Some((TxNodeInternal::Whole(_), _)), _) => {} - (Some((TxNodeInternal::Partial(_), _)), TxNodeInternal::Whole(update_tx)) => { - changeset.txs.insert(update_tx.clone()); - } - ( - Some((TxNodeInternal::Partial(txos), _)), - TxNodeInternal::Partial(update_txos), - ) => { - changeset.txouts.extend( - update_txos - .iter() - .filter(|(vout, _)| !txos.contains_key(*vout)) - .map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())), - ); - } - } - } - - 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 { - changeset.last_seen.insert(txid, update_last_seen); - } - } - - changeset.anchors = update.anchors.difference(&self.anchors).cloned().collect(); - - changeset - } } impl TxGraph { diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 8ddf7f30a..3ffa82439 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -301,59 +301,28 @@ fn insert_tx_can_retrieve_full_tx_from_graph() { #[test] fn insert_tx_displaces_txouts() { let mut tx_graph = TxGraph::<()>::default(); + let tx = Transaction { version: transaction::Version::ONE, lock_time: absolute::LockTime::ZERO, input: vec![], output: vec![TxOut { value: Amount::from_sat(42_000), - script_pubkey: ScriptBuf::new(), + script_pubkey: ScriptBuf::default(), }], }; + let txid = tx.compute_txid(); + let outpoint = OutPoint::new(txid, 0); + let txout = tx.output.first().unwrap(); - let changeset = tx_graph.insert_txout( - OutPoint { - txid: tx.compute_txid(), - vout: 0, - }, - TxOut { - value: Amount::from_sat(1_337_000), - script_pubkey: ScriptBuf::default(), - }, - ); - + let changeset = tx_graph.insert_txout(outpoint, txout.clone()); assert!(!changeset.is_empty()); - let _ = tx_graph.insert_txout( - OutPoint { - txid: tx.compute_txid(), - vout: 0, - }, - TxOut { - value: Amount::from_sat(1_000_000_000), - script_pubkey: ScriptBuf::new(), - }, - ); - - let _changeset = tx_graph.insert_tx(tx.clone()); - - assert_eq!( - tx_graph - .get_txout(OutPoint { - txid: tx.compute_txid(), - vout: 0 - }) - .unwrap() - .value, - Amount::from_sat(42_000) - ); - assert_eq!( - tx_graph.get_txout(OutPoint { - txid: tx.compute_txid(), - vout: 1 - }), - None - ); + let changeset = tx_graph.insert_tx(tx.clone()); + assert_eq!(changeset.txs.len(), 1); + assert!(changeset.txouts.is_empty()); + assert!(tx_graph.get_tx(txid).is_some()); + assert_eq!(tx_graph.get_txout(outpoint), Some(txout)); } #[test] @@ -385,7 +354,7 @@ fn insert_txout_does_not_displace_tx() { let _ = tx_graph.insert_txout( OutPoint { txid: tx.compute_txid(), - vout: 0, + vout: 1, }, TxOut { value: Amount::from_sat(1_000_000_000), From 5150801dc5a38cce1571f916499622db755c5ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 23 Aug 2024 06:13:42 +0000 Subject: [PATCH 2/6] feat!: introduce `tx_graph::Update` Instead of updating a `TxGraph` with a `TxGraph`, we introduce a dedicated data object (`tx_graph::Update`). This brings us closer to completing #1543. Co-authored-by: Wei Chen --- crates/chain/src/indexed_tx_graph.rs | 11 +- crates/chain/src/spk_client.rs | 10 +- crates/chain/src/tx_graph.rs | 99 ++++++++-- crates/chain/tests/test_tx_graph.rs | 88 +++------ crates/electrum/src/bdk_electrum_client.rs | 57 +++--- crates/electrum/tests/test_electrum.rs | 61 ++++--- crates/esplora/src/async_ext.rs | 169 ++++++++++-------- crates/esplora/src/blocking_ext.rs | 130 +++++++------- crates/esplora/src/lib.rs | 10 +- crates/esplora/tests/async_ext.rs | 46 +++-- crates/esplora/tests/blocking_ext.rs | 47 +++-- crates/wallet/src/wallet/export.rs | 9 +- crates/wallet/src/wallet/mod.rs | 14 +- crates/wallet/tests/common.rs | 9 +- crates/wallet/tests/wallet.rs | 9 +- example-crates/example_electrum/src/main.rs | 2 +- example-crates/example_esplora/src/main.rs | 4 +- example-crates/wallet_electrum/src/main.rs | 2 +- .../wallet_esplora_async/src/main.rs | 2 +- .../wallet_esplora_blocking/src/main.rs | 2 +- 20 files changed, 439 insertions(+), 342 deletions(-) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 92a08a9ef..d24b1b307 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -91,13 +91,10 @@ where /// Apply an `update` directly. /// /// `update` is a [`TxGraph`] and the resultant changes is returned as [`ChangeSet`]. - pub fn apply_update(&mut self, update: TxGraph) -> ChangeSet { - let graph = self.graph.apply_update(update); - let indexer = self.index_tx_graph_changeset(&graph); - ChangeSet { - tx_graph: graph, - indexer, - } + pub fn apply_update(&mut self, update: tx_graph::Update) -> ChangeSet { + let tx_graph = self.graph.apply_update(update); + let indexer = self.index_tx_graph_changeset(&tx_graph); + ChangeSet { tx_graph, indexer } } /// Insert a floating `txout` of given `outpoint`. diff --git a/crates/chain/src/spk_client.rs b/crates/chain/src/spk_client.rs index 567a8f0a9..e31b431dd 100644 --- a/crates/chain/src/spk_client.rs +++ b/crates/chain/src/spk_client.rs @@ -3,7 +3,7 @@ use crate::{ alloc::{boxed::Box, collections::VecDeque, vec::Vec}, collections::BTreeMap, local_chain::CheckPoint, - ConfirmationBlockTime, Indexed, TxGraph, + ConfirmationBlockTime, Indexed, }; use bitcoin::{OutPoint, Script, ScriptBuf, Txid}; @@ -345,8 +345,8 @@ impl SyncRequest { #[must_use] #[derive(Debug)] pub struct SyncResult { - /// The update to apply to the receiving [`TxGraph`]. - pub graph_update: TxGraph, + /// The update to apply to the receiving [`TxGraph`](crate::tx_graph::TxGraph). + pub graph_update: crate::tx_graph::Update, /// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain). pub chain_update: Option, } @@ -497,8 +497,8 @@ impl FullScanRequest { #[derive(Debug)] pub struct FullScanResult { /// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain). - pub graph_update: TxGraph, - /// The update to apply to the receiving [`TxGraph`]. + pub graph_update: crate::tx_graph::Update, + /// The update to apply to the receiving [`TxGraph`](crate::tx_graph::TxGraph). pub chain_update: Option, /// Last active indices for the corresponding keychains (`K`). pub last_active_indices: BTreeMap, diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 9ab1268b3..ba894fa93 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -70,13 +70,17 @@ //! //! ``` //! # use bdk_chain::{Merge, BlockId}; -//! # use bdk_chain::tx_graph::TxGraph; +//! # use bdk_chain::tx_graph::{self, TxGraph}; //! # use bdk_chain::example_utils::*; //! # use bitcoin::Transaction; +//! # use std::sync::Arc; //! # let tx_a = tx_from_hex(RAW_TX_1); //! # let tx_b = tx_from_hex(RAW_TX_2); //! let mut graph: TxGraph = TxGraph::default(); -//! let update = TxGraph::new(vec![tx_a, tx_b]); +//! +//! let mut update = tx_graph::Update::default(); +//! update.txs.push(Arc::new(tx_a)); +//! update.txs.push(Arc::new(tx_b)); //! //! // apply the update graph //! let changeset = graph.apply_update(update.clone()); @@ -101,6 +105,79 @@ use core::{ ops::{Deref, RangeInclusive}, }; +/// Data object used to update the [`TxGraph`] with. +#[derive(Debug, Clone)] +pub struct Update { + /// Full transactions. + pub txs: Vec>, + /// Floating txouts. + pub txouts: BTreeMap, + /// Transaction anchors. + pub anchors: BTreeSet<(A, Txid)>, + /// Seen at times for transactions. + pub seen_ats: HashMap, +} + +impl Default for Update { + fn default() -> Self { + Self { + txs: Default::default(), + txouts: Default::default(), + anchors: Default::default(), + seen_ats: Default::default(), + } + } +} + +impl From> for Update { + fn from(graph: TxGraph) -> Self { + Self { + txs: graph.full_txs().map(|tx_node| tx_node.tx).collect(), + txouts: graph + .floating_txouts() + .map(|(op, txo)| (op, txo.clone())) + .collect(), + anchors: graph.anchors, + seen_ats: graph.last_seen.into_iter().collect(), + } + } +} + +impl From> for TxGraph { + fn from(update: Update) -> Self { + let mut graph = TxGraph::::default(); + let _ = graph.apply_update(update); + graph + } +} + +impl Update { + /// Update the [`seen_ats`](Self::seen_ats) for all unanchored transactions. + pub fn update_last_seen_unconfirmed(&mut self, seen_at: u64) { + let seen_ats = &mut self.seen_ats; + let anchors = &self.anchors; + let unanchored_txids = self.txs.iter().map(|tx| tx.compute_txid()).filter(|txid| { + for (_, anchor_txid) in anchors { + if txid == anchor_txid { + return false; + } + } + true + }); + for txid in unanchored_txids { + seen_ats.insert(txid, seen_at); + } + } + + /// Extend this update with `other`. + pub fn extend(&mut self, other: Update) { + self.txs.extend(other.txs); + self.txouts.extend(other.txouts); + self.anchors.extend(other.anchors); + self.seen_ats.extend(other.seen_ats); + } +} + /// A graph of transactions and spends. /// /// See the [module-level documentation] for more. @@ -690,19 +767,19 @@ impl TxGraph { /// /// The returned [`ChangeSet`] is the set difference between `update` and `self` (transactions that /// exist in `update` but not in `self`). - pub fn apply_update(&mut self, update: TxGraph) -> ChangeSet { + pub fn apply_update(&mut self, update: Update) -> ChangeSet { let mut changeset = ChangeSet::::default(); - for tx_node in update.full_txs() { - changeset.merge(self.insert_tx(tx_node.tx)); + for tx in update.txs { + changeset.merge(self.insert_tx(tx)); } - for (outpoint, txout) in update.floating_txouts() { - changeset.merge(self.insert_txout(outpoint, txout.clone())); + for (outpoint, txout) in update.txouts { + changeset.merge(self.insert_txout(outpoint, txout)); } - for (anchor, txid) in &update.anchors { - changeset.merge(self.insert_anchor(*txid, anchor.clone())); + for (anchor, txid) in update.anchors { + changeset.merge(self.insert_anchor(txid, anchor)); } - for (&txid, &last_seen) in &update.last_seen { - changeset.merge(self.insert_seen_at(txid, last_seen)); + for (txid, seen_at) in update.seen_ats { + changeset.merge(self.insert_seen_at(txid, seen_at)); } changeset } diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 3ffa82439..c6399f53b 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -2,7 +2,7 @@ #[macro_use] mod common; -use bdk_chain::tx_graph::CalculateFeeError; +use bdk_chain::tx_graph::{self, CalculateFeeError}; use bdk_chain::{ collections::*, local_chain::LocalChain, @@ -49,7 +49,7 @@ fn insert_txouts() { )]; // One full transaction to be included in the update - let update_txs = Transaction { + let update_tx = Transaction { version: transaction::Version::ONE, lock_time: absolute::LockTime::ZERO, input: vec![TxIn { @@ -63,17 +63,17 @@ fn insert_txouts() { }; // Conf anchor used to mark the full transaction as confirmed. - let conf_anchor = ChainPosition::Confirmed(BlockId { + let conf_anchor = BlockId { height: 100, hash: h!("random blockhash"), - }); + }; - // Unconfirmed anchor to mark the partial transactions as unconfirmed - let unconf_anchor = ChainPosition::::Unconfirmed(1000000); + // Unconfirmed seen_at timestamp to mark the partial transactions as unconfirmed. + let unconf_seen_at = 1000000_u64; // Make the original graph let mut graph = { - let mut graph = TxGraph::>::default(); + let mut graph = TxGraph::::default(); for (outpoint, txout) in &original_ops { assert_eq!( graph.insert_txout(*outpoint, txout.clone()), @@ -88,57 +88,21 @@ fn insert_txouts() { // Make the update graph let update = { - let mut graph = TxGraph::default(); + let mut update = tx_graph::Update::default(); for (outpoint, txout) in &update_ops { - // Insert partials transactions - assert_eq!( - graph.insert_txout(*outpoint, txout.clone()), - ChangeSet { - txouts: [(*outpoint, txout.clone())].into(), - ..Default::default() - } - ); + // Insert partials transactions. + update.txouts.insert(*outpoint, txout.clone()); // Mark them unconfirmed. - assert_eq!( - graph.insert_anchor(outpoint.txid, unconf_anchor), - ChangeSet { - txs: [].into(), - txouts: [].into(), - anchors: [(unconf_anchor, outpoint.txid)].into(), - last_seen: [].into() - } - ); - // Mark them last seen at. - assert_eq!( - graph.insert_seen_at(outpoint.txid, 1000000), - ChangeSet { - txs: [].into(), - txouts: [].into(), - anchors: [].into(), - last_seen: [(outpoint.txid, 1000000)].into() - } - ); + update.seen_ats.insert(outpoint.txid, unconf_seen_at); } - // Insert the full transaction - assert_eq!( - graph.insert_tx(update_txs.clone()), - ChangeSet { - txs: [Arc::new(update_txs.clone())].into(), - ..Default::default() - } - ); + // Insert the full transaction. + update.txs.push(update_tx.clone().into()); // Mark it as confirmed. - assert_eq!( - graph.insert_anchor(update_txs.compute_txid(), conf_anchor), - ChangeSet { - txs: [].into(), - txouts: [].into(), - anchors: [(conf_anchor, update_txs.compute_txid())].into(), - last_seen: [].into() - } - ); - graph + update + .anchors + .insert((conf_anchor, update_tx.compute_txid())); + update }; // Check the resulting addition. @@ -147,13 +111,9 @@ fn insert_txouts() { assert_eq!( changeset, ChangeSet { - txs: [Arc::new(update_txs.clone())].into(), + txs: [Arc::new(update_tx.clone())].into(), txouts: update_ops.clone().into(), - anchors: [ - (conf_anchor, update_txs.compute_txid()), - (unconf_anchor, h!("tx2")) - ] - .into(), + anchors: [(conf_anchor, update_tx.compute_txid()),].into(), last_seen: [(h!("tx2"), 1000000)].into() } ); @@ -188,7 +148,7 @@ fn insert_txouts() { assert_eq!( graph - .tx_outputs(update_txs.compute_txid()) + .tx_outputs(update_tx.compute_txid()) .expect("should exists"), [( 0u32, @@ -204,13 +164,9 @@ fn insert_txouts() { assert_eq!( graph.initial_changeset(), ChangeSet { - txs: [Arc::new(update_txs.clone())].into(), + txs: [Arc::new(update_tx.clone())].into(), txouts: update_ops.into_iter().chain(original_ops).collect(), - anchors: [ - (conf_anchor, update_txs.compute_txid()), - (unconf_anchor, h!("tx2")) - ] - .into(), + anchors: [(conf_anchor, update_tx.compute_txid()),].into(), last_seen: [(h!("tx2"), 1000000)].into() } ); diff --git a/crates/electrum/src/bdk_electrum_client.rs b/crates/electrum/src/bdk_electrum_client.rs index 1458e2bd9..571660756 100644 --- a/crates/electrum/src/bdk_electrum_client.rs +++ b/crates/electrum/src/bdk_electrum_client.rs @@ -3,12 +3,12 @@ use bdk_chain::{ collections::{BTreeMap, HashMap}, local_chain::CheckPoint, spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}, - tx_graph::TxGraph, + tx_graph::{self, TxGraph}, Anchor, BlockId, ConfirmationBlockTime, }; use electrum_client::{ElectrumApi, Error, HeaderNotification}; use std::{ - collections::BTreeSet, + collections::HashSet, sync::{Arc, Mutex}, }; @@ -138,7 +138,7 @@ impl BdkElectrumClient { None => None, }; - let mut graph_update = TxGraph::::default(); + let mut graph_update = tx_graph::Update::::default(); let mut last_active_indices = BTreeMap::::default(); for keychain in request.keychains() { let spks = request.iter_spks(keychain.clone()); @@ -158,7 +158,7 @@ impl BdkElectrumClient { Some((chain_tip, latest_blocks)) => Some(chain_update( chain_tip, &latest_blocks, - graph_update.all_anchors(), + graph_update.anchors.iter().cloned(), )?), _ => None, }; @@ -205,7 +205,7 @@ impl BdkElectrumClient { None => None, }; - let mut graph_update = TxGraph::::default(); + let mut graph_update = tx_graph::Update::::default(); self.populate_with_spks( &mut graph_update, request @@ -227,7 +227,7 @@ impl BdkElectrumClient { Some((chain_tip, latest_blocks)) => Some(chain_update( chain_tip, &latest_blocks, - graph_update.all_anchors(), + graph_update.anchors.iter().cloned(), )?), None => None, }; @@ -245,7 +245,7 @@ impl BdkElectrumClient { /// also included. fn populate_with_spks( &self, - graph_update: &mut TxGraph, + graph_update: &mut tx_graph::Update, mut spks: impl Iterator, stop_gap: usize, batch_size: usize, @@ -278,7 +278,7 @@ impl BdkElectrumClient { } for tx_res in spk_history { - let _ = graph_update.insert_tx(self.fetch_tx(tx_res.tx_hash)?); + graph_update.txs.push(self.fetch_tx(tx_res.tx_hash)?); self.validate_merkle_for_anchor(graph_update, tx_res.tx_hash, tx_res.height)?; } } @@ -291,7 +291,7 @@ impl BdkElectrumClient { /// included. Anchors of the aforementioned transactions are included. fn populate_with_outpoints( &self, - graph_update: &mut TxGraph, + graph_update: &mut tx_graph::Update, outpoints: impl IntoIterator, ) -> Result<(), Error> { for outpoint in outpoints { @@ -314,7 +314,7 @@ impl BdkElectrumClient { if !has_residing && res.tx_hash == op_txid { has_residing = true; - let _ = graph_update.insert_tx(Arc::clone(&op_tx)); + graph_update.txs.push(Arc::clone(&op_tx)); self.validate_merkle_for_anchor(graph_update, res.tx_hash, res.height)?; } @@ -328,7 +328,7 @@ impl BdkElectrumClient { if !has_spending { continue; } - let _ = graph_update.insert_tx(Arc::clone(&res_tx)); + graph_update.txs.push(Arc::clone(&res_tx)); self.validate_merkle_for_anchor(graph_update, res.tx_hash, res.height)?; } } @@ -339,7 +339,7 @@ impl BdkElectrumClient { /// Populate the `graph_update` with transactions/anchors of the provided `txids`. fn populate_with_txids( &self, - graph_update: &mut TxGraph, + graph_update: &mut tx_graph::Update, txids: impl IntoIterator, ) -> Result<(), Error> { for txid in txids { @@ -366,7 +366,7 @@ impl BdkElectrumClient { self.validate_merkle_for_anchor(graph_update, txid, r.height)?; } - let _ = graph_update.insert_tx(tx); + graph_update.txs.push(tx); } Ok(()) } @@ -375,7 +375,7 @@ impl BdkElectrumClient { // An anchor is inserted if the transaction is validated to be in a confirmed block. fn validate_merkle_for_anchor( &self, - graph_update: &mut TxGraph, + graph_update: &mut tx_graph::Update, txid: Txid, confirmation_height: i32, ) -> Result<(), Error> { @@ -402,8 +402,7 @@ impl BdkElectrumClient { } if is_confirmed_tx { - let _ = graph_update.insert_anchor( - txid, + graph_update.anchors.insert(( ConfirmationBlockTime { confirmation_time: header.time as u64, block_id: BlockId { @@ -411,7 +410,8 @@ impl BdkElectrumClient { hash: header.block_hash(), }, }, - ); + txid, + )); } } Ok(()) @@ -421,17 +421,18 @@ impl BdkElectrumClient { // which we do not have by default. This data is needed to calculate the transaction fee. fn fetch_prev_txout( &self, - graph_update: &mut TxGraph, + graph_update: &mut tx_graph::Update, ) -> Result<(), Error> { - let full_txs: Vec> = - graph_update.full_txs().map(|tx_node| tx_node.tx).collect(); - for tx in full_txs { - for vin in &tx.input { - let outpoint = vin.previous_output; - let vout = outpoint.vout; - let prev_tx = self.fetch_tx(outpoint.txid)?; - let txout = prev_tx.output[vout as usize].clone(); - let _ = graph_update.insert_txout(outpoint, txout); + let mut no_dup = HashSet::::new(); + for tx in &graph_update.txs { + if no_dup.insert(tx.compute_txid()) { + for vin in &tx.input { + let outpoint = vin.previous_output; + let vout = outpoint.vout; + let prev_tx = self.fetch_tx(outpoint.txid)?; + let txout = prev_tx.output[vout as usize].clone(); + let _ = graph_update.txouts.insert(outpoint, txout); + } } } Ok(()) @@ -516,7 +517,7 @@ fn fetch_tip_and_latest_blocks( fn chain_update( mut tip: CheckPoint, latest_blocks: &BTreeMap, - anchors: &BTreeSet<(A, Txid)>, + anchors: impl Iterator, ) -> Result { for anchor in anchors { let height = anchor.0.anchor_block().height; diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index 63e91081b..e8b054d33 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -1,9 +1,9 @@ use bdk_chain::{ - bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, Txid, WScriptHash}, + bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash}, local_chain::LocalChain, spk_client::{FullScanRequest, SyncRequest, SyncResult}, spk_txout::SpkTxOutIndex, - Balance, ConfirmationBlockTime, IndexedTxGraph, Indexer, Merge, + Balance, ConfirmationBlockTime, IndexedTxGraph, Indexer, Merge, TxGraph, }; use bdk_electrum::BdkElectrumClient; use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; @@ -49,7 +49,7 @@ where .elapsed() .expect("must get time") .as_secs(); - let _ = update.graph_update.update_last_seen_unconfirmed(now); + update.graph_update.update_last_seen_unconfirmed(now); if let Some(chain_update) = update.chain_update.clone() { let _ = chain @@ -128,18 +128,23 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { ); let graph_update = sync_update.graph_update; + let updated_graph = { + let mut graph = TxGraph::::default(); + let _ = graph.apply_update(graph_update.clone()); + graph + }; // Check to see if we have the floating txouts available from our two created transactions' // previous outputs in order to calculate transaction fees. - for tx in graph_update.full_txs() { + for tx in &graph_update.txs { // Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the // floating txouts available from the transactions' previous outputs. - let fee = graph_update.calculate_fee(&tx.tx).expect("Fee must exist"); + let fee = updated_graph.calculate_fee(tx).expect("Fee must exist"); // Retrieve the fee in the transaction data from `bitcoind`. let tx_fee = env .bitcoind .client - .get_transaction(&tx.txid, None) + .get_transaction(&tx.compute_txid(), None) .expect("Tx must exist") .fee .expect("Fee must exist") @@ -151,12 +156,15 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { assert_eq!(fee, tx_fee); } - let mut graph_update_txids: Vec = graph_update.full_txs().map(|tx| tx.txid).collect(); - graph_update_txids.sort(); - let mut expected_txids = vec![txid1, txid2]; - expected_txids.sort(); - assert_eq!(graph_update_txids, expected_txids); - + assert_eq!( + graph_update + .txs + .iter() + .map(|tx| tx.compute_txid()) + .collect::>(), + [txid1, txid2].into(), + "update must include all expected transactions", + ); Ok(()) } @@ -216,7 +224,7 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { .spks_for_keychain(0, spks.clone()); client.full_scan(request, 3, 1, false)? }; - assert!(full_scan_update.graph_update.full_txs().next().is_none()); + assert!(full_scan_update.graph_update.txs.is_empty()); assert!(full_scan_update.last_active_indices.is_empty()); let full_scan_update = { let request = FullScanRequest::builder() @@ -227,10 +235,10 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { assert_eq!( full_scan_update .graph_update - .full_txs() - .next() + .txs + .first() .unwrap() - .txid, + .compute_txid(), txid_4th_addr ); assert_eq!(full_scan_update.last_active_indices[&0], 3); @@ -259,8 +267,9 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { }; let txs: HashSet<_> = full_scan_update .graph_update - .full_txs() - .map(|tx| tx.txid) + .txs + .iter() + .map(|tx| tx.compute_txid()) .collect(); assert_eq!(txs.len(), 1); assert!(txs.contains(&txid_4th_addr)); @@ -273,8 +282,9 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { }; let txs: HashSet<_> = full_scan_update .graph_update - .full_txs() - .map(|tx| tx.txid) + .txs + .iter() + .map(|tx| tx.compute_txid()) .collect(); assert_eq!(txs.len(), 2); assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr)); @@ -475,13 +485,12 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { )?; // Retain a snapshot of all anchors before reorg process. - let initial_anchors = update.graph_update.all_anchors(); - let anchors: Vec<_> = initial_anchors.iter().cloned().collect(); - assert_eq!(anchors.len(), REORG_COUNT); + let initial_anchors = update.graph_update.anchors.clone(); + assert_eq!(initial_anchors.len(), REORG_COUNT); for i in 0..REORG_COUNT { - let (anchor, txid) = anchors[i]; + let (anchor, txid) = initial_anchors.iter().nth(i).unwrap(); assert_eq!(anchor.block_id.hash, hashes[i]); - assert_eq!(txid, txids[i]); + assert_eq!(*txid, txids[i]); } // Check if initial balance is correct. @@ -507,7 +516,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { )?; // Check that no new anchors are added during current reorg. - assert!(initial_anchors.is_superset(update.graph_update.all_anchors())); + assert!(initial_anchors.is_superset(&update.graph_update.anchors)); assert_eq!( get_balance(&recv_chain, &recv_graph)?, diff --git a/crates/esplora/src/async_ext.rs b/crates/esplora/src/async_ext.rs index 066b91e17..f3c8e966a 100644 --- a/crates/esplora/src/async_ext.rs +++ b/crates/esplora/src/async_ext.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeSet; +use std::collections::{BTreeSet, HashSet}; use async_trait::async_trait; use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}; @@ -6,10 +6,9 @@ use bdk_chain::{ bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid}, collections::BTreeMap, local_chain::CheckPoint, - BlockId, ConfirmationBlockTime, TxGraph, + BlockId, ConfirmationBlockTime, }; -use bdk_chain::{Anchor, Indexed}; -use esplora_client::{Tx, TxStatus}; +use bdk_chain::{tx_graph, Anchor, Indexed}; use futures::{stream::FuturesOrdered, TryStreamExt}; use crate::{insert_anchor_from_status, insert_prevouts}; @@ -72,23 +71,29 @@ impl EsploraAsyncExt for esplora_client::AsyncClient { None }; - let mut graph_update = TxGraph::default(); + let mut graph_update = tx_graph::Update::::default(); + let mut inserted_txs = HashSet::::new(); let mut last_active_indices = BTreeMap::::new(); for keychain in keychains { let keychain_spks = request.iter_spks(keychain.clone()); - let (tx_graph, last_active_index) = - fetch_txs_with_keychain_spks(self, keychain_spks, stop_gap, parallel_requests) - .await?; - let _ = graph_update.apply_update(tx_graph); + let (update, last_active_index) = fetch_txs_with_keychain_spks( + self, + &mut inserted_txs, + keychain_spks, + stop_gap, + parallel_requests, + ) + .await?; + graph_update.extend(update); if let Some(last_active_index) = last_active_index { last_active_indices.insert(keychain, last_active_index); } } let chain_update = match (chain_tip, latest_blocks) { - (Some(chain_tip), Some(latest_blocks)) => Some( - chain_update(self, &latest_blocks, &chain_tip, graph_update.all_anchors()).await?, - ), + (Some(chain_tip), Some(latest_blocks)) => { + Some(chain_update(self, &latest_blocks, &chain_tip, &graph_update.anchors).await?) + } _ => None, }; @@ -113,20 +118,40 @@ impl EsploraAsyncExt for esplora_client::AsyncClient { None }; - let mut graph_update = TxGraph::::default(); - let _ = graph_update - .apply_update(fetch_txs_with_spks(self, request.iter_spks(), parallel_requests).await?); - let _ = graph_update.apply_update( - fetch_txs_with_txids(self, request.iter_txids(), parallel_requests).await?, + let mut graph_update = tx_graph::Update::::default(); + let mut inserted_txs = HashSet::::new(); + graph_update.extend( + fetch_txs_with_spks( + self, + &mut inserted_txs, + request.iter_spks(), + parallel_requests, + ) + .await?, + ); + graph_update.extend( + fetch_txs_with_txids( + self, + &mut inserted_txs, + request.iter_txids(), + parallel_requests, + ) + .await?, ); - let _ = graph_update.apply_update( - fetch_txs_with_outpoints(self, request.iter_outpoints(), parallel_requests).await?, + graph_update.extend( + fetch_txs_with_outpoints( + self, + &mut inserted_txs, + request.iter_outpoints(), + parallel_requests, + ) + .await?, ); let chain_update = match (chain_tip, latest_blocks) { - (Some(chain_tip), Some(latest_blocks)) => Some( - chain_update(self, &latest_blocks, &chain_tip, graph_update.all_anchors()).await?, - ), + (Some(chain_tip), Some(latest_blocks)) => { + Some(chain_update(self, &latest_blocks, &chain_tip, &graph_update.anchors).await?) + } _ => None, }; @@ -252,13 +277,14 @@ async fn chain_update( /// Refer to [crate-level docs](crate) for more. async fn fetch_txs_with_keychain_spks> + Send>( client: &esplora_client::AsyncClient, + inserted_txs: &mut HashSet, mut keychain_spks: I, stop_gap: usize, parallel_requests: usize, -) -> Result<(TxGraph, Option), Error> { +) -> Result<(tx_graph::Update, Option), Error> { type TxsOfSpkIndex = (u32, Vec); - let mut tx_graph = TxGraph::default(); + let mut update = tx_graph::Update::::default(); let mut last_index = Option::::None; let mut last_active_index = Option::::None; @@ -294,9 +320,11 @@ async fn fetch_txs_with_keychain_spks> + S last_active_index = Some(index); } for tx in txs { - let _ = tx_graph.insert_tx(tx.to_tx()); - insert_anchor_from_status(&mut tx_graph, tx.txid, tx.status); - insert_prevouts(&mut tx_graph, tx.vin); + if inserted_txs.insert(tx.txid) { + update.txs.push(tx.to_tx().into()); + } + insert_anchor_from_status(&mut update, tx.txid, tx.status); + insert_prevouts(&mut update, tx.vin); } } @@ -311,7 +339,7 @@ async fn fetch_txs_with_keychain_spks> + S } } - Ok((tx_graph, last_active_index)) + Ok((update, last_active_index)) } /// Fetch transactions and associated [`ConfirmationBlockTime`]s by scanning `spks` @@ -324,20 +352,22 @@ async fn fetch_txs_with_keychain_spks> + S /// Refer to [crate-level docs](crate) for more. async fn fetch_txs_with_spks + Send>( client: &esplora_client::AsyncClient, + inserted_txs: &mut HashSet, spks: I, parallel_requests: usize, -) -> Result, Error> +) -> Result, Error> where I::IntoIter: Send, { fetch_txs_with_keychain_spks( client, + inserted_txs, spks.into_iter().enumerate().map(|(i, spk)| (i as u32, spk)), usize::MAX, parallel_requests, ) .await - .map(|(tx_graph, _)| tx_graph) + .map(|(update, _)| update) } /// Fetch transactions and associated [`ConfirmationBlockTime`]s by scanning `txids` @@ -348,39 +378,27 @@ where /// Refer to [crate-level docs](crate) for more. async fn fetch_txs_with_txids + Send>( client: &esplora_client::AsyncClient, + inserted_txs: &mut HashSet, txids: I, parallel_requests: usize, -) -> Result, Error> +) -> Result, Error> where I::IntoIter: Send, { - enum EsploraResp { - TxStatus(TxStatus), - Tx(Option), - } - - let mut tx_graph = TxGraph::default(); - let mut txids = txids.into_iter(); + let mut update = tx_graph::Update::::default(); + // Only fetch for non-inserted txs. + let mut txids = txids + .into_iter() + .filter(|txid| !inserted_txs.contains(txid)) + .collect::>() + .into_iter(); loop { let handles = txids .by_ref() .take(parallel_requests) .map(|txid| { let client = client.clone(); - let tx_already_exists = tx_graph.get_tx(txid).is_some(); - async move { - if tx_already_exists { - client - .get_tx_status(&txid) - .await - .map(|s| (txid, EsploraResp::TxStatus(s))) - } else { - client - .get_tx_info(&txid) - .await - .map(|t| (txid, EsploraResp::Tx(t))) - } - } + async move { client.get_tx_info(&txid).await.map(|t| (txid, t)) } }) .collect::>(); @@ -388,21 +406,17 @@ where break; } - for (txid, resp) in handles.try_collect::>().await? { - match resp { - EsploraResp::TxStatus(status) => { - insert_anchor_from_status(&mut tx_graph, txid, status); - } - EsploraResp::Tx(Some(tx_info)) => { - let _ = tx_graph.insert_tx(tx_info.to_tx()); - insert_anchor_from_status(&mut tx_graph, txid, tx_info.status); - insert_prevouts(&mut tx_graph, tx_info.vin); + for (txid, tx_info) in handles.try_collect::>().await? { + if let Some(tx_info) = tx_info { + if inserted_txs.insert(txid) { + update.txs.push(tx_info.to_tx().into()); } - _ => continue, + insert_anchor_from_status(&mut update, txid, tx_info.status); + insert_prevouts(&mut update, tx_info.vin); } } } - Ok(tx_graph) + Ok(update) } /// Fetch transactions and [`ConfirmationBlockTime`]s that contain and spend the provided @@ -413,22 +427,27 @@ where /// Refer to [crate-level docs](crate) for more. async fn fetch_txs_with_outpoints + Send>( client: &esplora_client::AsyncClient, + inserted_txs: &mut HashSet, outpoints: I, parallel_requests: usize, -) -> Result, Error> +) -> Result, Error> where I::IntoIter: Send, { let outpoints = outpoints.into_iter().collect::>(); + let mut update = tx_graph::Update::::default(); // make sure txs exists in graph and tx statuses are updated // TODO: We should maintain a tx cache (like we do with Electrum). - let mut tx_graph = fetch_txs_with_txids( - client, - outpoints.iter().copied().map(|op| op.txid), - parallel_requests, - ) - .await?; + update.extend( + fetch_txs_with_txids( + client, + inserted_txs, + outpoints.iter().copied().map(|op| op.txid), + parallel_requests, + ) + .await?, + ); // get outpoint spend-statuses let mut outpoints = outpoints.into_iter(); @@ -452,18 +471,18 @@ where Some(txid) => txid, None => continue, }; - if tx_graph.get_tx(spend_txid).is_none() { + if !inserted_txs.contains(&spend_txid) { missing_txs.push(spend_txid); } if let Some(spend_status) = op_status.status { - insert_anchor_from_status(&mut tx_graph, spend_txid, spend_status); + insert_anchor_from_status(&mut update, spend_txid, spend_status); } } } - let _ = - tx_graph.apply_update(fetch_txs_with_txids(client, missing_txs, parallel_requests).await?); - Ok(tx_graph) + update + .extend(fetch_txs_with_txids(client, inserted_txs, missing_txs, parallel_requests).await?); + Ok(update) } #[cfg(test)] diff --git a/crates/esplora/src/blocking_ext.rs b/crates/esplora/src/blocking_ext.rs index 6e3e25afe..62f0d351e 100644 --- a/crates/esplora/src/blocking_ext.rs +++ b/crates/esplora/src/blocking_ext.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeSet; +use std::collections::{BTreeSet, HashSet}; use std::thread::JoinHandle; use bdk_chain::collections::BTreeMap; @@ -6,10 +6,10 @@ use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncRe use bdk_chain::{ bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid}, local_chain::CheckPoint, - BlockId, ConfirmationBlockTime, TxGraph, + BlockId, ConfirmationBlockTime, }; -use bdk_chain::{Anchor, Indexed}; -use esplora_client::{OutputStatus, Tx, TxStatus}; +use bdk_chain::{tx_graph, Anchor, Indexed}; +use esplora_client::{OutputStatus, Tx}; use crate::{insert_anchor_from_status, insert_prevouts}; @@ -66,13 +66,19 @@ impl EsploraExt for esplora_client::BlockingClient { None }; - let mut graph_update = TxGraph::default(); + let mut graph_update = tx_graph::Update::default(); + let mut inserted_txs = HashSet::::new(); let mut last_active_indices = BTreeMap::::new(); for keychain in request.keychains() { let keychain_spks = request.iter_spks(keychain.clone()); - let (tx_graph, last_active_index) = - fetch_txs_with_keychain_spks(self, keychain_spks, stop_gap, parallel_requests)?; - let _ = graph_update.apply_update(tx_graph); + let (update, last_active_index) = fetch_txs_with_keychain_spks( + self, + &mut inserted_txs, + keychain_spks, + stop_gap, + parallel_requests, + )?; + graph_update.extend(update); if let Some(last_active_index) = last_active_index { last_active_indices.insert(keychain, last_active_index); } @@ -83,7 +89,7 @@ impl EsploraExt for esplora_client::BlockingClient { self, &latest_blocks, &chain_tip, - graph_update.all_anchors(), + &graph_update.anchors, )?), _ => None, }; @@ -109,19 +115,23 @@ impl EsploraExt for esplora_client::BlockingClient { None }; - let mut graph_update = TxGraph::default(); - let _ = graph_update.apply_update(fetch_txs_with_spks( + let mut graph_update = tx_graph::Update::::default(); + let mut inserted_txs = HashSet::::new(); + graph_update.extend(fetch_txs_with_spks( self, + &mut inserted_txs, request.iter_spks(), parallel_requests, )?); - let _ = graph_update.apply_update(fetch_txs_with_txids( + graph_update.extend(fetch_txs_with_txids( self, + &mut inserted_txs, request.iter_txids(), parallel_requests, )?); - let _ = graph_update.apply_update(fetch_txs_with_outpoints( + graph_update.extend(fetch_txs_with_outpoints( self, + &mut inserted_txs, request.iter_outpoints(), parallel_requests, )?); @@ -131,7 +141,7 @@ impl EsploraExt for esplora_client::BlockingClient { self, &latest_blocks, &chain_tip, - graph_update.all_anchors(), + &graph_update.anchors, )?), _ => None, }; @@ -244,13 +254,14 @@ fn chain_update( fn fetch_txs_with_keychain_spks>>( client: &esplora_client::BlockingClient, + inserted_txs: &mut HashSet, mut keychain_spks: I, stop_gap: usize, parallel_requests: usize, -) -> Result<(TxGraph, Option), Error> { +) -> Result<(tx_graph::Update, Option), Error> { type TxsOfSpkIndex = (u32, Vec); - let mut tx_graph = TxGraph::default(); + let mut update = tx_graph::Update::::default(); let mut last_index = Option::::None; let mut last_active_index = Option::::None; @@ -289,9 +300,11 @@ fn fetch_txs_with_keychain_spks>>( last_active_index = Some(index); } for tx in txs { - let _ = tx_graph.insert_tx(tx.to_tx()); - insert_anchor_from_status(&mut tx_graph, tx.txid, tx.status); - insert_prevouts(&mut tx_graph, tx.vin); + if inserted_txs.insert(tx.txid) { + update.txs.push(tx.to_tx().into()); + } + insert_anchor_from_status(&mut update, tx.txid, tx.status); + insert_prevouts(&mut update, tx.vin); } } @@ -306,7 +319,7 @@ fn fetch_txs_with_keychain_spks>>( } } - Ok((tx_graph, last_active_index)) + Ok((update, last_active_index)) } /// Fetch transactions and associated [`ConfirmationBlockTime`]s by scanning `spks` @@ -319,16 +332,18 @@ fn fetch_txs_with_keychain_spks>>( /// Refer to [crate-level docs](crate) for more. fn fetch_txs_with_spks>( client: &esplora_client::BlockingClient, + inserted_txs: &mut HashSet, spks: I, parallel_requests: usize, -) -> Result, Error> { +) -> Result, Error> { fetch_txs_with_keychain_spks( client, + inserted_txs, spks.into_iter().enumerate().map(|(i, spk)| (i as u32, spk)), usize::MAX, parallel_requests, ) - .map(|(tx_graph, _)| tx_graph) + .map(|(update, _)| update) } /// Fetch transactions and associated [`ConfirmationBlockTime`]s by scanning `txids` @@ -339,59 +354,48 @@ fn fetch_txs_with_spks>( /// Refer to [crate-level docs](crate) for more. fn fetch_txs_with_txids>( client: &esplora_client::BlockingClient, + inserted_txs: &mut HashSet, txids: I, parallel_requests: usize, -) -> Result, Error> { - enum EsploraResp { - TxStatus(TxStatus), - Tx(Option), - } - - let mut tx_graph = TxGraph::default(); - let mut txids = txids.into_iter(); +) -> Result, Error> { + let mut update = tx_graph::Update::::default(); + // Only fetch for non-inserted txs. + let mut txids = txids + .into_iter() + .filter(|txid| !inserted_txs.contains(txid)) + .collect::>() + .into_iter(); loop { let handles = txids .by_ref() .take(parallel_requests) .map(|txid| { let client = client.clone(); - let tx_already_exists = tx_graph.get_tx(txid).is_some(); std::thread::spawn(move || { - if tx_already_exists { - client - .get_tx_status(&txid) - .map_err(Box::new) - .map(|s| (txid, EsploraResp::TxStatus(s))) - } else { - client - .get_tx_info(&txid) - .map_err(Box::new) - .map(|t| (txid, EsploraResp::Tx(t))) - } + client + .get_tx_info(&txid) + .map_err(Box::new) + .map(|t| (txid, t)) }) }) - .collect::>>>(); + .collect::), Error>>>>(); if handles.is_empty() { break; } for handle in handles { - let (txid, resp) = handle.join().expect("thread must not panic")?; - match resp { - EsploraResp::TxStatus(status) => { - insert_anchor_from_status(&mut tx_graph, txid, status); + let (txid, tx_info) = handle.join().expect("thread must not panic")?; + if let Some(tx_info) = tx_info { + if inserted_txs.insert(txid) { + update.txs.push(tx_info.to_tx().into()); } - EsploraResp::Tx(Some(tx_info)) => { - let _ = tx_graph.insert_tx(tx_info.to_tx()); - insert_anchor_from_status(&mut tx_graph, txid, tx_info.status); - insert_prevouts(&mut tx_graph, tx_info.vin); - } - _ => continue, + insert_anchor_from_status(&mut update, txid, tx_info.status); + insert_prevouts(&mut update, tx_info.vin); } } } - Ok(tx_graph) + Ok(update) } /// Fetch transactions and [`ConfirmationBlockTime`]s that contain and spend the provided @@ -402,18 +406,21 @@ fn fetch_txs_with_txids>( /// Refer to [crate-level docs](crate) for more. fn fetch_txs_with_outpoints>( client: &esplora_client::BlockingClient, + inserted_txs: &mut HashSet, outpoints: I, parallel_requests: usize, -) -> Result, Error> { +) -> Result, Error> { let outpoints = outpoints.into_iter().collect::>(); + let mut update = tx_graph::Update::::default(); // make sure txs exists in graph and tx statuses are updated // TODO: We should maintain a tx cache (like we do with Electrum). - let mut tx_graph = fetch_txs_with_txids( + update.extend(fetch_txs_with_txids( client, + inserted_txs, outpoints.iter().map(|op| op.txid), parallel_requests, - )?; + )?); // get outpoint spend-statuses let mut outpoints = outpoints.into_iter(); @@ -442,22 +449,23 @@ fn fetch_txs_with_outpoints>( Some(txid) => txid, None => continue, }; - if tx_graph.get_tx(spend_txid).is_none() { + if !inserted_txs.contains(&spend_txid) { missing_txs.push(spend_txid); } if let Some(spend_status) = op_status.status { - insert_anchor_from_status(&mut tx_graph, spend_txid, spend_status); + insert_anchor_from_status(&mut update, spend_txid, spend_status); } } } } - let _ = tx_graph.apply_update(fetch_txs_with_txids( + update.extend(fetch_txs_with_txids( client, + inserted_txs, missing_txs, parallel_requests, )?); - Ok(tx_graph) + Ok(update) } #[cfg(test)] diff --git a/crates/esplora/src/lib.rs b/crates/esplora/src/lib.rs index 7db6967b6..9a6e8f1df 100644 --- a/crates/esplora/src/lib.rs +++ b/crates/esplora/src/lib.rs @@ -26,7 +26,7 @@ //! [`example_esplora`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_esplora use bdk_chain::bitcoin::{Amount, OutPoint, TxOut, Txid}; -use bdk_chain::{BlockId, ConfirmationBlockTime, TxGraph}; +use bdk_chain::{tx_graph, BlockId, ConfirmationBlockTime}; use esplora_client::TxStatus; pub use esplora_client; @@ -42,7 +42,7 @@ mod async_ext; pub use async_ext::*; fn insert_anchor_from_status( - tx_graph: &mut TxGraph, + update: &mut tx_graph::Update, txid: Txid, status: TxStatus, ) { @@ -57,21 +57,21 @@ fn insert_anchor_from_status( block_id: BlockId { height, hash }, confirmation_time: time, }; - let _ = tx_graph.insert_anchor(txid, anchor); + update.anchors.insert((anchor, txid)); } } /// Inserts floating txouts into `tx_graph` using [`Vin`](esplora_client::api::Vin)s returned by /// Esplora. fn insert_prevouts( - tx_graph: &mut TxGraph, + update: &mut tx_graph::Update, esplora_inputs: impl IntoIterator, ) { let prevouts = esplora_inputs .into_iter() .filter_map(|vin| Some((vin.txid, vin.vout, vin.prevout?))); for (prev_txid, prev_vout, prev_txout) in prevouts { - let _ = tx_graph.insert_txout( + update.txouts.insert( OutPoint::new(prev_txid, prev_vout), TxOut { script_pubkey: prev_txout.scriptpubkey, diff --git a/crates/esplora/tests/async_ext.rs b/crates/esplora/tests/async_ext.rs index 70d464194..7b0ef7fa6 100644 --- a/crates/esplora/tests/async_ext.rs +++ b/crates/esplora/tests/async_ext.rs @@ -1,4 +1,5 @@ use bdk_chain::spk_client::{FullScanRequest, SyncRequest}; +use bdk_chain::{ConfirmationBlockTime, TxGraph}; use bdk_esplora::EsploraAsyncExt; use esplora_client::{self, Builder}; use std::collections::{BTreeSet, HashSet}; @@ -6,7 +7,7 @@ use std::str::FromStr; use std::thread::sleep; use std::time::Duration; -use bdk_chain::bitcoin::{Address, Amount, Txid}; +use bdk_chain::bitcoin::{Address, Amount}; use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; #[tokio::test] @@ -78,18 +79,23 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { ); let graph_update = sync_update.graph_update; + let updated_graph = { + let mut graph = TxGraph::::default(); + let _ = graph.apply_update(graph_update.clone()); + graph + }; // Check to see if we have the floating txouts available from our two created transactions' // previous outputs in order to calculate transaction fees. - for tx in graph_update.full_txs() { + for tx in &graph_update.txs { // Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the // floating txouts available from the transactions' previous outputs. - let fee = graph_update.calculate_fee(&tx.tx).expect("Fee must exist"); + let fee = updated_graph.calculate_fee(tx).expect("Fee must exist"); // Retrieve the fee in the transaction data from `bitcoind`. let tx_fee = env .bitcoind .client - .get_transaction(&tx.txid, None) + .get_transaction(&tx.compute_txid(), None) .expect("Tx must exist") .fee .expect("Fee must exist") @@ -101,11 +107,15 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { assert_eq!(fee, tx_fee); } - let mut graph_update_txids: Vec = graph_update.full_txs().map(|tx| tx.txid).collect(); - graph_update_txids.sort(); - let mut expected_txids = vec![txid1, txid2]; - expected_txids.sort(); - assert_eq!(graph_update_txids, expected_txids); + assert_eq!( + graph_update + .txs + .iter() + .map(|tx| tx.compute_txid()) + .collect::>(), + [txid1, txid2].into(), + "update must include all expected transactions" + ); Ok(()) } @@ -167,7 +177,7 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> { .spks_for_keychain(0, spks.clone()); client.full_scan(request, 3, 1).await? }; - assert!(full_scan_update.graph_update.full_txs().next().is_none()); + assert!(full_scan_update.graph_update.txs.is_empty()); assert!(full_scan_update.last_active_indices.is_empty()); let full_scan_update = { let request = FullScanRequest::builder() @@ -178,10 +188,10 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> { assert_eq!( full_scan_update .graph_update - .full_txs() - .next() + .txs + .first() .unwrap() - .txid, + .compute_txid(), txid_4th_addr ); assert_eq!(full_scan_update.last_active_indices[&0], 3); @@ -212,8 +222,9 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> { }; let txs: HashSet<_> = full_scan_update .graph_update - .full_txs() - .map(|tx| tx.txid) + .txs + .iter() + .map(|tx| tx.compute_txid()) .collect(); assert_eq!(txs.len(), 1); assert!(txs.contains(&txid_4th_addr)); @@ -226,8 +237,9 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> { }; let txs: HashSet<_> = full_scan_update .graph_update - .full_txs() - .map(|tx| tx.txid) + .txs + .iter() + .map(|tx| tx.compute_txid()) .collect(); assert_eq!(txs.len(), 2); assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr)); diff --git a/crates/esplora/tests/blocking_ext.rs b/crates/esplora/tests/blocking_ext.rs index 818f1f5fb..b3833b899 100644 --- a/crates/esplora/tests/blocking_ext.rs +++ b/crates/esplora/tests/blocking_ext.rs @@ -1,4 +1,5 @@ use bdk_chain::spk_client::{FullScanRequest, SyncRequest}; +use bdk_chain::{ConfirmationBlockTime, TxGraph}; use bdk_esplora::EsploraExt; use esplora_client::{self, Builder}; use std::collections::{BTreeSet, HashSet}; @@ -6,7 +7,7 @@ use std::str::FromStr; use std::thread::sleep; use std::time::Duration; -use bdk_chain::bitcoin::{Address, Amount, Txid}; +use bdk_chain::bitcoin::{Address, Amount}; use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; #[test] @@ -78,18 +79,23 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { ); let graph_update = sync_update.graph_update; + let updated_graph = { + let mut graph = TxGraph::::default(); + let _ = graph.apply_update(graph_update.clone()); + graph + }; // Check to see if we have the floating txouts available from our two created transactions' // previous outputs in order to calculate transaction fees. - for tx in graph_update.full_txs() { + for tx in &graph_update.txs { // Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the // floating txouts available from the transactions' previous outputs. - let fee = graph_update.calculate_fee(&tx.tx).expect("Fee must exist"); + let fee = updated_graph.calculate_fee(tx).expect("Fee must exist"); // Retrieve the fee in the transaction data from `bitcoind`. let tx_fee = env .bitcoind .client - .get_transaction(&tx.txid, None) + .get_transaction(&tx.compute_txid(), None) .expect("Tx must exist") .fee .expect("Fee must exist") @@ -101,12 +107,15 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { assert_eq!(fee, tx_fee); } - let mut graph_update_txids: Vec = graph_update.full_txs().map(|tx| tx.txid).collect(); - graph_update_txids.sort(); - let mut expected_txids = vec![txid1, txid2]; - expected_txids.sort(); - assert_eq!(graph_update_txids, expected_txids); - + assert_eq!( + graph_update + .txs + .iter() + .map(|tx| tx.compute_txid()) + .collect::>(), + [txid1, txid2].into(), + "update must include all expected transactions" + ); Ok(()) } @@ -168,7 +177,7 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { .spks_for_keychain(0, spks.clone()); client.full_scan(request, 3, 1)? }; - assert!(full_scan_update.graph_update.full_txs().next().is_none()); + assert!(full_scan_update.graph_update.txs.is_empty()); assert!(full_scan_update.last_active_indices.is_empty()); let full_scan_update = { let request = FullScanRequest::builder() @@ -179,10 +188,10 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { assert_eq!( full_scan_update .graph_update - .full_txs() - .next() + .txs + .first() .unwrap() - .txid, + .compute_txid(), txid_4th_addr ); assert_eq!(full_scan_update.last_active_indices[&0], 3); @@ -213,8 +222,9 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { }; let txs: HashSet<_> = full_scan_update .graph_update - .full_txs() - .map(|tx| tx.txid) + .txs + .iter() + .map(|tx| tx.compute_txid()) .collect(); assert_eq!(txs.len(), 1); assert!(txs.contains(&txid_4th_addr)); @@ -227,8 +237,9 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { }; let txs: HashSet<_> = full_scan_update .graph_update - .full_txs() - .map(|tx| tx.txid) + .txs + .iter() + .map(|tx| tx.compute_txid()) .collect(); assert_eq!(txs.len(), 2); assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr)); diff --git a/crates/wallet/src/wallet/export.rs b/crates/wallet/src/wallet/export.rs index 6dce7503b..386d9d4e3 100644 --- a/crates/wallet/src/wallet/export.rs +++ b/crates/wallet/src/wallet/export.rs @@ -219,13 +219,13 @@ mod test { use bdk_chain::{BlockId, ConfirmationBlockTime}; use bitcoin::hashes::Hash; use bitcoin::{transaction, BlockHash, Network, Transaction}; + use chain::tx_graph; use super::*; use crate::Wallet; fn get_test_wallet(descriptor: &str, change_descriptor: &str, network: Network) -> Wallet { use crate::wallet::Update; - use bdk_chain::TxGraph; let mut wallet = Wallet::create(descriptor.to_string(), change_descriptor.to_string()) .network(network) .create_wallet_no_persist() @@ -253,11 +253,12 @@ mod test { confirmation_time: 0, block_id, }; - let mut graph = TxGraph::default(); - let _ = graph.insert_anchor(txid, anchor); wallet .apply_update(Update { - graph, + graph: tx_graph::Update { + anchors: [(anchor, txid)].into_iter().collect(), + ..Default::default() + }, ..Default::default() }) .unwrap(); diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 47e440c71..0d6cdf184 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -132,7 +132,7 @@ pub struct Update { pub last_active_indices: BTreeMap, /// Update for the wallet's internal [`TxGraph`]. - pub graph: TxGraph, + pub graph: chain::tx_graph::Update, /// Update for the wallet's internal [`LocalChain`]. /// @@ -2562,7 +2562,7 @@ macro_rules! floating_rate { macro_rules! doctest_wallet { () => {{ use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash}; - use $crate::chain::{ConfirmationBlockTime, BlockId, TxGraph}; + use $crate::chain::{ConfirmationBlockTime, BlockId, TxGraph, tx_graph}; use $crate::{Update, KeychainKind, Wallet}; let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)"; let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)"; @@ -2590,9 +2590,13 @@ macro_rules! doctest_wallet { confirmation_time: 50_000, block_id, }; - let mut graph = TxGraph::default(); - let _ = graph.insert_anchor(txid, anchor); - let update = Update { graph, ..Default::default() }; + let update = Update { + graph: tx_graph::Update { + anchors: [(anchor, txid)].into_iter().collect(), + ..Default::default() + }, + ..Default::default() + }; wallet.apply_update(update).unwrap(); wallet }} diff --git a/crates/wallet/tests/common.rs b/crates/wallet/tests/common.rs index 288560b0a..561a9a5fb 100644 --- a/crates/wallet/tests/common.rs +++ b/crates/wallet/tests/common.rs @@ -1,5 +1,5 @@ #![allow(unused)] -use bdk_chain::{BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph}; +use bdk_chain::{tx_graph, BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph}; use bdk_wallet::{CreateParams, KeychainKind, LocalOutput, Update, Wallet}; use bitcoin::{ hashes::Hash, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction, @@ -218,11 +218,12 @@ pub fn insert_anchor_from_conf(wallet: &mut Wallet, txid: Txid, position: Confir }) .expect("confirmation height cannot be greater than tip"); - let mut graph = TxGraph::default(); - let _ = graph.insert_anchor(txid, anchor); wallet .apply_update(Update { - graph, + graph: tx_graph::Update { + anchors: [(anchor, txid)].into(), + ..Default::default() + }, ..Default::default() }) .unwrap(); diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index c530e779c..243161658 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -5,7 +5,7 @@ use std::str::FromStr; use anyhow::Context; use assert_matches::assert_matches; -use bdk_chain::COINBASE_MATURITY; +use bdk_chain::{tx_graph, COINBASE_MATURITY}; use bdk_chain::{BlockId, ConfirmationTime}; use bdk_wallet::coin_selection::{self, LargestFirstCoinSelection}; use bdk_wallet::descriptor::{calc_checksum, DescriptorError, IntoWalletDescriptor}; @@ -81,11 +81,12 @@ fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint { fn insert_seen_at(wallet: &mut Wallet, txid: Txid, seen_at: u64) { use bdk_wallet::Update; - let mut graph = bdk_chain::TxGraph::default(); - let _ = graph.insert_seen_at(txid, seen_at); wallet .apply_update(Update { - graph, + graph: tx_graph::Update { + seen_ats: [(txid, seen_at)].into_iter().collect(), + ..Default::default() + }, ..Default::default() }) .unwrap(); diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index 49608fbf1..7212547d6 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -252,7 +252,7 @@ fn main() -> anyhow::Result<()> { .elapsed() .expect("must get time") .as_secs(); - let _ = graph_update.update_last_seen_unconfirmed(now); + graph_update.update_last_seen_unconfirmed(now); let db_changeset = { let mut chain = chain.lock().unwrap(); diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index b07a6697d..d188eab76 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -172,7 +172,7 @@ fn main() -> anyhow::Result<()> { // We want to keep track of the latest time a transaction was seen unconfirmed. let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - let _ = update.graph_update.update_last_seen_unconfirmed(now); + update.graph_update.update_last_seen_unconfirmed(now); let mut graph = graph.lock().expect("mutex must not be poisoned"); let mut chain = chain.lock().expect("mutex must not be poisoned"); @@ -269,7 +269,7 @@ fn main() -> anyhow::Result<()> { // Update last seen unconfirmed let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - let _ = update.graph_update.update_last_seen_unconfirmed(now); + update.graph_update.update_last_seen_unconfirmed(now); ( chain diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index f4596ce18..4cc698a00 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -67,7 +67,7 @@ fn main() -> Result<(), anyhow::Error> { let mut update = client.full_scan(request, STOP_GAP, BATCH_SIZE, false)?; let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - let _ = update.graph_update.update_last_seen_unconfirmed(now); + update.graph_update.update_last_seen_unconfirmed(now); println!(); diff --git a/example-crates/wallet_esplora_async/src/main.rs b/example-crates/wallet_esplora_async/src/main.rs index f81f8101c..d4dae1f3f 100644 --- a/example-crates/wallet_esplora_async/src/main.rs +++ b/example-crates/wallet_esplora_async/src/main.rs @@ -61,7 +61,7 @@ async fn main() -> Result<(), anyhow::Error> { .full_scan(request, STOP_GAP, PARALLEL_REQUESTS) .await?; let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - let _ = update.graph_update.update_last_seen_unconfirmed(now); + update.graph_update.update_last_seen_unconfirmed(now); wallet.apply_update(update)?; wallet.persist(&mut conn)?; diff --git a/example-crates/wallet_esplora_blocking/src/main.rs b/example-crates/wallet_esplora_blocking/src/main.rs index bec395611..9f79d6bf6 100644 --- a/example-crates/wallet_esplora_blocking/src/main.rs +++ b/example-crates/wallet_esplora_blocking/src/main.rs @@ -61,7 +61,7 @@ fn main() -> Result<(), anyhow::Error> { let mut update = client.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)?; let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - let _ = update.graph_update.update_last_seen_unconfirmed(now); + update.graph_update.update_last_seen_unconfirmed(now); wallet.apply_update(update)?; if let Some(changeset) = wallet.take_staged() { From 9d47ad14ef671d83235f374e2a78f2b60e230bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 23 Aug 2024 12:14:42 +0000 Subject: [PATCH 3/6] docs(chain): use `doc_cfg` feature --- crates/chain/src/lib.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 3fb8c0eda..029eedc28 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -17,6 +17,12 @@ //! //! [Bitcoin Dev Kit]: https://bitcoindevkit.org/ +// only enables the `doc_cfg` feature when the `docsrs` configuration attribute is defined +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr( + docsrs, + doc(html_logo_url = "https://github.com/bitcoindevkit/bdk/raw/master/static/bdk.png") +)] #![no_std] #![warn(missing_docs)] From 1adcb62ff0a7dd62e2c644e69365008e7f8a4389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 23 Aug 2024 09:53:42 +0000 Subject: [PATCH 4/6] feat(chain)!: `TxGraph::apply_update` auto-adds `seen_at` for unanchored Change `apply_update` to use the current timestamp as `seen_at` for unanchored transactions of the update. This makes `apply_update` only avaliable with the "std" feature. Introduce `apply_update_at` which includes an optional `seen_at` input. This is the no-std version of `apply_update`. Also update docs. --- crates/chain/src/indexed_tx_graph.rs | 27 ++++++++- crates/chain/src/tx_graph.rs | 56 ++++++++++++------- crates/electrum/tests/test_electrum.rs | 9 +-- crates/wallet/src/wallet/mod.rs | 33 ++++++++++- example-crates/example_electrum/src/main.rs | 8 +-- example-crates/example_esplora/src/main.rs | 12 +--- example-crates/wallet_electrum/src/main.rs | 5 +- .../wallet_esplora_async/src/main.rs | 4 +- .../wallet_esplora_blocking/src/main.rs | 4 +- 9 files changed, 100 insertions(+), 58 deletions(-) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index d24b1b307..73ae458ff 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -90,13 +90,38 @@ where /// Apply an `update` directly. /// - /// `update` is a [`TxGraph`] and the resultant changes is returned as [`ChangeSet`]. + /// `update` is a [`tx_graph::Update`] and the resultant changes is returned as [`ChangeSet`]. + #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn apply_update(&mut self, update: tx_graph::Update) -> ChangeSet { let tx_graph = self.graph.apply_update(update); let indexer = self.index_tx_graph_changeset(&tx_graph); ChangeSet { tx_graph, indexer } } + /// Apply the given `update` with an optional `seen_at` timestamp. + /// + /// `seen_at` represents when the update is seen (in unix seconds). It is used to determine the + /// `last_seen`s for all transactions in the update which have no corresponding anchor(s). The + /// `last_seen` value is used internally to determine precedence of conflicting unconfirmed + /// transactions (where the transaction with the lower `last_seen` value is omitted from the + /// canonical history). + /// + /// Not setting a `seen_at` value means unconfirmed transactions introduced by this update will + /// not be part of the canonical history of transactions. + /// + /// Use [`apply_update`](IndexedTxGraph::apply_update) to have the `seen_at` value automatically + /// set to the current time. + pub fn apply_update_at( + &mut self, + update: tx_graph::Update, + seen_at: Option, + ) -> ChangeSet { + let tx_graph = self.graph.apply_update_at(update, seen_at); + let indexer = self.index_tx_graph_changeset(&tx_graph); + ChangeSet { tx_graph, indexer } + } + /// Insert a floating `txout` of given `outpoint`. pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet { let graph = self.graph.insert_txout(outpoint, txout); diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index ba894fa93..6da4d8afd 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -146,29 +146,12 @@ impl From> for Update { impl From> for TxGraph { fn from(update: Update) -> Self { let mut graph = TxGraph::::default(); - let _ = graph.apply_update(update); + let _ = graph.apply_update_at(update, None); graph } } impl Update { - /// Update the [`seen_ats`](Self::seen_ats) for all unanchored transactions. - pub fn update_last_seen_unconfirmed(&mut self, seen_at: u64) { - let seen_ats = &mut self.seen_ats; - let anchors = &self.anchors; - let unanchored_txids = self.txs.iter().map(|tx| tx.compute_txid()).filter(|txid| { - for (_, anchor_txid) in anchors { - if txid == anchor_txid { - return false; - } - } - true - }); - for txid in unanchored_txids { - seen_ats.insert(txid, seen_at); - } - } - /// Extend this update with `other`. pub fn extend(&mut self, other: Update) { self.txs.extend(other.txs); @@ -762,25 +745,56 @@ impl TxGraph { changeset } - /// Extends this graph with another so that `self` becomes the union of the two sets of - /// transactions. + /// Extends this graph with the given `update`. /// /// The returned [`ChangeSet`] is the set difference between `update` and `self` (transactions that /// exist in `update` but not in `self`). + #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn apply_update(&mut self, update: Update) -> ChangeSet { + use std::time::*; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("current time must be greater than epoch anchor"); + self.apply_update_at(update, Some(now.as_secs())) + } + + /// Extends this graph with the given `update` alongside an optional `seen_at` timestamp. + /// + /// `seen_at` represents when the update is seen (in unix seconds). It is used to determine the + /// `last_seen`s for all transactions in the update which have no corresponding anchor(s). The + /// `last_seen` value is used internally to determine precedence of conflicting unconfirmed + /// transactions (where the transaction with the lower `last_seen` value is omitted from the + /// canonical history). + /// + /// Not setting a `seen_at` value means unconfirmed transactions introduced by this update will + /// not be part of the canonical history of transactions. + /// + /// Use [`apply_update`](TxGraph::apply_update) to have the `seen_at` value automatically set + /// to the current time. + pub fn apply_update_at(&mut self, update: Update, seen_at: Option) -> ChangeSet { let mut changeset = ChangeSet::::default(); + let mut unanchored_txs = HashSet::::new(); for tx in update.txs { - changeset.merge(self.insert_tx(tx)); + if unanchored_txs.insert(tx.compute_txid()) { + changeset.merge(self.insert_tx(tx)); + } } for (outpoint, txout) in update.txouts { changeset.merge(self.insert_txout(outpoint, txout)); } for (anchor, txid) in update.anchors { + unanchored_txs.remove(&txid); changeset.merge(self.insert_anchor(txid, anchor)); } for (txid, seen_at) in update.seen_ats { changeset.merge(self.insert_seen_at(txid, seen_at)); } + if let Some(seen_at) = seen_at { + for txid in unanchored_txs { + changeset.merge(self.insert_seen_at(txid, seen_at)); + } + } changeset } diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index e8b054d33..d5e4a1596 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -38,19 +38,12 @@ where Spks: IntoIterator, Spks::IntoIter: ExactSizeIterator + Send + 'static, { - let mut update = client.sync( + let update = client.sync( SyncRequest::builder().chain_tip(chain.tip()).spks(spks), BATCH_SIZE, true, )?; - // Update `last_seen` to be able to calculate balance for unconfirmed transactions. - let now = std::time::UNIX_EPOCH - .elapsed() - .expect("must get time") - .as_secs(); - update.graph_update.update_last_seen_unconfirmed(now); - if let Some(chain_update) = update.chain_update.clone() { let _ = chain .apply_update(chain_update) diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 0d6cdf184..638bb5757 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -2277,7 +2277,34 @@ impl Wallet { /// to persist staged wallet changes see [`Wallet::reveal_next_address`]. ` /// /// [`commit`]: Self::commit + #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn apply_update(&mut self, update: impl Into) -> Result<(), CannotConnectError> { + use std::time::*; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time now must surpass epoch anchor"); + self.apply_update_at(update, Some(now.as_secs())) + } + + /// Applies an `update` alongside an optional `seen_at` timestamp and stages the changes. + /// + /// `seen_at` represents when the update is seen (in unix seconds). It is used to determine the + /// `last_seen`s for all transactions in the update which have no corresponding anchor(s). The + /// `last_seen` value is used internally to determine precedence of conflicting unconfirmed + /// transactions (where the transaction with the lower `last_seen` value is omitted from the + /// canonical history). + /// + /// Not setting a `seen_at` value means unconfirmed transactions introduced by this update will + /// not be part of the canonical history of transactions. + /// + /// Use [`apply_update`](Wallet::apply_update) to have the `seen_at` value automatically set to + /// the current time. + pub fn apply_update_at( + &mut self, + update: impl Into, + seen_at: Option, + ) -> Result<(), CannotConnectError> { let update = update.into(); let mut changeset = match update.chain { Some(chain_update) => ChangeSet::from(self.chain.apply_update(chain_update)?), @@ -2289,7 +2316,11 @@ impl Wallet { .index .reveal_to_target_multi(&update.last_active_indices); changeset.merge(index_changeset.into()); - changeset.merge(self.indexed_graph.apply_update(update.graph).into()); + changeset.merge( + self.indexed_graph + .apply_update_at(update.graph, seen_at) + .into(), + ); self.stage.merge(changeset); Ok(()) } diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index 7212547d6..662bc4237 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -129,7 +129,7 @@ fn main() -> anyhow::Result<()> { // Tell the electrum client about the txs we've already got locally so it doesn't re-download them client.populate_tx_cache(&*graph.lock().unwrap()); - let (chain_update, mut graph_update, keychain_update) = match electrum_cmd.clone() { + let (chain_update, graph_update, keychain_update) = match electrum_cmd.clone() { ElectrumCommands::Scan { stop_gap, scan_options, @@ -248,12 +248,6 @@ fn main() -> anyhow::Result<()> { } }; - let now = std::time::UNIX_EPOCH - .elapsed() - .expect("must get time") - .as_secs(); - graph_update.update_last_seen_unconfirmed(now); - let db_changeset = { let mut chain = chain.lock().unwrap(); let mut graph = graph.lock().unwrap(); diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index d188eab76..d4692e35c 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -166,14 +166,10 @@ fn main() -> anyhow::Result<()> { // is reached. It returns a `TxGraph` update (`graph_update`) and a structure that // represents the last active spk derivation indices of keychains // (`keychain_indices_update`). - let mut update = client + let update = client .full_scan(request, *stop_gap, scan_options.parallel_requests) .context("scanning for transactions")?; - // We want to keep track of the latest time a transaction was seen unconfirmed. - let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - update.graph_update.update_last_seen_unconfirmed(now); - let mut graph = graph.lock().expect("mutex must not be poisoned"); let mut chain = chain.lock().expect("mutex must not be poisoned"); // Because we did a stop gap based scan we are likely to have some updates to our @@ -265,11 +261,7 @@ fn main() -> anyhow::Result<()> { } } - let mut update = client.sync(request, scan_options.parallel_requests)?; - - // Update last seen unconfirmed - let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - update.graph_update.update_last_seen_unconfirmed(now); + let update = client.sync(request, scan_options.parallel_requests)?; ( chain diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index 4cc698a00..c05184052 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -64,10 +64,7 @@ fn main() -> Result<(), anyhow::Error> { } }); - let mut update = client.full_scan(request, STOP_GAP, BATCH_SIZE, false)?; - - let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - update.graph_update.update_last_seen_unconfirmed(now); + let update = client.full_scan(request, STOP_GAP, BATCH_SIZE, false)?; println!(); diff --git a/example-crates/wallet_esplora_async/src/main.rs b/example-crates/wallet_esplora_async/src/main.rs index d4dae1f3f..6fd215dff 100644 --- a/example-crates/wallet_esplora_async/src/main.rs +++ b/example-crates/wallet_esplora_async/src/main.rs @@ -57,11 +57,9 @@ async fn main() -> Result<(), anyhow::Error> { } }); - let mut update = client + let update = client .full_scan(request, STOP_GAP, PARALLEL_REQUESTS) .await?; - let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - update.graph_update.update_last_seen_unconfirmed(now); wallet.apply_update(update)?; wallet.persist(&mut conn)?; diff --git a/example-crates/wallet_esplora_blocking/src/main.rs b/example-crates/wallet_esplora_blocking/src/main.rs index 9f79d6bf6..45e4685b7 100644 --- a/example-crates/wallet_esplora_blocking/src/main.rs +++ b/example-crates/wallet_esplora_blocking/src/main.rs @@ -59,9 +59,7 @@ fn main() -> Result<(), anyhow::Error> { } }); - let mut update = client.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)?; - let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - update.graph_update.update_last_seen_unconfirmed(now); + let update = client.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)?; wallet.apply_update(update)?; if let Some(changeset) = wallet.take_staged() { From 60b14f025404d7282e6d4cef5e1151f42ac37a5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 23 Aug 2024 11:50:09 +0000 Subject: [PATCH 5/6] revert(chain)!: rm `TxGraph::update_last_seen_unconfirmed` This is no longer needed as `TxGraph::apply_update` now automatically adds `seen_at` timestamps for unanchored transactions. --- crates/chain/src/tx_graph.rs | 65 +---------------------------- crates/chain/tests/test_tx_graph.rs | 36 ---------------- 2 files changed, 1 insertion(+), 100 deletions(-) diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 6da4d8afd..e953580ce 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -671,11 +671,7 @@ impl TxGraph { /// Inserts the given `seen_at` for `txid` into [`TxGraph`]. /// - /// Note that [`TxGraph`] only keeps track of the latest `seen_at`. To batch - /// update all unconfirmed transactions with the latest `seen_at`, see - /// [`update_last_seen_unconfirmed`]. - /// - /// [`update_last_seen_unconfirmed`]: Self::update_last_seen_unconfirmed + /// Note that [`TxGraph`] only keeps track of the latest `seen_at`. pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet { let mut changeset = ChangeSet::::default(); let last_seen = self.last_seen.entry(txid).or_default(); @@ -686,65 +682,6 @@ impl TxGraph { changeset } - /// Update the last seen time for all unconfirmed transactions. - /// - /// This method updates the last seen unconfirmed time for this [`TxGraph`] by inserting - /// the given `seen_at` for every transaction not yet anchored to a confirmed block, - /// and returns the [`ChangeSet`] after applying all updates to `self`. - /// - /// This is useful for keeping track of the latest time a transaction was seen - /// unconfirmed, which is important for evaluating transaction conflicts in the same - /// [`TxGraph`]. For details of how [`TxGraph`] resolves conflicts, see the docs for - /// [`try_get_chain_position`]. - /// - /// A normal use of this method is to call it with the current system time. Although - /// block headers contain a timestamp, using the header time would be less effective - /// at tracking mempool transactions, because it can drift from actual clock time, plus - /// we may want to update a transaction's last seen time repeatedly between blocks. - /// - /// # Example - /// - /// ```rust - /// # use bdk_chain::example_utils::*; - /// # use std::time::UNIX_EPOCH; - /// # let tx = tx_from_hex(RAW_TX_1); - /// # let mut tx_graph = bdk_chain::TxGraph::<()>::new([tx]); - /// let now = std::time::SystemTime::now() - /// .duration_since(UNIX_EPOCH) - /// .expect("valid duration") - /// .as_secs(); - /// let changeset = tx_graph.update_last_seen_unconfirmed(now); - /// assert!(!changeset.last_seen.is_empty()); - /// ``` - /// - /// Note that [`TxGraph`] only keeps track of the latest `seen_at`, so the given time must - /// by strictly greater than what is currently stored for a transaction to have an effect. - /// To insert a last seen time for a single txid, see [`insert_seen_at`]. - /// - /// [`insert_seen_at`]: Self::insert_seen_at - /// [`try_get_chain_position`]: Self::try_get_chain_position - pub fn update_last_seen_unconfirmed(&mut self, seen_at: u64) -> ChangeSet { - let mut changeset = ChangeSet::default(); - let unanchored_txs: Vec = self - .txs - .iter() - .filter_map( - |(&txid, (_, anchors))| { - if anchors.is_empty() { - Some(txid) - } else { - None - } - }, - ) - .collect(); - - for txid in unanchored_txs { - changeset.merge(self.insert_seen_at(txid, seen_at)); - } - changeset - } - /// Extends this graph with the given `update`. /// /// The returned [`ChangeSet`] is the set difference between `update` and `self` (transactions that diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index c6399f53b..ba8a00031 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -1013,42 +1013,6 @@ fn test_changeset_last_seen_merge() { } } -#[test] -fn update_last_seen_unconfirmed() { - let mut graph = TxGraph::<()>::default(); - let tx = new_tx(0); - let txid = tx.compute_txid(); - - // insert a new tx - // initially we have a last_seen of None and no anchors - let _ = graph.insert_tx(tx); - let tx = graph.full_txs().next().unwrap(); - assert_eq!(tx.last_seen_unconfirmed, None); - assert!(tx.anchors.is_empty()); - - // higher timestamp should update last seen - let changeset = graph.update_last_seen_unconfirmed(2); - assert_eq!(changeset.last_seen.get(&txid).unwrap(), &2); - - // lower timestamp has no effect - let changeset = graph.update_last_seen_unconfirmed(1); - assert!(changeset.last_seen.is_empty()); - - // once anchored, last seen is not updated - let _ = graph.insert_anchor(txid, ()); - let changeset = graph.update_last_seen_unconfirmed(4); - assert!(changeset.is_empty()); - assert_eq!( - graph - .full_txs() - .next() - .unwrap() - .last_seen_unconfirmed - .unwrap(), - 2 - ); -} - #[test] fn transactions_inserted_into_tx_graph_are_not_canonical_until_they_have_an_anchor_in_best_chain() { let txs = vec![new_tx(0), new_tx(1)]; From ccb8c796b259ae348cb8b8ef35b8c60e5621ccb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 23 Aug 2024 13:36:54 +0000 Subject: [PATCH 6/6] test(chain): `TxGraph` and `tx_graph::Update` conversion tests --- crates/chain/tests/test_tx_graph.rs | 127 ++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index ba8a00031..df5e6a621 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -1136,3 +1136,130 @@ fn call_map_anchors_with_non_deterministic_anchor() { ] ); } + +/// Tests `From` impls for conversion between [`TxGraph`] and [`tx_graph::Update`]. +#[test] +fn tx_graph_update_conversion() { + use tx_graph::Update; + + type TestCase = (&'static str, Update); + + fn make_tx(v: i32) -> Transaction { + Transaction { + version: transaction::Version(v), + lock_time: absolute::LockTime::ZERO, + input: vec![], + output: vec![], + } + } + + fn make_txout(a: u64) -> TxOut { + TxOut { + value: Amount::from_sat(a), + script_pubkey: ScriptBuf::default(), + } + } + + let test_cases: &[TestCase] = &[ + ("empty_update", Update::default()), + ( + "single_tx", + Update { + txs: vec![make_tx(0).into()], + ..Default::default() + }, + ), + ( + "two_txs", + Update { + txs: vec![make_tx(0).into(), make_tx(1).into()], + ..Default::default() + }, + ), + ( + "with_floating_txouts", + Update { + txs: vec![make_tx(0).into(), make_tx(1).into()], + txouts: [ + (OutPoint::new(h!("a"), 0), make_txout(0)), + (OutPoint::new(h!("a"), 1), make_txout(1)), + (OutPoint::new(h!("b"), 0), make_txout(2)), + ] + .into(), + ..Default::default() + }, + ), + ( + "with_anchors", + Update { + txs: vec![make_tx(0).into(), make_tx(1).into()], + txouts: [ + (OutPoint::new(h!("a"), 0), make_txout(0)), + (OutPoint::new(h!("a"), 1), make_txout(1)), + (OutPoint::new(h!("b"), 0), make_txout(2)), + ] + .into(), + anchors: [ + (ConfirmationBlockTime::default(), h!("a")), + (ConfirmationBlockTime::default(), h!("b")), + ] + .into(), + ..Default::default() + }, + ), + ( + "with_seen_ats", + Update { + txs: vec![make_tx(0).into(), make_tx(1).into()], + txouts: [ + (OutPoint::new(h!("a"), 0), make_txout(0)), + (OutPoint::new(h!("a"), 1), make_txout(1)), + (OutPoint::new(h!("d"), 0), make_txout(2)), + ] + .into(), + anchors: [ + (ConfirmationBlockTime::default(), h!("a")), + (ConfirmationBlockTime::default(), h!("b")), + ] + .into(), + seen_ats: [(h!("c"), 12346)].into_iter().collect(), + }, + ), + ]; + + for (test_name, update) in test_cases { + let mut tx_graph = TxGraph::::default(); + let _ = tx_graph.apply_update_at(update.clone(), None); + let update_from_tx_graph: Update = tx_graph.into(); + + assert_eq!( + update + .txs + .iter() + .map(|tx| tx.compute_txid()) + .collect::>(), + update_from_tx_graph + .txs + .iter() + .map(|tx| tx.compute_txid()) + .collect::>(), + "{}: txs do not match", + test_name + ); + assert_eq!( + update.txouts, update_from_tx_graph.txouts, + "{}: txouts do not match", + test_name + ); + assert_eq!( + update.anchors, update_from_tx_graph.anchors, + "{}: anchors do not match", + test_name + ); + assert_eq!( + update.seen_ats, update_from_tx_graph.seen_ats, + "{}: seen_ats do not match", + test_name + ); + } +}