From 7307bb1819c94185d4001e358f4914d410b2c32f Mon Sep 17 00:00:00 2001 From: LLFourn Date: Thu, 11 Jul 2024 16:13:54 +1000 Subject: [PATCH] =?UTF-8?q?refactor(chain)!=20remove=20IndexedTxGraph=20?= =?UTF-8?q?=F0=9F=97=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit in favour of adding a type parameter to TxGraph. When the second type parameter `X: Indexer` is set then TxGraph behaves like `IndexedTxGraph` used to. I reworked the internals of `TxGraph` as I thought things were a bit convoluted. --- crates/bitcoind_rpc/tests/test_emitter.rs | 40 +- crates/chain/src/changeset.rs | 24 +- crates/chain/src/indexed_tx_graph.rs | 338 ---------- crates/chain/src/indexer.rs | 25 +- crates/chain/src/lib.rs | 2 - crates/chain/src/tx_graph.rs | 615 ++++++++++++------ crates/chain/tests/test_indexed_tx_graph.rs | 65 +- crates/chain/tests/test_tx_graph.rs | 26 +- crates/electrum/src/bdk_electrum_client.rs | 2 +- crates/electrum/tests/test_electrum.rs | 21 +- crates/sqlite/src/store.rs | 86 +-- crates/wallet/src/wallet/mod.rs | 198 +++--- .../example_bitcoind_rpc_polling/src/main.rs | 18 +- example-crates/example_cli/src/lib.rs | 48 +- example-crates/example_electrum/src/main.rs | 278 ++++---- example-crates/example_esplora/src/main.rs | 48 +- 16 files changed, 820 insertions(+), 1014 deletions(-) delete mode 100644 crates/chain/src/indexed_tx_graph.rs diff --git a/crates/bitcoind_rpc/tests/test_emitter.rs b/crates/bitcoind_rpc/tests/test_emitter.rs index d7c8b60f7..d5b414502 100644 --- a/crates/bitcoind_rpc/tests/test_emitter.rs +++ b/crates/bitcoind_rpc/tests/test_emitter.rs @@ -4,7 +4,8 @@ use bdk_bitcoind_rpc::Emitter; use bdk_chain::{ bitcoin::{Address, Amount, Txid}, local_chain::{CheckPoint, LocalChain}, - Balance, BlockId, IndexedTxGraph, Merge, SpkTxOutIndex, + tx_graph::TxGraph, + Balance, BlockId, Merge, SpkTxOutIndex, }; use bdk_testenv::{anyhow, TestEnv}; use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash}; @@ -149,7 +150,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> { println!("mined blocks!"); let (mut chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?); - let mut indexed_tx_graph = IndexedTxGraph::::new({ + let mut tx_graph = TxGraph::::new({ let mut index = SpkTxOutIndex::::default(); index.insert_spk(0, addr_0.script_pubkey()); index.insert_spk(1, addr_1.script_pubkey()); @@ -162,8 +163,8 @@ fn test_into_tx_graph() -> anyhow::Result<()> { while let Some(emission) = emitter.next_block()? { let height = emission.block_height(); let _ = chain.apply_update(emission.checkpoint)?; - let indexed_additions = indexed_tx_graph.apply_block_relevant(&emission.block, height); - assert!(indexed_additions.is_empty()); + let tx_graph_changeset = tx_graph.apply_block_relevant(&emission.block, height); + assert!(tx_graph_changeset.is_empty()); } // send 3 txs to a tracked address, these txs will be in the mempool @@ -190,10 +191,9 @@ fn test_into_tx_graph() -> anyhow::Result<()> { assert!(emitter.next_block()?.is_none()); let mempool_txs = emitter.mempool()?; - let indexed_additions = indexed_tx_graph.batch_insert_unconfirmed(mempool_txs); + let tx_graph_changeset = tx_graph.batch_insert_unconfirmed(mempool_txs); assert_eq!( - indexed_additions - .graph + tx_graph_changeset .txs .iter() .map(|tx| tx.compute_txid()) @@ -201,7 +201,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> { exp_txids, "changeset should have the 3 mempool transactions", ); - assert!(indexed_additions.graph.anchors.is_empty()); + assert!(tx_graph_changeset.anchors.is_empty()); } // mine a block that confirms the 3 txs @@ -223,10 +223,10 @@ fn test_into_tx_graph() -> anyhow::Result<()> { let emission = emitter.next_block()?.expect("must get mined block"); let height = emission.block_height(); let _ = chain.apply_update(emission.checkpoint)?; - let indexed_additions = indexed_tx_graph.apply_block_relevant(&emission.block, height); - assert!(indexed_additions.graph.txs.is_empty()); - assert!(indexed_additions.graph.txouts.is_empty()); - assert_eq!(indexed_additions.graph.anchors, exp_anchors); + let tx_graph_changeset = tx_graph.apply_block_relevant(&emission.block, height); + assert!(tx_graph_changeset.txs.is_empty()); + assert!(tx_graph_changeset.txouts.is_empty()); + assert_eq!(tx_graph_changeset.anchors, exp_anchors); } Ok(()) @@ -277,18 +277,18 @@ fn ensure_block_emitted_after_reorg_is_at_reorg_height() -> anyhow::Result<()> { fn process_block( recv_chain: &mut LocalChain, - recv_graph: &mut IndexedTxGraph>, + recv_graph: &mut TxGraph>, block: Block, block_height: u32, ) -> anyhow::Result<()> { recv_chain.apply_update(CheckPoint::from_header(&block.header, block_height))?; - let _ = recv_graph.apply_block(block, block_height); + let _ = recv_graph.apply_block(&block, block_height); Ok(()) } fn sync_from_emitter( recv_chain: &mut LocalChain, - recv_graph: &mut IndexedTxGraph>, + recv_graph: &mut TxGraph>, emitter: &mut Emitter, ) -> anyhow::Result<()> where @@ -303,13 +303,11 @@ where fn get_balance( recv_chain: &LocalChain, - recv_graph: &IndexedTxGraph>, + recv_graph: &TxGraph>, ) -> anyhow::Result { let chain_tip = recv_chain.tip().block_id(); - let outpoints = recv_graph.index.outpoints().clone(); - let balance = recv_graph - .graph() - .balance(recv_chain, chain_tip, outpoints, |_, _| true); + let outpoints = recv_graph.indexer.outpoints().clone(); + let balance = recv_graph.balance(recv_chain, chain_tip, outpoints, |_, _| true); Ok(balance) } @@ -341,7 +339,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { // setup receiver let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?); - let mut recv_graph = IndexedTxGraph::::new({ + let mut recv_graph = TxGraph::::new({ let mut recv_index = SpkTxOutIndex::default(); recv_index.insert_spk((), spk_to_track.clone()); recv_index diff --git a/crates/chain/src/changeset.rs b/crates/chain/src/changeset.rs index e5777f46c..d6b3165f4 100644 --- a/crates/chain/src/changeset.rs +++ b/crates/chain/src/changeset.rs @@ -15,9 +15,8 @@ pub struct CombinedChangeSet { /// Changes to the [`LocalChain`](crate::local_chain::LocalChain). pub chain: crate::local_chain::ChangeSet, - /// Changes to [`IndexedTxGraph`](crate::indexed_tx_graph::IndexedTxGraph). - pub indexed_tx_graph: - crate::indexed_tx_graph::ChangeSet>, + /// Changes to [`TxGraph`](crate::tx_graph::TxGraph). + pub tx_graph: crate::tx_graph::ChangeSet>, /// Stores the network type of the transaction data. pub network: Option, } @@ -26,8 +25,8 @@ pub struct CombinedChangeSet { impl core::default::Default for CombinedChangeSet { fn default() -> Self { Self { - chain: core::default::Default::default(), - indexed_tx_graph: core::default::Default::default(), + chain: Default::default(), + tx_graph: Default::default(), network: None, } } @@ -37,7 +36,7 @@ impl core::default::Default for CombinedChangeSet { impl crate::Merge for CombinedChangeSet { fn merge(&mut self, other: Self) { crate::Merge::merge(&mut self.chain, other.chain); - crate::Merge::merge(&mut self.indexed_tx_graph, other.indexed_tx_graph); + crate::Merge::merge(&mut self.tx_graph, other.tx_graph); if other.network.is_some() { debug_assert!( self.network.is_none() || self.network == other.network, @@ -48,7 +47,7 @@ impl crate::Merge for CombinedChangeSet { } fn is_empty(&self) -> bool { - self.chain.is_empty() && self.indexed_tx_graph.is_empty() && self.network.is_none() + self.chain.is_empty() && self.tx_graph.is_empty() && self.network.is_none() } } @@ -63,17 +62,14 @@ impl From for CombinedChangeSet { } #[cfg(feature = "miniscript")] -impl From>> +impl From>> for CombinedChangeSet { fn from( - indexed_tx_graph: crate::indexed_tx_graph::ChangeSet< - A, - crate::indexer::keychain_txout::ChangeSet, - >, + tx_graph: crate::tx_graph::ChangeSet>, ) -> Self { Self { - indexed_tx_graph, + tx_graph, ..Default::default() } } @@ -83,7 +79,7 @@ impl From From> for CombinedChangeSet { fn from(indexer: crate::indexer::keychain_txout::ChangeSet) -> Self { Self { - indexed_tx_graph: crate::indexed_tx_graph::ChangeSet { + tx_graph: crate::tx_graph::ChangeSet { indexer, ..Default::default() }, diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs deleted file mode 100644 index 7181768a1..000000000 --- a/crates/chain/src/indexed_tx_graph.rs +++ /dev/null @@ -1,338 +0,0 @@ -//! Contains the [`IndexedTxGraph`] and associated types. Refer to the -//! [`IndexedTxGraph`] documentation for more. -use alloc::vec::Vec; -use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid}; - -use crate::{ - tx_graph::{self, TxGraph}, - Anchor, AnchorFromBlockPosition, BlockId, Indexer, Merge, -}; - -/// The [`IndexedTxGraph`] combines a [`TxGraph`] and an [`Indexer`] implementation. -/// -/// It ensures that [`TxGraph`] and [`Indexer`] are updated atomically. -#[derive(Debug)] -pub struct IndexedTxGraph { - /// Transaction index. - pub index: I, - graph: TxGraph, -} - -impl Default for IndexedTxGraph { - fn default() -> Self { - Self { - graph: Default::default(), - index: Default::default(), - } - } -} - -impl IndexedTxGraph { - /// Construct a new [`IndexedTxGraph`] with a given `index`. - pub fn new(index: I) -> Self { - Self { - index, - graph: TxGraph::default(), - } - } - - /// Get a reference of the internal transaction graph. - pub fn graph(&self) -> &TxGraph { - &self.graph - } -} - -impl IndexedTxGraph { - /// Applies the [`ChangeSet`] to the [`IndexedTxGraph`]. - pub fn apply_changeset(&mut self, changeset: ChangeSet) { - self.index.apply_changeset(changeset.indexer); - - for tx in &changeset.graph.txs { - self.index.index_tx(tx); - } - for (&outpoint, txout) in &changeset.graph.txouts { - self.index.index_txout(outpoint, txout); - } - - self.graph.apply_changeset(changeset.graph); - } - - /// Determines the [`ChangeSet`] between `self` and an empty [`IndexedTxGraph`]. - pub fn initial_changeset(&self) -> ChangeSet { - let graph = self.graph.initial_changeset(); - let indexer = self.index.initial_changeset(); - ChangeSet { graph, indexer } - } -} - -impl IndexedTxGraph -where - I::ChangeSet: Default + Merge, -{ - fn index_tx_graph_changeset( - &mut self, - tx_graph_changeset: &tx_graph::ChangeSet, - ) -> I::ChangeSet { - let mut changeset = I::ChangeSet::default(); - for added_tx in &tx_graph_changeset.txs { - changeset.merge(self.index.index_tx(added_tx)); - } - for (&added_outpoint, added_txout) in &tx_graph_changeset.txouts { - changeset.merge(self.index.index_txout(added_outpoint, added_txout)); - } - changeset - } - - /// 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 { 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); - let indexer = self.index_tx_graph_changeset(&graph); - ChangeSet { graph, indexer } - } - - /// Insert and index a transaction into the graph. - pub fn insert_tx(&mut self, tx: Transaction) -> ChangeSet { - let graph = self.graph.insert_tx(tx); - let indexer = self.index_tx_graph_changeset(&graph); - ChangeSet { graph, indexer } - } - - /// Insert an `anchor` for a given transaction. - pub fn insert_anchor(&mut self, txid: Txid, anchor: A) -> ChangeSet { - self.graph.insert_anchor(txid, anchor).into() - } - - /// Insert a unix timestamp of when a transaction is seen in the mempool. - /// - /// This is used for transaction conflict resolution in [`TxGraph`] where the transaction with - /// the later last-seen is prioritized. - pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet { - self.graph.insert_seen_at(txid, seen_at).into() - } - - /// Batch insert transactions, filtering out those that are irrelevant. - /// - /// Relevancy is determined by the [`Indexer::is_tx_relevant`] implementation of `I`. Irrelevant - /// transactions in `txs` will be ignored. `txs` do not need to be in topological order. - pub fn batch_insert_relevant<'t>( - &mut self, - txs: impl IntoIterator)>, - ) -> ChangeSet { - // The algorithm below allows for non-topologically ordered transactions by using two loops. - // This is achieved by: - // 1. insert all txs into the index. If they are irrelevant then that's fine it will just - // not store anything about them. - // 2. decide whether to insert them into the graph depending on whether `is_tx_relevant` - // returns true or not. (in a second loop). - let txs = txs.into_iter().collect::>(); - - let mut indexer = I::ChangeSet::default(); - for (tx, _) in &txs { - indexer.merge(self.index.index_tx(tx)); - } - - let mut graph = tx_graph::ChangeSet::default(); - for (tx, anchors) in txs { - if self.index.is_tx_relevant(tx) { - let txid = tx.compute_txid(); - graph.merge(self.graph.insert_tx(tx.clone())); - for anchor in anchors { - graph.merge(self.graph.insert_anchor(txid, anchor)); - } - } - } - - ChangeSet { graph, indexer } - } - - /// Batch insert unconfirmed transactions, filtering out those that are irrelevant. - /// - /// Relevancy is determined by the internal [`Indexer::is_tx_relevant`] implementation of `I`. - /// Irrelevant transactions in `txs` will be ignored. - /// - /// Items of `txs` are tuples containing the transaction and a *last seen* timestamp. The - /// *last seen* communicates when the transaction is last seen in the mempool which is used for - /// conflict-resolution in [`TxGraph`] (refer to [`TxGraph::insert_seen_at`] for details). - pub fn batch_insert_relevant_unconfirmed<'t>( - &mut self, - unconfirmed_txs: impl IntoIterator, - ) -> ChangeSet { - // The algorithm below allows for non-topologically ordered transactions by using two loops. - // This is achieved by: - // 1. insert all txs into the index. If they are irrelevant then that's fine it will just - // not store anything about them. - // 2. decide whether to insert them into the graph depending on whether `is_tx_relevant` - // returns true or not. (in a second loop). - let txs = unconfirmed_txs.into_iter().collect::>(); - - let mut indexer = I::ChangeSet::default(); - for (tx, _) in &txs { - indexer.merge(self.index.index_tx(tx)); - } - - let graph = self.graph.batch_insert_unconfirmed( - txs.into_iter() - .filter(|(tx, _)| self.index.is_tx_relevant(tx)) - .map(|(tx, seen_at)| (tx.clone(), seen_at)), - ); - - ChangeSet { graph, indexer } - } - - /// Batch insert unconfirmed transactions. - /// - /// Items of `txs` are tuples containing the transaction and a *last seen* timestamp. The - /// *last seen* communicates when the transaction is last seen in the mempool which is used for - /// conflict-resolution in [`TxGraph`] (refer to [`TxGraph::insert_seen_at`] for details). - /// - /// To filter out irrelevant transactions, use [`batch_insert_relevant_unconfirmed`] instead. - /// - /// [`batch_insert_relevant_unconfirmed`]: IndexedTxGraph::batch_insert_relevant_unconfirmed - pub fn batch_insert_unconfirmed( - &mut self, - txs: impl IntoIterator, - ) -> ChangeSet { - let graph = self.graph.batch_insert_unconfirmed(txs); - let indexer = self.index_tx_graph_changeset(&graph); - ChangeSet { graph, indexer } - } -} - -/// Methods are available if the anchor (`A`) implements [`AnchorFromBlockPosition`]. -impl IndexedTxGraph -where - I::ChangeSet: Default + Merge, - A: AnchorFromBlockPosition, -{ - /// Batch insert all transactions of the given `block` of `height`, filtering out those that are - /// irrelevant. - /// - /// Each inserted transaction's anchor will be constructed from - /// [`AnchorFromBlockPosition::from_block_position`]. - /// - /// Relevancy is determined by the internal [`Indexer::is_tx_relevant`] implementation of `I`. - /// Irrelevant transactions in `txs` will be ignored. - pub fn apply_block_relevant( - &mut self, - block: &Block, - height: u32, - ) -> ChangeSet { - let block_id = BlockId { - hash: block.block_hash(), - height, - }; - let mut changeset = ChangeSet::::default(); - for (tx_pos, tx) in block.txdata.iter().enumerate() { - changeset.indexer.merge(self.index.index_tx(tx)); - if self.index.is_tx_relevant(tx) { - let txid = tx.compute_txid(); - let anchor = A::from_block_position(block, block_id, tx_pos); - changeset.graph.merge(self.graph.insert_tx(tx.clone())); - changeset - .graph - .merge(self.graph.insert_anchor(txid, anchor)); - } - } - changeset - } - - /// Batch insert all transactions of the given `block` of `height`. - /// - /// Each inserted transaction's anchor will be constructed from - /// [`AnchorFromBlockPosition::from_block_position`]. - /// - /// To only insert relevant transactions, use [`apply_block_relevant`] instead. - /// - /// [`apply_block_relevant`]: IndexedTxGraph::apply_block_relevant - pub fn apply_block(&mut self, block: Block, height: u32) -> ChangeSet { - let block_id = BlockId { - hash: block.block_hash(), - height, - }; - let mut graph = tx_graph::ChangeSet::default(); - for (tx_pos, tx) in block.txdata.iter().enumerate() { - let anchor = A::from_block_position(&block, block_id, tx_pos); - graph.merge(self.graph.insert_anchor(tx.compute_txid(), anchor)); - graph.merge(self.graph.insert_tx(tx.clone())); - } - let indexer = self.index_tx_graph_changeset(&graph); - ChangeSet { graph, indexer } - } -} - -/// Represents changes to an [`IndexedTxGraph`]. -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr( - feature = "serde", - derive(serde::Deserialize, serde::Serialize), - serde( - crate = "serde_crate", - bound( - deserialize = "A: Ord + serde::Deserialize<'de>, IA: serde::Deserialize<'de>", - serialize = "A: Ord + serde::Serialize, IA: serde::Serialize" - ) - ) -)] -#[must_use] -pub struct ChangeSet { - /// [`TxGraph`] changeset. - pub graph: tx_graph::ChangeSet, - /// [`Indexer`] changeset. - pub indexer: IA, -} - -impl Default for ChangeSet { - fn default() -> Self { - Self { - graph: Default::default(), - indexer: Default::default(), - } - } -} - -impl Merge for ChangeSet { - fn merge(&mut self, other: Self) { - self.graph.merge(other.graph); - self.indexer.merge(other.indexer); - } - - fn is_empty(&self) -> bool { - self.graph.is_empty() && self.indexer.is_empty() - } -} - -impl From> for ChangeSet { - fn from(graph: tx_graph::ChangeSet) -> Self { - Self { - graph, - ..Default::default() - } - } -} - -#[cfg(feature = "miniscript")] -impl From> - for ChangeSet> -{ - fn from(indexer: crate::indexer::keychain_txout::ChangeSet) -> Self { - Self { - graph: Default::default(), - indexer, - } - } -} - -impl AsRef> for IndexedTxGraph { - fn as_ref(&self) -> &TxGraph { - &self.graph - } -} diff --git a/crates/chain/src/indexer.rs b/crates/chain/src/indexer.rs index 22e839815..8e5c76835 100644 --- a/crates/chain/src/indexer.rs +++ b/crates/chain/src/indexer.rs @@ -8,13 +8,14 @@ pub mod spk_txout; /// Utilities for indexing transaction data. /// -/// Types which implement this trait can be used to construct an [`IndexedTxGraph`]. -/// This trait's methods should rarely be called directly. +/// An `Indexer` is the second type parameter of a [`TxGraph`]. The `TxGraph` calls the +/// indexer whenever new transaction data is inserted into it, allowing the indexer to look at the +/// new data and mutate its state. /// -/// [`IndexedTxGraph`]: crate::IndexedTxGraph +/// [`TxGraph`]: crate::TxGraph pub trait Indexer { /// The resultant "changeset" when new transaction data is indexed. - type ChangeSet; + type ChangeSet: crate::Merge + Clone; /// Scan and index the given `outpoint` and `txout`. fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet; @@ -31,3 +32,19 @@ pub trait Indexer { /// Determines whether the transaction should be included in the index. fn is_tx_relevant(&self, tx: &Transaction) -> bool; } + +impl Indexer for () { + type ChangeSet = (); + + fn index_txout(&mut self, _outpoint: OutPoint, _txout: &TxOut) -> Self::ChangeSet {} + + fn index_tx(&mut self, _tx: &Transaction) -> Self::ChangeSet {} + + fn apply_changeset(&mut self, _changeset: Self::ChangeSet) {} + + fn initial_changeset(&self) -> Self::ChangeSet {} + + fn is_tx_relevant(&self, _tx: &Transaction) -> bool { + false + } +} diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index ec0c61a35..3f9566914 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -25,8 +25,6 @@ mod balance; pub use balance::*; mod chain_data; pub use chain_data::*; -pub mod indexed_tx_graph; -pub use indexed_tx_graph::IndexedTxGraph; pub mod indexer; pub use indexer::spk_txout::*; pub use indexer::Indexer; diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 8c11e737a..b71ae5cd6 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -1,11 +1,9 @@ -//! Module for structures that store and traverse transactions. -//! -//! [`TxGraph`] contains transactions and indexes them so you can easily traverse the graph of -//! those transactions. `TxGraph` is *monotone* in that you can always insert a transaction -- it -//! does not care whether that transaction is in the current best chain or whether it conflicts with -//! any of the existing transactions or what order you insert the transactions. This means that you -//! can always combine two [`TxGraph`]s together, without resulting in inconsistencies. Furthermore, -//! there is currently no way to delete a transaction. +//! The [`TxGraph`] stores and indexes transactions so you can easily traverse the resulting graph. The +//! nodes are transactions and the edges are spends. `TxGraph` is *monotone* in that you can always +//! insert a transaction -- it does not care whether that transaction is in the current best chain +//! or whether it conflicts with any of the existing transactions or what order you insert the +//! transactions. This means that you can always combine two [`TxGraph`]s together, without +//! resulting in inconsistencies. Furthermore, there is currently no way to delete a transaction. //! //! Transactions can be either whole or partial (i.e., transactions for which we only know some //! outputs, which we usually call "floating outputs"; these are usually inserted using the @@ -37,6 +35,8 @@ //! //! # Generics //! +//! ## Anchors +//! //! Anchors are represented as generics within `TxGraph`. To make use of all functionality of the //! `TxGraph`, anchors (`A`) should implement [`Anchor`]. //! @@ -47,6 +47,14 @@ //! the transaction is contained in. Note that a transaction can be contained in multiple //! conflicting blocks (by nature of the Bitcoin network). //! +//! ### Indexer +//! +//! You can add your own [`Indexer`] to the transaction graph which will be notified whenver new +//! transaction data is attached to the graph. Usually this is instantiated with indexers like +//! [`KeychainTxOutIndex`] to keep track of transaction outputs owned by a output descriptor. +//! +//! # Examples +//! //! ``` //! # use bdk_chain::BlockId; //! # use bdk_chain::tx_graph::TxGraph; @@ -75,21 +83,25 @@ //! # use bitcoin::Transaction; //! # 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 tx_graph: TxGraph = Default::default(); +//! let mut update: TxGraph = Default::default(); +//! update.insert_tx(tx_a); +//! update.insert_tx(tx_b); //! //! // apply the update graph -//! let changeset = graph.apply_update(update.clone()); +//! let changeset = tx_graph.apply_update(update.clone()); //! //! // if we apply it again, the resulting changeset will be empty -//! let changeset = graph.apply_update(update); +//! let changeset = tx_graph.apply_update(update); //! assert!(changeset.is_empty()); //! ``` //! [`try_get_chain_position`]: TxGraph::try_get_chain_position //! [`insert_txout`]: TxGraph::insert_txout +//! [`KeychainTxOutIndex`]: crate::indexer::keychain_txout::KeychainTxOutIndex use crate::{ - collections::*, Anchor, Balance, BlockId, ChainOracle, ChainPosition, FullTxOut, Merge, + collections::*, Anchor, AnchorFromBlockPosition, Balance, BlockId, ChainOracle, ChainPosition, + FullTxOut, Indexer, Merge, }; use alloc::collections::vec_deque::VecDeque; use alloc::sync::Arc; @@ -107,26 +119,36 @@ use core::{ /// /// [module-level documentation]: crate::tx_graph #[derive(Clone, Debug, PartialEq)] -pub struct TxGraph { +pub struct TxGraph { // all transactions that the graph is aware of in format: `(tx_node, tx_anchors)` - txs: HashMap)>, + txs: HashMap, spends: BTreeMap>, - anchors: BTreeSet<(A, Txid)>, + anchors: HashMap>, + rev_anchors: BTreeSet<(A, Txid)>, last_seen: HashMap, - // This atrocity exists so that `TxGraph::outspends()` can return a reference. - // FIXME: This can be removed once `HashSet::new` is a const fn. + // These atrocities exist so that `TxGraph::outspends()` can return a reference. empty_outspends: HashSet, + empty_anchors: BTreeSet, + /// Indexer. This is called to index all transactions and outpoints added to the transaction + /// graph. + pub indexer: X, } -impl Default for TxGraph { +impl Default for TxGraph +where + I: Default, +{ fn default() -> Self { Self { txs: Default::default(), spends: Default::default(), anchors: Default::default(), + rev_anchors: Default::default(), last_seen: Default::default(), empty_outspends: Default::default(), + empty_anchors: Default::default(), + indexer: Default::default(), } } } @@ -206,12 +228,12 @@ impl fmt::Display for CalculateFeeError { #[cfg(feature = "std")] impl std::error::Error for CalculateFeeError {} -impl TxGraph { +impl TxGraph { /// Iterate over all tx outputs known by [`TxGraph`]. /// /// This includes txouts of both full transactions as well as floating transactions. pub fn all_txouts(&self) -> impl Iterator { - self.txs.iter().flat_map(|(txid, (tx, _))| match tx { + self.txs.iter().flat_map(|(txid, tx)| match tx { TxNodeInternal::Whole(tx) => tx .as_ref() .output @@ -233,7 +255,7 @@ impl TxGraph { pub fn floating_txouts(&self) -> impl Iterator { self.txs .iter() - .filter_map(|(txid, (tx_node, _))| match tx_node { + .filter_map(|(txid, tx_node)| match tx_node { TxNodeInternal::Whole(_) => None, TxNodeInternal::Partial(txouts) => Some( txouts @@ -246,17 +268,15 @@ impl TxGraph { /// Iterate over all full transactions in the graph. pub fn full_txs(&self) -> impl Iterator, A>> { - self.txs - .iter() - .filter_map(|(&txid, (tx, anchors))| match tx { - TxNodeInternal::Whole(tx) => Some(TxNode { - txid, - tx: tx.clone(), - anchors, - last_seen_unconfirmed: self.last_seen.get(&txid).copied(), - }), - TxNodeInternal::Partial(_) => None, - }) + self.txs.iter().filter_map(|(&txid, tx)| match tx { + TxNodeInternal::Whole(tx) => Some(TxNode { + txid, + tx: tx.clone(), + anchors: self.get_anchors(txid), + last_seen_unconfirmed: self.last_seen.get(&txid).copied(), + }), + TxNodeInternal::Partial(_) => None, + }) } /// Iterate over graph transactions with no anchors or last-seen. @@ -284,10 +304,10 @@ impl TxGraph { /// Get a transaction node by txid. This only returns `Some` for full transactions. pub fn get_tx_node(&self, txid: Txid) -> Option, A>> { match &self.txs.get(&txid)? { - (TxNodeInternal::Whole(tx), anchors) => Some(TxNode { + TxNodeInternal::Whole(tx) => Some(TxNode { txid, tx: tx.clone(), - anchors, + anchors: self.get_anchors(txid), last_seen_unconfirmed: self.last_seen.get(&txid).copied(), }), _ => None, @@ -296,17 +316,22 @@ impl TxGraph { /// Obtains a single tx output (if any) at the specified outpoint. pub fn get_txout(&self, outpoint: OutPoint) -> Option<&TxOut> { - match &self.txs.get(&outpoint.txid)?.0 { + match &self.txs.get(&outpoint.txid)? { TxNodeInternal::Whole(tx) => tx.as_ref().output.get(outpoint.vout as usize), TxNodeInternal::Partial(txouts) => txouts.get(&outpoint.vout), } } + /// Get the anchors associated with a transaction. + pub fn get_anchors(&self, txid: Txid) -> &BTreeSet { + self.anchors.get(&txid).unwrap_or(&self.empty_anchors) + } + /// Returns known outputs of a given `txid`. /// /// Returns a [`BTreeMap`] of vout to output of the provided `txid`. pub fn tx_outputs(&self, txid: Txid) -> Option> { - Some(match &self.txs.get(&txid)?.0 { + Some(match &self.txs.get(&txid)? { TxNodeInternal::Whole(tx) => tx .as_ref() .output @@ -391,7 +416,7 @@ impl TxGraph { } } -impl TxGraph { +impl TxGraph { /// Creates an iterator that filters and maps ancestor transactions. /// /// The iterator starts with the ancestors of the supplied `tx` (ancestor transactions of `tx` @@ -405,7 +430,7 @@ impl TxGraph { /// /// The supplied closure returns an `Option`, allowing the caller to map each `Transaction` /// it visits and decide whether to visit ancestors. - pub fn walk_ancestors<'g, T, F, O>(&'g self, tx: T, walk_map: F) -> TxAncestors<'g, A, F> + pub fn walk_ancestors<'g, T, F, O>(&'g self, tx: T, walk_map: F) -> TxAncestors<'g, A, F, X> where T: Into>, F: FnMut(usize, Arc) -> Option + 'g, @@ -423,7 +448,7 @@ impl TxGraph { /// /// The supplied closure returns an `Option`, allowing the caller to map each node it visits /// and decide whether to visit descendants. - pub fn walk_descendants<'g, F, O>(&'g self, txid: Txid, walk_map: F) -> TxDescendants + pub fn walk_descendants<'g, F, O>(&'g self, txid: Txid, walk_map: F) -> TxDescendants where F: FnMut(usize, Txid) -> Option + 'g, { @@ -431,7 +456,7 @@ impl TxGraph { } } -impl TxGraph { +impl TxGraph { /// Creates an iterator that both filters and maps conflicting transactions (this includes /// descendants of directly-conflicting transactions, which are also considered conflicts). /// @@ -440,7 +465,7 @@ impl TxGraph { &'g self, tx: &'g Transaction, walk_map: F, - ) -> TxDescendants + ) -> TxDescendants where F: FnMut(usize, Txid) -> Option + 'g, { @@ -470,7 +495,7 @@ impl TxGraph { /// Get all transaction anchors known by [`TxGraph`]. pub fn all_anchors(&self) -> &BTreeSet<(A, Txid)> { - &self.anchors + &self.rev_anchors } /// Whether the graph has any transactions or outputs in it. @@ -479,27 +504,47 @@ impl TxGraph { } } -impl TxGraph { +impl TxGraph { /// Transform the [`TxGraph`] to have [`Anchor`]s of another type. /// /// This takes in a closure of signature `FnMut(A) -> A2` which is called for each [`Anchor`] to /// transform it. - pub fn map_anchors(self, f: F) -> TxGraph + pub fn map_anchors(self, mut f: F) -> TxGraph where F: FnMut(A) -> A2, { - let mut new_graph = TxGraph::::default(); - new_graph.apply_changeset(self.initial_changeset().map_anchors(f)); - new_graph + let mut new_tx_graph = TxGraph:: { + txs: self.txs, + spends: self.spends, + rev_anchors: Default::default(), + anchors: Default::default(), + last_seen: self.last_seen, + empty_outspends: self.empty_outspends, + empty_anchors: Default::default(), + indexer: self.indexer, + }; + + let new_anchors = self + .rev_anchors + .into_iter() + .map(|(anchor, txid)| (txid, f(anchor))); + + for (txid, new_anchor) in new_anchors { + let _ = new_tx_graph.insert_anchor(txid, new_anchor); + } + + new_tx_graph } /// Construct a new [`TxGraph`] from a list of transactions. - pub fn new(txs: impl IntoIterator) -> Self { - let mut new = Self::default(); - for tx in txs.into_iter() { - let _ = new.insert_tx(tx); + pub fn new(indexer: X) -> Self + where + X: Default, + { + Self { + indexer, + ..Default::default() } - new } /// Inserts the given [`TxOut`] at [`OutPoint`]. @@ -511,29 +556,61 @@ impl TxGraph { /// the `outpoint`) already existed in `self`. /// /// [`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) + pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet { + let node = self.txs.entry(outpoint.txid).or_default(); + let mut changeset = match 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. + ChangeSet::default() + } + TxNodeInternal::Partial(partial_tx) => { + partial_tx.insert(outpoint.vout, txout.clone()); + ChangeSet { + txouts: [(outpoint, txout)].into(), + ..Default::default() + } + } + }; + self.index_changeset(&mut changeset); + 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 { + 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 txid = tx.compute_txid(); + let new_node = TxNodeInternal::Whole(tx.clone()); + let mut changeset = ChangeSet::default(); + let existing_node = self.txs.insert(txid, new_node.clone()); + if existing_node != Some(new_node) { + for input in &tx.input { + if input.previous_output.is_null() { + // this means the tx is coinbase so there is no previous output + continue; + } + let spends = self.spends.entry(input.previous_output).or_default(); + spends.insert(txid); + } + changeset.txs = [tx].into(); + } + self.index_changeset(&mut changeset); + changeset + } + + /// Inserts a bunch of transactions at once and returns the [`ChangeSet`]. + pub fn batch_insert_tx>>( + &mut self, + txs: impl IntoIterator, + ) -> ChangeSet { + let mut changeset = ChangeSet::::default(); + for tx in txs { + changeset.merge(self.insert_tx(tx)); + } + changeset } /// Batch insert unconfirmed transactions. @@ -544,8 +621,8 @@ impl TxGraph { pub fn batch_insert_unconfirmed( &mut self, txs: impl IntoIterator, - ) -> ChangeSet { - let mut changeset = ChangeSet::::default(); + ) -> ChangeSet { + let mut changeset = ChangeSet::::default(); for (tx, seen_at) in txs { changeset.merge(self.insert_seen_at(tx.compute_txid(), seen_at)); changeset.merge(self.insert_tx(tx)); @@ -553,14 +630,95 @@ impl TxGraph { changeset } + /// Batch insert transactions, filtering out those that are irrelevant. + /// + /// Relevancy is determined by the [`Indexer::is_tx_relevant`] implementation of `I`. Irrelevant + /// transactions in `txs` will be ignored. `txs` do not need to be in topological order. + /// + /// The second item in the input tuple is a list of *anchors* for the transaction. This is + /// usually a single item or none at all. Therefore using an `Option` as the type is the most + /// ergonomic approach. + pub fn batch_insert_relevant<'t>( + &mut self, + txs: impl IntoIterator)>, + ) -> ChangeSet { + // The algorithm below allows for non-topologically ordered transactions by using two loops. + // This is achieved by: + // 1. insert all txs into the index. If they are irrelevant then that's fine it will just + // not store anything about them. + // 2. decide whether to insert them into the graph depending on whether `is_tx_relevant` + // returns true or not. (in a second loop). + let txs = txs.into_iter().collect::>(); + // returns true or not. (in a second loop). + let mut changeset = ChangeSet::::default(); + for (tx, _) in &txs { + changeset.indexer.merge(self.indexer.index_tx(tx)); + } + + let mut graph = ChangeSet::default(); + for (tx, anchors) in txs { + if self.indexer.is_tx_relevant(tx) { + let txid = tx.compute_txid(); + changeset.merge(self.insert_tx(tx.clone())); + for anchor in anchors { + graph.merge(self.insert_anchor(txid, anchor)); + } + } + } + + changeset + } + + /// Batch insert unconfirmed transactions, filtering out those that are irrelevant. + /// + /// Relevancy is determined by the internal [`Indexer::is_tx_relevant`] implementation of `I`. + /// Irrelevant transactions in `txs` will be ignored. + /// + /// Items of `txs` are tuples containing the transaction and a *last seen* timestamp. The + /// *last seen* communicates when the transaction is last seen in the mempool which is used for + /// conflict-resolution in [`TxGraph`] (refer to [`TxGraph::insert_seen_at`] for details). + pub fn batch_insert_relevant_unconfirmed<'t>( + &mut self, + unconfirmed_txs: impl IntoIterator, + ) -> ChangeSet { + // The algorithm below allows for non-topologically ordered transactions by using two loops. + // This is achieved by: + // 1. insert all txs into the index. If they are irrelevant then that's fine it will just + // not store anything about them. + // 2. decide whether to insert them into the graph depending on whether `is_tx_relevant` + // returns true or not. (in a second loop). + let mut changeset = ChangeSet::::default(); + let txs = unconfirmed_txs.into_iter().collect::>(); + for (tx, _) in &txs { + changeset.indexer.merge(self.indexer.index_tx(tx)); + } + + let relevant = txs + .into_iter() + .filter(|(tx, _)| self.indexer.is_tx_relevant(tx)) + .map(|(tx, seen_at)| (tx.clone(), seen_at)) + .collect::>(); + + changeset.merge(self.batch_insert_unconfirmed(relevant)); + + changeset + } + /// Inserts the given `anchor` into [`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) + pub fn insert_anchor(&mut self, txid: Txid, anchor: A) -> ChangeSet { + if self.rev_anchors.insert((anchor.clone(), txid)) { + let anchors = self.anchors.entry(txid).or_default(); + anchors.insert(anchor.clone()); + ChangeSet { + anchors: [(anchor, txid)].into(), + ..Default::default() + } + } else { + Default::default() + } } /// Inserts the given `seen_at` for `txid` into [`TxGraph`]. @@ -570,10 +728,16 @@ impl TxGraph { /// [`update_last_seen_unconfirmed`]. /// /// [`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) + pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet { + if self.last_seen.get(&txid) < Some(&seen_at) { + self.last_seen.insert(txid, seen_at); + ChangeSet { + last_seen: [(txid, seen_at)].into(), + ..Default::default() + } + } else { + Default::default() + } } /// Update the last seen time for all unconfirmed transactions. @@ -598,7 +762,8 @@ impl TxGraph { /// # 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 mut tx_graph: bdk_chain::TxGraph = Default::default(); + /// # tx_graph.insert_tx(tx); /// let now = std::time::SystemTime::now() /// .duration_since(UNIX_EPOCH) /// .expect("valid duration") @@ -613,21 +778,14 @@ impl TxGraph { /// /// [`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 { + pub fn update_last_seen_unconfirmed(&mut self, seen_at: u64) -> ChangeSet { let mut changeset = ChangeSet::default(); - let unanchored_txs: Vec = self + let unanchored_txs = self .txs - .iter() - .filter_map( - |(&txid, (_, anchors))| { - if anchors.is_empty() { - Some(txid) - } else { - None - } - }, - ) - .collect(); + .keys() + .cloned() + .filter(|txid| self.get_anchors(*txid).is_empty()) + .collect::>(); for txid in unanchored_txs { changeset.merge(self.insert_seen_at(txid, seen_at)); @@ -635,133 +793,140 @@ impl TxGraph { changeset } + fn index_changeset(&mut self, tx_graph_changeset: &mut ChangeSet) { + let changeset = &mut tx_graph_changeset.indexer; + for added_tx in &tx_graph_changeset.txs { + changeset.merge(self.indexer.index_tx(added_tx)); + } + for (&added_outpoint, added_txout) in &tx_graph_changeset.txouts { + changeset.merge(self.indexer.index_txout(added_outpoint, added_txout)); + } + } + /// Extends this graph with another so that `self` becomes the union of the two sets of /// transactions. /// /// 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()); + pub fn apply_update(&mut self, update: TxGraph) -> ChangeSet { + let mut changeset = ChangeSet::default(); + for tx in update.full_txs() { + changeset.merge(self.insert_tx(tx.tx)); + } + for (outpoint, txout) in update.floating_txouts() { + changeset.merge(self.insert_txout(outpoint, txout.clone())); + } + + for (anchor, txid) in update.rev_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)); + } + + self.index_changeset(&mut changeset); changeset } /// Determines the [`ChangeSet`] between `self` and an empty [`TxGraph`]. - pub fn initial_changeset(&self) -> ChangeSet { - Self::default().determine_changeset(self.clone()) + pub fn initial_changeset(&self) -> ChangeSet { + ChangeSet { + txs: self.full_txs().map(|tx| tx.tx.clone()).collect(), + txouts: self + .floating_txouts() + .map(|(op, txout)| (op, txout.clone())) + .collect(), + anchors: self.rev_anchors.clone(), + last_seen: self.last_seen.clone().into_iter().collect(), + indexer: self.indexer.initial_changeset(), + } } /// 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(); + pub fn apply_changeset(&mut self, changeset: ChangeSet) { + // It's possible that the changeset contains some state for the indexer to help it index the + // transactions so we do it that first. + self.indexer.apply_changeset(changeset.indexer); - 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; - } + let _ = self.insert_seen_at(txid, new_last_seen); } } - /// Previews the resultant [`ChangeSet`] when [`Self`] is updated against the `update` graph. + /// Batch insert all transactions of the given `block` of `height`, filtering out those that are + /// irrelevant. /// - /// 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())), - ); - } + /// Each inserted transaction's anchor will be constructed from + /// [`AnchorFromBlockPosition::from_block_position`]. + /// + /// Relevancy is determined by the internal [`Indexer::is_tx_relevant`] implementation of `I`. + /// Irrelevant transactions in `txs` will be ignored. + pub fn apply_block_relevant( + &mut self, + block: &bitcoin::Block, + height: u32, + ) -> ChangeSet + where + A: AnchorFromBlockPosition, + { + let block_id = BlockId { + hash: block.block_hash(), + height, + }; + let mut changeset = ChangeSet::::default(); + for (tx_pos, tx) in block.txdata.iter().enumerate() { + changeset.indexer.merge(self.indexer.index_tx(tx)); + if self.indexer.is_tx_relevant(tx) { + let txid = tx.compute_txid(); + let anchor = A::from_block_position(block, block_id, tx_pos); + changeset.merge(self.insert_tx(tx.clone())); + changeset.merge(self.insert_anchor(txid, anchor)); } } + changeset + } - 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); - } + /// Batch insert all transactions of the given `block` of `height`. + /// + /// Each inserted transaction's anchor will be constructed from + /// [`AnchorFromBlockPosition::from_block_position`]. + /// + /// To only insert relevant transactions, use [`apply_block_relevant`] instead. + /// + /// [`apply_block_relevant`]: TxGraph::apply_block_relevant + pub fn apply_block(&mut self, block: &bitcoin::Block, height: u32) -> ChangeSet + where + A: AnchorFromBlockPosition, + { + let block_id = BlockId { + hash: block.block_hash(), + height, + }; + let mut changeset = ChangeSet::default(); + for (tx_pos, tx) in block.txdata.iter().enumerate() { + let anchor = A::from_block_position(block, block_id, tx_pos); + changeset.merge(self.insert_anchor(tx.compute_txid(), anchor)); + changeset.merge(self.insert_tx(tx.clone())); } - - changeset.anchors = update.anchors.difference(&self.anchors).cloned().collect(); - + self.index_changeset(&mut changeset); changeset } } -impl TxGraph { +impl TxGraph { /// Get the position of the transaction in `chain` with tip `chain_tip`. /// /// Chain data is fetched from `chain`, a [`ChainOracle`] implementation. @@ -791,12 +956,12 @@ impl TxGraph { chain_tip: BlockId, txid: Txid, ) -> Result>, C::Error> { - let (tx_node, anchors) = match self.txs.get(&txid) { + let tx_node = match self.txs.get(&txid) { Some(v) => v, None => return Ok(None), }; - for anchor in anchors { + for anchor in self.get_anchors(txid) { match chain.is_block_in_chain(anchor.anchor_block(), chain_tip)? { Some(true) => return Ok(Some(ChainPosition::Confirmed(anchor))), _ => continue, @@ -1231,13 +1396,13 @@ impl TxGraph { serde( crate = "serde_crate", bound( - deserialize = "A: Ord + serde::Deserialize<'de>", - serialize = "A: Ord + serde::Serialize", + deserialize = "A: Ord + serde::Deserialize<'de>, X: serde::Deserialize<'de>", + serialize = "A: Ord + serde::Serialize, X: serde::Serialize", ) ) )] #[must_use] -pub struct ChangeSet { +pub struct ChangeSet { /// Added transactions. pub txs: BTreeSet>, /// Added txouts. @@ -1246,20 +1411,35 @@ pub struct ChangeSet { pub anchors: BTreeSet<(A, Txid)>, /// Added last-seen unix timestamps of transactions. pub last_seen: BTreeMap, + /// [`Indexer`] changes + pub indexer: X, } -impl Default for ChangeSet { +impl Default for ChangeSet { fn default() -> Self { Self { txs: Default::default(), txouts: Default::default(), anchors: Default::default(), last_seen: Default::default(), + indexer: Default::default(), + } + } +} + +impl From for ChangeSet { + fn from(value: X) -> Self { + Self { + indexer: value, + txs: Default::default(), + txouts: Default::default(), + anchors: Default::default(), + last_seen: Default::default(), } } } -impl ChangeSet { +impl ChangeSet { /// Iterates over all outpoints contained within [`ChangeSet`]. pub fn txouts(&self) -> impl Iterator { self.txs @@ -1293,7 +1473,7 @@ impl ChangeSet { } } -impl Merge for ChangeSet { +impl Merge for ChangeSet { fn merge(&mut self, other: Self) { // We use `extend` instead of `BTreeMap::append` due to performance issues with `append`. // Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420 @@ -1309,6 +1489,7 @@ impl Merge for ChangeSet { .filter(|(txid, update_ls)| self.last_seen.get(txid) < Some(update_ls)) .collect::>(), ); + self.indexer.merge(other.indexer); } fn is_empty(&self) -> bool { @@ -1316,15 +1497,16 @@ impl Merge for ChangeSet { && self.txouts.is_empty() && self.anchors.is_empty() && self.last_seen.is_empty() + && self.indexer.is_empty() } } -impl ChangeSet { +impl ChangeSet { /// Transform the [`ChangeSet`] to have [`Anchor`]s of another type. /// /// This takes in a closure of signature `FnMut(A) -> A2` which is called for each [`Anchor`] to /// transform it. - pub fn map_anchors(self, mut f: F) -> ChangeSet + pub fn map_anchors(self, mut f: F) -> ChangeSet where F: FnMut(A) -> A2, { @@ -1335,12 +1517,13 @@ impl ChangeSet { self.anchors.into_iter().map(|(a, txid)| (f(a), txid)), ), last_seen: self.last_seen, + indexer: self.indexer, } } } -impl AsRef> for TxGraph { - fn as_ref(&self) -> &TxGraph { +impl AsRef> for TxGraph { + fn as_ref(&self) -> &TxGraph { self } } @@ -1352,17 +1535,17 @@ impl AsRef> for TxGraph { /// Returned by the [`walk_ancestors`] method of [`TxGraph`]. /// /// [`walk_ancestors`]: TxGraph::walk_ancestors -pub struct TxAncestors<'g, A, F> { - graph: &'g TxGraph, +pub struct TxAncestors<'g, A, F, X> { + graph: &'g TxGraph, visited: HashSet, queue: VecDeque<(usize, Arc)>, filter_map: F, } -impl<'g, A, F> TxAncestors<'g, A, F> { +impl<'g, A, F, X> TxAncestors<'g, A, F, X> { /// Creates a `TxAncestors` that includes the starting `Transaction` when iterating. pub(crate) fn new_include_root( - graph: &'g TxGraph, + graph: &'g TxGraph, tx: impl Into>, filter_map: F, ) -> Self { @@ -1376,7 +1559,7 @@ impl<'g, A, F> TxAncestors<'g, A, F> { /// Creates a `TxAncestors` that excludes the starting `Transaction` when iterating. pub(crate) fn new_exclude_root( - graph: &'g TxGraph, + graph: &'g TxGraph, tx: impl Into>, filter_map: F, ) -> Self { @@ -1394,7 +1577,7 @@ impl<'g, A, F> TxAncestors<'g, A, F> { /// `Transaction`s when iterating. #[allow(unused)] pub(crate) fn from_multiple_include_root( - graph: &'g TxGraph, + graph: &'g TxGraph, txs: I, filter_map: F, ) -> Self @@ -1414,7 +1597,7 @@ impl<'g, A, F> TxAncestors<'g, A, F> { /// `Transaction`s when iterating. #[allow(unused)] pub(crate) fn from_multiple_exclude_root( - graph: &'g TxGraph, + graph: &'g TxGraph, txs: I, filter_map: F, ) -> Self @@ -1446,7 +1629,7 @@ impl<'g, A, F> TxAncestors<'g, A, F> { } } -impl<'g, A, F, O> Iterator for TxAncestors<'g, A, F> +impl<'g, A, F, O, X> Iterator for TxAncestors<'g, A, F, X> where F: FnMut(usize, Arc) -> Option, { @@ -1472,17 +1655,17 @@ where /// Returned by the [`walk_descendants`] method of [`TxGraph`]. /// /// [`walk_descendants`]: TxGraph::walk_descendants -pub struct TxDescendants<'g, A, F> { - graph: &'g TxGraph, +pub struct TxDescendants<'g, A, F, X> { + graph: &'g TxGraph, visited: HashSet, queue: VecDeque<(usize, Txid)>, filter_map: F, } -impl<'g, A, F> TxDescendants<'g, A, F> { +impl<'g, A, F, X> TxDescendants<'g, A, F, X> { /// Creates a `TxDescendants` that includes the starting `txid` when iterating. #[allow(unused)] - pub(crate) fn new_include_root(graph: &'g TxGraph, txid: Txid, filter_map: F) -> Self { + pub(crate) fn new_include_root(graph: &'g TxGraph, txid: Txid, filter_map: F) -> Self { Self { graph, visited: Default::default(), @@ -1492,7 +1675,7 @@ impl<'g, A, F> TxDescendants<'g, A, F> { } /// Creates a `TxDescendants` that excludes the starting `txid` when iterating. - pub(crate) fn new_exclude_root(graph: &'g TxGraph, txid: Txid, filter_map: F) -> Self { + pub(crate) fn new_exclude_root(graph: &'g TxGraph, txid: Txid, filter_map: F) -> Self { let mut descendants = Self { graph, visited: Default::default(), @@ -1506,7 +1689,7 @@ impl<'g, A, F> TxDescendants<'g, A, F> { /// Creates a `TxDescendants` from multiple starting transactions that includes the starting /// `txid`s when iterating. pub(crate) fn from_multiple_include_root( - graph: &'g TxGraph, + graph: &'g TxGraph, txids: I, filter_map: F, ) -> Self @@ -1525,7 +1708,7 @@ impl<'g, A, F> TxDescendants<'g, A, F> { /// `txid`s when iterating. #[allow(unused)] pub(crate) fn from_multiple_exclude_root( - graph: &'g TxGraph, + graph: &'g TxGraph, txids: I, filter_map: F, ) -> Self @@ -1545,7 +1728,7 @@ impl<'g, A, F> TxDescendants<'g, A, F> { } } -impl<'g, A, F> TxDescendants<'g, A, F> { +impl<'g, A, F, X> TxDescendants<'g, A, F, X> { fn populate_queue(&mut self, depth: usize, txid: Txid) { let spend_paths = self .graph @@ -1557,7 +1740,7 @@ impl<'g, A, F> TxDescendants<'g, A, F> { } } -impl<'g, A, F, O> Iterator for TxDescendants<'g, A, F> +impl<'g, A, F, O, X> Iterator for TxDescendants<'g, A, F, X> where F: FnMut(usize, Txid) -> Option, { diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index 01d25c061..482342936 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -7,17 +7,17 @@ use std::{collections::BTreeSet, sync::Arc}; use crate::common::DESCRIPTORS; use bdk_chain::{ - indexed_tx_graph::{self, IndexedTxGraph}, indexer::keychain_txout::KeychainTxOutIndex, local_chain::LocalChain, - tx_graph, Balance, ChainPosition, ConfirmationBlockTime, DescriptorExt, Merge, + tx_graph::{self, TxGraph}, + Balance, ChainPosition, ConfirmationBlockTime, DescriptorExt, Merge, }; use bitcoin::{ secp256k1::Secp256k1, Amount, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut, }; use miniscript::Descriptor; -/// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented +/// Ensure [`TxGraph::insert_relevant_txs`] can successfully index transactions NOT presented /// in topological order. /// /// Given 3 transactions (A, B, C), where A has 2 owned outputs. B and C spends an output each of A. @@ -32,11 +32,10 @@ fn insert_relevant_txs() { let spk_0 = descriptor.at_derivation_index(0).unwrap().script_pubkey(); let spk_1 = descriptor.at_derivation_index(9).unwrap().script_pubkey(); - let mut graph = IndexedTxGraph::>::new( - KeychainTxOutIndex::new(10), - ); + let mut graph = + TxGraph::>::new(KeychainTxOutIndex::new(10)); let _ = graph - .index + .indexer .insert_descriptor((), descriptor.clone()) .unwrap(); @@ -72,15 +71,13 @@ fn insert_relevant_txs() { let txs = [tx_c, tx_b, tx_a]; - let changeset = indexed_tx_graph::ChangeSet { - graph: tx_graph::ChangeSet { - txs: txs.iter().cloned().map(Arc::new).collect(), - ..Default::default() - }, + let changeset = tx_graph::ChangeSet { + txs: txs.iter().cloned().map(Arc::new).collect(), indexer: keychain_txout::ChangeSet { last_revealed: [(descriptor.descriptor_id(), 9_u32)].into(), keychains_added: [].into(), }, + ..Default::default() }; assert_eq!( @@ -89,18 +86,19 @@ fn insert_relevant_txs() { ); // The initial changeset will also contain info about the keychain we added - let initial_changeset = indexed_tx_graph::ChangeSet { - graph: changeset.graph, + let initial_changeset = tx_graph::ChangeSet { + txs: changeset.txs, indexer: keychain_txout::ChangeSet { last_revealed: changeset.indexer.last_revealed, keychains_added: [((), descriptor)].into(), }, + ..Default::default() }; assert_eq!(graph.initial_changeset(), initial_changeset); } -/// Ensure consistency IndexedTxGraph list_* and balance methods. These methods lists +/// Ensure consistency TxGraph list_* and balance methods. These methods lists /// relevant txouts and utxos from the information fetched from a ChainOracle (here a LocalChain). /// /// Test Setup: @@ -133,24 +131,24 @@ fn test_list_owned_txouts() { let local_chain = LocalChain::from_blocks((0..150).map(|i| (i as u32, h!("random"))).collect()) .expect("must have genesis hash"); - // Initiate IndexedTxGraph + // Initiate TxGraph let (desc_1, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), common::DESCRIPTORS[2]).unwrap(); let (desc_2, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), common::DESCRIPTORS[3]).unwrap(); - let mut graph = IndexedTxGraph::>::new( + let mut graph = TxGraph::>::new( KeychainTxOutIndex::new(10), ); assert!(!graph - .index + .indexer .insert_descriptor("keychain_1".into(), desc_1) .unwrap() .is_empty()); assert!(!graph - .index + .indexer .insert_descriptor("keychain_2".into(), desc_2) .unwrap() .is_empty()); @@ -164,7 +162,7 @@ fn test_list_owned_txouts() { // we need to scope here to take immutable reference of the graph for _ in 0..10 { let ((_, script), _) = graph - .index + .indexer .reveal_next_spk(&"keychain_1".to_string()) .unwrap(); // TODO Assert indexes @@ -174,7 +172,7 @@ fn test_list_owned_txouts() { { for _ in 0..10 { let ((_, script), _) = graph - .index + .indexer .reveal_next_spk(&"keychain_2".to_string()) .unwrap(); untrusted_spks.push(script.to_owned()); @@ -261,33 +259,31 @@ fn test_list_owned_txouts() { // A helper lambda to extract and filter data from the graph. let fetch = - |height: u32, graph: &IndexedTxGraph>| { + |height: u32, graph: &TxGraph>| { let chain_tip = local_chain .get(height) .map(|cp| cp.block_id()) .unwrap_or_else(|| panic!("block must exist at {}", height)); let txouts = graph - .graph() .filter_chain_txouts( &local_chain, chain_tip, - graph.index.outpoints().iter().cloned(), + graph.indexer.outpoints().iter().cloned(), ) .collect::>(); let utxos = graph - .graph() .filter_chain_unspents( &local_chain, chain_tip, - graph.index.outpoints().iter().cloned(), + graph.indexer.outpoints().iter().cloned(), ) .collect::>(); - let balance = graph.graph().balance( + let balance = graph.balance( &local_chain, chain_tip, - graph.index.outpoints().iter().cloned(), + graph.indexer.outpoints().iter().cloned(), |_, spk: &Script| trusted_spks.contains(&spk.to_owned()), ); @@ -443,6 +439,7 @@ fn test_list_owned_txouts() { ); // tx3 also gets into confirmed utxo set + // but tx2 is no longer unspent because tx3 spent it assert_eq!( confirmed_utxos_txid, [tx1.compute_txid(), tx3.compute_txid()].into() @@ -521,7 +518,7 @@ fn test_list_owned_txouts() { } } -/// Given a `LocalChain`, `IndexedTxGraph`, and a `Transaction`, when we insert some anchor +/// Given a `LocalChain`, `TxGraph`, and a `Transaction`, when we insert some anchor /// (possibly non-canonical) and/or a last-seen timestamp into the graph, we expect the /// result of `get_chain_position` in these cases: /// @@ -545,7 +542,7 @@ fn test_get_chain_position() { // addr: bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm let spk = ScriptBuf::from_hex("0014c692ecf13534982a9a2834565cbd37add8027140").unwrap(); - let mut graph = IndexedTxGraph::new({ + let mut graph = TxGraph::new({ let mut index = SpkTxOutIndex::default(); let _ = index.insert_spk(0u32, spk.clone()); index @@ -557,12 +554,12 @@ fn test_get_chain_position() { let cp = CheckPoint::from_block_ids(blocks.clone()).unwrap(); let chain = LocalChain::from_tip(cp).unwrap(); - // The test will insert a transaction into the indexed tx graph + // The test will insert a transaction into the tx graph // along with any anchors and timestamps, then check the value // returned by `get_chain_position`. fn run( chain: &LocalChain, - graph: &mut IndexedTxGraph>, + graph: &mut TxGraph>, test: TestCase, ) { let TestCase { @@ -584,9 +581,7 @@ fn test_get_chain_position() { } // check chain position - let res = graph - .graph() - .get_chain_position(chain, chain.tip().block_id(), txid); + let res = graph.get_chain_position(chain, chain.tip().block_id(), txid); assert_eq!( res.map(ChainPosition::cloned), exp_pos, diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 8ddf7f30a..c8a5e1e72 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -102,20 +102,16 @@ fn insert_txouts() { assert_eq!( graph.insert_anchor(outpoint.txid, unconf_anchor), ChangeSet { - txs: [].into(), - txouts: [].into(), anchors: [(unconf_anchor, outpoint.txid)].into(), - last_seen: [].into() + ..Default::default() } ); // 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() + last_seen: [(outpoint.txid, 1000000)].into(), + ..Default::default() } ); } @@ -132,10 +128,8 @@ fn insert_txouts() { 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() + ..Default::default() } ); graph @@ -154,7 +148,8 @@ fn insert_txouts() { (unconf_anchor, h!("tx2")) ] .into(), - last_seen: [(h!("tx2"), 1000000)].into() + last_seen: [(h!("tx2"), 1000000)].into(), + ..Default::default() } ); @@ -211,7 +206,8 @@ fn insert_txouts() { (unconf_anchor, h!("tx2")) ] .into(), - last_seen: [(h!("tx2"), 1000000)].into() + last_seen: [(h!("tx2"), 1000000)].into(), + ..Default::default() } ); } @@ -662,7 +658,8 @@ fn test_walk_ancestors() { ..common::new_tx(0) }; - let mut graph = TxGraph::::new([ + let mut graph = TxGraph::::default(); + let _ = graph.batch_insert_tx([ tx_a0.clone(), tx_b0.clone(), tx_b1.clone(), @@ -1130,7 +1127,8 @@ fn transactions_inserted_into_tx_graph_are_not_canonical_until_they_have_an_anch let txids: Vec = txs.iter().map(Transaction::compute_txid).collect(); // graph - let mut graph = TxGraph::::new(txs); + let mut graph = TxGraph::::default(); + let _ = graph.batch_insert_tx(txs); let full_txs: Vec<_> = graph.full_txs().collect(); assert_eq!(full_txs.len(), 2); let unseen_txs: Vec<_> = graph.txs_with_no_anchor_or_last_seen().collect(); diff --git a/crates/electrum/src/bdk_electrum_client.rs b/crates/electrum/src/bdk_electrum_client.rs index 93c9dea74..0e0b0ee75 100644 --- a/crates/electrum/src/bdk_electrum_client.rs +++ b/crates/electrum/src/bdk_electrum_client.rs @@ -39,7 +39,7 @@ impl BdkElectrumClient { /// Inserts transactions into the transaction cache so that the client will not fetch these /// transactions. - pub fn populate_tx_cache(&self, tx_graph: impl AsRef>) { + pub fn populate_tx_cache(&self, tx_graph: impl AsRef>) { let txs = tx_graph .as_ref() .full_txs() diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index 825454331..24e0f1ca7 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -2,7 +2,7 @@ use bdk_chain::{ bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, Txid, WScriptHash}, local_chain::LocalChain, spk_client::{FullScanRequest, SyncRequest}, - Balance, ConfirmationBlockTime, IndexedTxGraph, SpkTxOutIndex, + Balance, ConfirmationBlockTime, SpkTxOutIndex, TxGraph, }; use bdk_electrum::BdkElectrumClient; use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; @@ -11,13 +11,11 @@ use std::str::FromStr; fn get_balance( recv_chain: &LocalChain, - recv_graph: &IndexedTxGraph>, + recv_graph: &TxGraph>, ) -> anyhow::Result { let chain_tip = recv_chain.tip().block_id(); - let outpoints = recv_graph.index.outpoints().clone(); - let balance = recv_graph - .graph() - .balance(recv_chain, chain_tip, outpoints, |_, _| true); + let outpoints = recv_graph.indexer.outpoints().clone(); + let balance = recv_graph.balance(recv_chain, chain_tip, outpoints, |_, _| true); Ok(balance) } @@ -262,7 +260,7 @@ fn scan_detects_confirmed_tx() -> anyhow::Result<()> { // Setup receiver. let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?); - let mut recv_graph = IndexedTxGraph::::new({ + let mut recv_graph = TxGraph::::new({ let mut recv_index = SpkTxOutIndex::default(); recv_index.insert_spk((), spk_to_track.clone()); recv_index @@ -299,13 +297,10 @@ fn scan_detects_confirmed_tx() -> anyhow::Result<()> { }, ); - for tx in recv_graph.graph().full_txs() { + for tx in recv_graph.full_txs() { // Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the // floating txouts available from the transaction's previous outputs. - let fee = recv_graph - .graph() - .calculate_fee(&tx.tx) - .expect("fee must exist"); + let fee = recv_graph.calculate_fee(&tx.tx).expect("fee must exist"); // Retrieve the fee in the transaction data from `bitcoind`. let tx_fee = env @@ -352,7 +347,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { // Setup receiver. let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?); - let mut recv_graph = IndexedTxGraph::::new({ + let mut recv_graph = TxGraph::::new({ let mut recv_index = SpkTxOutIndex::default(); recv_index.insert_spk((), spk_to_track.clone()); recv_index diff --git a/crates/sqlite/src/store.rs b/crates/sqlite/src/store.rs index 5b7992518..724b8a8c0 100644 --- a/crates/sqlite/src/store.rs +++ b/crates/sqlite/src/store.rs @@ -14,8 +14,7 @@ use std::sync::{Arc, Mutex}; use crate::Error; use bdk_chain::CombinedChangeSet; use bdk_chain::{ - indexed_tx_graph, indexer::keychain_txout, local_chain, tx_graph, Anchor, DescriptorExt, - DescriptorId, Merge, + indexer::keychain_txout, local_chain, tx_graph, Anchor, DescriptorExt, DescriptorId, Merge, }; /// Persists data in to a relational schema based [SQLite] database file. @@ -188,7 +187,7 @@ where /// If keychain exists only update last active index. fn insert_keychains( db_transaction: &rusqlite::Transaction, - tx_graph_changeset: &indexed_tx_graph::ChangeSet>, + tx_graph_changeset: &tx_graph::ChangeSet>, ) -> Result<(), Error> { let keychain_changeset = &tx_graph_changeset.indexer; for (keychain, descriptor) in keychain_changeset.keychains_added.iter() { @@ -207,7 +206,7 @@ where /// Update descriptor last revealed index. fn update_last_revealed( db_transaction: &rusqlite::Transaction, - tx_graph_changeset: &indexed_tx_graph::ChangeSet>, + tx_graph_changeset: &tx_graph::ChangeSet>, ) -> Result<(), Error> { let keychain_changeset = &tx_graph_changeset.indexer; for (descriptor_id, last_revealed) in keychain_changeset.last_revealed.iter() { @@ -280,9 +279,9 @@ impl Store { /// Error if trying to insert existing txid. fn insert_txs( db_transaction: &rusqlite::Transaction, - tx_graph_changeset: &indexed_tx_graph::ChangeSet>, + tx_graph_changeset: &tx_graph::ChangeSet>, ) -> Result<(), Error> { - for tx in tx_graph_changeset.graph.txs.iter() { + for tx in tx_graph_changeset.txs.iter() { let insert_tx_stmt = &mut db_transaction .prepare_cached("INSERT INTO tx (txid, whole_tx) VALUES (:txid, :whole_tx) ON CONFLICT (txid) DO UPDATE SET whole_tx = :whole_tx WHERE txid = :txid") .expect("insert or update tx whole_tx statement"); @@ -344,9 +343,9 @@ impl Store { /// Error if trying to insert existing outpoint. fn insert_txouts( db_transaction: &rusqlite::Transaction, - tx_graph_changeset: &indexed_tx_graph::ChangeSet>, + tx_graph_changeset: &tx_graph::ChangeSet>, ) -> Result<(), Error> { - for txout in tx_graph_changeset.graph.txouts.iter() { + for txout in tx_graph_changeset.txouts.iter() { let insert_txout_stmt = &mut db_transaction .prepare_cached("INSERT INTO txout (txid, vout, value, script) VALUES (:txid, :vout, :value, :script)") .expect("insert txout statement"); @@ -394,9 +393,9 @@ impl Store { /// Update transaction last seen times. fn update_last_seen( db_transaction: &rusqlite::Transaction, - tx_graph_changeset: &indexed_tx_graph::ChangeSet>, + tx_graph_changeset: &tx_graph::ChangeSet>, ) -> Result<(), Error> { - for tx_last_seen in tx_graph_changeset.graph.last_seen.iter() { + for tx_last_seen in tx_graph_changeset.last_seen.iter() { let insert_or_update_tx_stmt = &mut db_transaction .prepare_cached("INSERT INTO tx (txid, last_seen) VALUES (:txid, :last_seen) ON CONFLICT (txid) DO UPDATE SET last_seen = :last_seen WHERE txid = :txid") .expect("insert or update tx last_seen statement"); @@ -419,10 +418,10 @@ where /// Insert anchors. fn insert_anchors( db_transaction: &rusqlite::Transaction, - tx_graph_changeset: &indexed_tx_graph::ChangeSet>, + tx_graph_changeset: &tx_graph::ChangeSet>, ) -> Result<(), Error> { // serde_json::to_string - for anchor in tx_graph_changeset.graph.anchors.iter() { + for anchor in tx_graph_changeset.anchors.iter() { let insert_anchor_stmt = &mut db_transaction .prepare_cached("INSERT INTO anchor_tx (block_hash, anchor, txid) VALUES (:block_hash, jsonb(:anchor), :txid)") .expect("insert anchor statement"); @@ -485,7 +484,7 @@ where let chain_changeset = &changeset.chain; Self::insert_or_delete_blocks(&db_transaction, chain_changeset)?; - let tx_graph_changeset = &changeset.indexed_tx_graph; + let tx_graph_changeset = &changeset.tx_graph; Self::insert_keychains(&db_transaction, tx_graph_changeset)?; Self::update_last_revealed(&db_transaction, tx_graph_changeset)?; Self::insert_txs(&db_transaction, tx_graph_changeset)?; @@ -508,27 +507,23 @@ where let txouts = Self::select_txouts(&db_transaction)?; let anchors = Self::select_anchors(&db_transaction)?; - let graph: tx_graph::ChangeSet = tx_graph::ChangeSet { + let tx_graph: tx_graph::ChangeSet = tx_graph::ChangeSet { txs, txouts, anchors, last_seen, + indexer: keychain_txout::ChangeSet { + keychains_added, + last_revealed, + }, }; - let indexer = keychain_txout::ChangeSet { - keychains_added, - last_revealed, - }; - - let indexed_tx_graph: indexed_tx_graph::ChangeSet> = - indexed_tx_graph::ChangeSet { graph, indexer }; - - if network.is_none() && chain.is_empty() && indexed_tx_graph.is_empty() { + if network.is_none() && chain.is_empty() && tx_graph.is_empty() { Ok(None) } else { Ok(Some(CombinedChangeSet { chain, - indexed_tx_graph, + tx_graph, network, })) } @@ -547,7 +542,7 @@ mod test { use bdk_chain::bitcoin::{secp256k1, BlockHash, OutPoint}; use bdk_chain::miniscript::Descriptor; use bdk_chain::CombinedChangeSet; - use bdk_chain::{indexed_tx_graph, tx_graph, BlockId, ConfirmationBlockTime, DescriptorExt}; + use bdk_chain::{tx_graph, BlockId, ConfirmationBlockTime, DescriptorExt}; use std::str::FromStr; use std::sync::Arc; @@ -648,7 +643,7 @@ mod test { let anchor1 = anchor_fn(1, 1296667328, block_hash_1); let anchor2 = anchor_fn(2, 1296688946, block_hash_2); - let tx_graph_changeset = tx_graph::ChangeSet:: { + let tx_graph_changeset = tx_graph::ChangeSet { txs: [tx0.clone(), tx1.clone()].into(), txouts: [(outpoint0_0, txout0_0), (outpoint1_0, txout1_0)].into(), anchors: [(anchor1, tx0.compute_txid()), (anchor1, tx1.compute_txid())].into(), @@ -658,65 +653,48 @@ mod test { (tx2.compute_txid(), 1608919121), ] .into(), + indexer: keychain_txout::ChangeSet { + keychains_added: [(ext_keychain, ext_desc), (int_keychain, int_desc)].into(), + last_revealed: [(ext_desc_id, 124), (int_desc_id, 421)].into(), + }, }; - let keychain_changeset = keychain_txout::ChangeSet { - keychains_added: [(ext_keychain, ext_desc), (int_keychain, int_desc)].into(), - last_revealed: [(ext_desc_id, 124), (int_desc_id, 421)].into(), - }; - - let graph_changeset: indexed_tx_graph::ChangeSet> = - indexed_tx_graph::ChangeSet { - graph: tx_graph_changeset, - indexer: keychain_changeset, - }; - // test changesets to write to db let mut changesets = Vec::new(); changesets.push(CombinedChangeSet { chain: block_changeset, - indexed_tx_graph: graph_changeset, + tx_graph: tx_graph_changeset, network: network_changeset, }); // create changeset that sets the whole tx2 and updates it's lastseen where before there was only the txid and last_seen - let tx_graph_changeset2 = tx_graph::ChangeSet:: { + let tx_graph_changeset2 = tx_graph::ChangeSet { txs: [tx2.clone()].into(), txouts: BTreeMap::default(), anchors: BTreeSet::default(), last_seen: [(tx2.compute_txid(), 1708919121)].into(), + ..Default::default() }; - let graph_changeset2: indexed_tx_graph::ChangeSet> = - indexed_tx_graph::ChangeSet { - graph: tx_graph_changeset2, - indexer: keychain_txout::ChangeSet::default(), - }; - changesets.push(CombinedChangeSet { chain: local_chain::ChangeSet::default(), - indexed_tx_graph: graph_changeset2, + tx_graph: tx_graph_changeset2, network: None, }); // create changeset that adds a new anchor2 for tx0 and tx1 - let tx_graph_changeset3 = tx_graph::ChangeSet:: { + let tx_graph_changeset3 = tx_graph::ChangeSet { txs: BTreeSet::default(), txouts: BTreeMap::default(), anchors: [(anchor2, tx0.compute_txid()), (anchor2, tx1.compute_txid())].into(), last_seen: BTreeMap::default(), + ..Default::default() }; - let graph_changeset3: indexed_tx_graph::ChangeSet> = - indexed_tx_graph::ChangeSet { - graph: tx_graph_changeset3, - indexer: keychain_txout::ChangeSet::default(), - }; - changesets.push(CombinedChangeSet { chain: local_chain::ChangeSet::default(), - indexed_tx_graph: graph_changeset3, + tx_graph: tx_graph_changeset3, network: None, }); diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 9db21ac71..c0dd38876 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -21,15 +21,13 @@ use alloc::{ }; pub use bdk_chain::Balance; use bdk_chain::{ - indexed_tx_graph, indexer::keychain_txout::KeychainTxOutIndex, local_chain::{ self, ApplyHeaderError, CannotConnectError, CheckPoint, CheckPointIter, LocalChain, }, spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}, - tx_graph::{CanonicalTx, TxGraph, TxNode}, - BlockId, ChainPosition, ConfirmationBlockTime, ConfirmationTime, FullTxOut, Indexed, - IndexedTxGraph, Merge, + tx_graph::{self, CanonicalTx, TxGraph, TxNode}, + BlockId, ChainPosition, ConfirmationBlockTime, ConfirmationTime, FullTxOut, Indexed, Merge, }; use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; use bitcoin::{ @@ -104,7 +102,7 @@ pub struct Wallet { signers: Arc, change_signers: Arc, chain: LocalChain, - indexed_graph: IndexedTxGraph>, + tx_graph: TxGraph>, stage: ChangeSet, network: Network, secp: SecpCtx, @@ -346,17 +344,20 @@ impl Wallet { ) -> Result { let secp = Secp256k1::new(); let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash); - let mut index = KeychainTxOutIndex::::default(); - - let (signers, change_signers) = - create_signers(&mut index, &secp, descriptor, change_descriptor, network) - .map_err(NewError::Descriptor)?; + let mut tx_graph = TxGraph::default(); - let indexed_graph = IndexedTxGraph::new(index); + let (signers, change_signers) = create_signers( + &mut tx_graph.indexer, + &secp, + descriptor, + change_descriptor, + network, + ) + .map_err(NewError::Descriptor)?; let staged = ChangeSet { chain: chain_changeset, - indexed_tx_graph: indexed_graph.initial_changeset(), + tx_graph: tx_graph.initial_changeset(), network: Some(network), }; @@ -365,7 +366,7 @@ impl Wallet { change_signers, network, chain, - indexed_graph, + tx_graph, stage: staged, secp, }) @@ -420,14 +421,14 @@ impl Wallet { LocalChain::from_changeset(changeset.chain).map_err(|_| LoadError::MissingGenesis)?; let mut index = KeychainTxOutIndex::::default(); let descriptor = changeset - .indexed_tx_graph + .tx_graph .indexer .keychains_added .get(&KeychainKind::External) .ok_or(LoadError::MissingDescriptor(KeychainKind::External))? .clone(); let change_descriptor = changeset - .indexed_tx_graph + .tx_graph .indexer .keychains_added .get(&KeychainKind::Internal) @@ -438,8 +439,8 @@ impl Wallet { create_signers(&mut index, &secp, descriptor, change_descriptor, network) .expect("Can't fail: we passed in valid descriptors, recovered from the changeset"); - let mut indexed_graph = IndexedTxGraph::new(index); - indexed_graph.apply_changeset(changeset.indexed_tx_graph); + let mut tx_graph = TxGraph::new(index); + tx_graph.apply_changeset(changeset.tx_graph); let stage = ChangeSet::default(); @@ -447,7 +448,7 @@ impl Wallet { signers, change_signers, chain, - indexed_graph, + tx_graph, stage, network, secp, @@ -600,7 +601,7 @@ impl Wallet { /// Iterator over all keychains in this wallet pub fn keychains(&self) -> impl Iterator { - self.indexed_graph.index.keychains() + self.tx_graph.indexer.keychains() } /// Peek an address of the given `keychain` at `index` without revealing it. @@ -613,8 +614,8 @@ impl Wallet { /// [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) max index. pub fn peek_address(&self, keychain: KeychainKind, mut index: u32) -> AddressInfo { let mut spk_iter = self - .indexed_graph - .index + .tx_graph + .indexer .unbounded_spk_iter(&keychain) .expect("keychain must exist"); if !spk_iter.descriptor().has_wildcard() { @@ -659,14 +660,14 @@ impl Wallet { /// # Ok::<(), anyhow::Error>(()) /// ``` pub fn reveal_next_address(&mut self, keychain: KeychainKind) -> AddressInfo { - let index = &mut self.indexed_graph.index; + let index = &mut self.tx_graph.indexer; let stage = &mut self.stage; let ((index, spk), index_changeset) = index .reveal_next_spk(&keychain) .expect("keychain must exist"); - stage.merge(indexed_tx_graph::ChangeSet::from(index_changeset).into()); + stage.merge(tx_graph::ChangeSet::from(index_changeset).into()); AddressInfo { index, @@ -691,8 +692,8 @@ impl Wallet { index: u32, ) -> impl Iterator + '_ { let (spks, index_changeset) = self - .indexed_graph - .index + .tx_graph + .indexer .reveal_to_target(&keychain, index) .expect("keychain must exist"); @@ -714,14 +715,14 @@ impl Wallet { /// **WARNING**: To avoid address reuse you must persist the changes resulting from one or more /// calls to this method before closing the wallet. See [`Wallet::reveal_next_address`]. pub fn next_unused_address(&mut self, keychain: KeychainKind) -> AddressInfo { - let index = &mut self.indexed_graph.index; + let index = &mut self.tx_graph.indexer; let ((index, spk), index_changeset) = index .next_unused_spk(&keychain) .expect("keychain must exist"); self.stage - .merge(indexed_tx_graph::ChangeSet::from(index_changeset).into()); + .merge(tx_graph::ChangeSet::from(index_changeset).into()); AddressInfo { index, @@ -735,7 +736,7 @@ impl Wallet { /// /// Returns whether the given index was present and then removed from the unused set. pub fn mark_used(&mut self, keychain: KeychainKind, index: u32) -> bool { - self.indexed_graph.index.mark_used(keychain, index) + self.tx_graph.indexer.mark_used(keychain, index) } /// Undoes the effect of [`mark_used`] and returns whether the `index` was inserted @@ -747,7 +748,7 @@ impl Wallet { /// /// [`mark_used`]: Self::mark_used pub fn unmark_used(&mut self, keychain: KeychainKind, index: u32) -> bool { - self.indexed_graph.index.unmark_used(keychain, index) + self.tx_graph.indexer.unmark_used(keychain, index) } /// List addresses that are revealed but unused. @@ -759,8 +760,8 @@ impl Wallet { &self, keychain: KeychainKind, ) -> impl DoubleEndedIterator + '_ { - self.indexed_graph - .index + self.tx_graph + .indexer .unused_keychain_spks(&keychain) .map(move |(index, spk)| AddressInfo { index, @@ -771,24 +772,23 @@ impl Wallet { /// Return whether or not a `script` is part of this wallet (either internal or external) pub fn is_mine(&self, script: &Script) -> bool { - self.indexed_graph.index.index_of_spk(script).is_some() + self.tx_graph.indexer.index_of_spk(script).is_some() } /// Finds how the wallet derived the script pubkey `spk`. /// /// Will only return `Some(_)` if the wallet has given out the spk. pub fn derivation_of_spk(&self, spk: &Script) -> Option<(KeychainKind, u32)> { - self.indexed_graph.index.index_of_spk(spk).cloned() + self.tx_graph.indexer.index_of_spk(spk).cloned() } /// Return the list of unspent outputs of this wallet pub fn list_unspent(&self) -> impl Iterator + '_ { - self.indexed_graph - .graph() + self.tx_graph .filter_chain_unspents( &self.chain, self.chain.tip().block_id(), - self.indexed_graph.index.outpoints().iter().cloned(), + self.tx_graph.indexer.outpoints().iter().cloned(), ) .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo)) } @@ -797,12 +797,11 @@ impl Wallet { /// /// To list only unspent outputs (UTXOs), use [`Wallet::list_unspent`] instead. pub fn list_output(&self) -> impl Iterator + '_ { - self.indexed_graph - .graph() + self.tx_graph .filter_chain_txouts( &self.chain, self.chain.tip().block_id(), - self.indexed_graph.index.outpoints().iter().cloned(), + self.tx_graph.indexer.outpoints().iter().cloned(), ) .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo)) } @@ -828,7 +827,7 @@ impl Wallet { pub fn all_unbounded_spk_iters( &self, ) -> BTreeMap> + Clone> { - self.indexed_graph.index.all_unbounded_spk_iters() + self.tx_graph.indexer.all_unbounded_spk_iters() } /// Get an unbounded script pubkey iterator for the given `keychain`. @@ -840,8 +839,8 @@ impl Wallet { &self, keychain: KeychainKind, ) -> impl Iterator> + Clone { - self.indexed_graph - .index + self.tx_graph + .indexer .unbounded_spk_iter(&keychain) .expect("keychain must exist") } @@ -849,9 +848,8 @@ impl Wallet { /// Returns the utxo owned by this wallet corresponding to `outpoint` if it exists in the /// wallet's database. pub fn get_utxo(&self, op: OutPoint) -> Option { - let ((keychain, index), _) = self.indexed_graph.index.txout(op)?; - self.indexed_graph - .graph() + let ((keychain, index), _) = self.tx_graph.indexer.txout(op)?; + self.tx_graph .filter_chain_unspents( &self.chain, self.chain.tip().block_id(), @@ -879,7 +877,7 @@ impl Wallet { /// [`list_unspent`]: Self::list_unspent /// [`list_output`]: Self::list_output pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) { - let additions = self.indexed_graph.insert_txout(outpoint, txout); + let additions = self.tx_graph.insert_txout(outpoint, txout); self.stage.merge(additions.into()); } @@ -911,7 +909,7 @@ impl Wallet { /// ``` /// [`insert_txout`]: Self::insert_txout pub fn calculate_fee(&self, tx: &Transaction) -> Result { - self.indexed_graph.graph().calculate_fee(tx) + self.tx_graph.calculate_fee(tx) } /// Calculate the [`FeeRate`] for a given transaction. @@ -971,7 +969,7 @@ impl Wallet { /// let (sent, received) = wallet.sent_and_received(tx); /// ``` pub fn sent_and_received(&self, tx: &Transaction) -> (Amount, Amount) { - self.indexed_graph.index.sent_and_received(tx, ..) + self.tx_graph.indexer.sent_and_received(tx, ..) } /// Get a single transaction from the wallet as a [`CanonicalTx`] (if the transaction exists). @@ -1021,7 +1019,7 @@ impl Wallet { &self, txid: Txid, ) -> Option, ConfirmationBlockTime>> { - let graph = self.indexed_graph.graph(); + let graph = &self.tx_graph; Some(CanonicalTx { chain_position: graph.get_chain_position( @@ -1067,7 +1065,7 @@ impl Wallet { /// must be broadcast to the network and the wallet synced via a chain source. pub fn insert_tx(&mut self, tx: Transaction) -> bool { let mut changeset = ChangeSet::default(); - changeset.merge(self.indexed_graph.insert_tx(tx).into()); + changeset.merge(self.tx_graph.insert_tx(tx).into()); let ret = !changeset.is_empty(); self.stage.merge(changeset); ret @@ -1077,18 +1075,17 @@ impl Wallet { pub fn transactions( &self, ) -> impl Iterator, ConfirmationBlockTime>> + '_ { - self.indexed_graph - .graph() + self.tx_graph .list_canonical_txs(&self.chain, self.chain.tip().block_id()) } /// Return the balance, separated into available, trusted-pending, untrusted-pending and immature /// values. pub fn balance(&self) -> Balance { - self.indexed_graph.graph().balance( + self.tx_graph.balance( &self.chain, self.chain.tip().block_id(), - self.indexed_graph.index.outpoints().iter().cloned(), + self.tx_graph.indexer.outpoints().iter().cloned(), |&(k, _), _| k == KeychainKind::Internal, ) } @@ -1176,7 +1173,7 @@ impl Wallet { params: TxParams, rng: &mut impl RngCore, ) -> Result { - let keychains: BTreeMap<_, _> = self.indexed_graph.index.keychains().collect(); + let keychains: BTreeMap<_, _> = self.tx_graph.indexer.keychains().collect(); let external_descriptor = keychains.get(&KeychainKind::External).expect("must exist"); let internal_descriptor = keychains.get(&KeychainKind::Internal).expect("must exist"); @@ -1388,11 +1385,11 @@ impl Wallet { None => { let change_keychain = KeychainKind::Internal; let ((index, spk), index_changeset) = self - .indexed_graph - .index + .tx_graph + .indexer .next_unused_spk(&change_keychain) .expect("keychain must exist"); - self.indexed_graph.index.mark_used(change_keychain, index); + self.tx_graph.indexer.mark_used(change_keychain, index); self.stage.merge(index_changeset.into()); spk } @@ -1541,8 +1538,8 @@ impl Wallet { &mut self, txid: Txid, ) -> Result, BuildFeeBumpError> { - let graph = self.indexed_graph.graph(); - let txout_index = &self.indexed_graph.index; + let graph = &self.tx_graph; + let txout_index = &self.tx_graph.indexer; let chain_tip = self.chain.tip().block_id(); let mut tx = graph @@ -1768,8 +1765,8 @@ impl Wallet { /// /// This can be used to build a watch-only version of a wallet. pub fn public_descriptor(&self, keychain: KeychainKind) -> &ExtendedDescriptor { - self.indexed_graph - .index + self.tx_graph + .indexer .get_descriptor(&keychain) .expect("keychain must exist") } @@ -1802,8 +1799,7 @@ impl Wallet { continue; } let confirmation_height = self - .indexed_graph - .graph() + .tx_graph .get_chain_position(&self.chain, chain_tip, input.previous_output.txid) .map(|chain_position| match chain_position { ChainPosition::Confirmed(a) => a.block_id.height, @@ -1823,7 +1819,7 @@ impl Wallet { .get_utxo_for(n) .and_then(|txout| self.get_descriptor_for_txout(&txout)) .or_else(|| { - self.indexed_graph.index.keychains().find_map(|(_, desc)| { + self.tx_graph.indexer.keychains().find_map(|(_, desc)| { desc.derive_from_psbt_input(psbt_input, psbt.get_utxo_for(n), &self.secp) }) }); @@ -1879,13 +1875,13 @@ impl Wallet { /// The derivation index of this wallet. It will return `None` if it has not derived any addresses. /// Otherwise, it will return the index of the highest address it has derived. pub fn derivation_index(&self, keychain: KeychainKind) -> Option { - self.indexed_graph.index.last_revealed_index(&keychain) + self.tx_graph.indexer.last_revealed_index(&keychain) } /// The index of the next address that you would get if you were to ask the wallet for a new address pub fn next_derivation_index(&self, keychain: KeychainKind) -> u32 { - self.indexed_graph - .index + self.tx_graph + .indexer .next_index(&keychain) .expect("keychain must exist") .0 @@ -1896,7 +1892,7 @@ impl Wallet { /// This frees up the change address used when creating the tx for use in future transactions. // TODO: Make this free up reserved utxos when that's implemented pub fn cancel_tx(&mut self, tx: &Transaction) { - let txout_index = &mut self.indexed_graph.index; + let txout_index = &mut self.tx_graph.indexer; for txout in &tx.output { if let Some((keychain, index)) = txout_index.index_of_spk(&txout.script_pubkey) { // NOTE: unmark_used will **not** make something unused if it has actually been used @@ -1907,10 +1903,7 @@ impl Wallet { } fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option { - let &(keychain, child) = self - .indexed_graph - .index - .index_of_spk(&txout.script_pubkey)?; + let &(keychain, child) = self.tx_graph.indexer.index_of_spk(&txout.script_pubkey)?; let descriptor = self.public_descriptor(keychain); descriptor.at_derivation_index(child).ok() } @@ -1972,18 +1965,18 @@ impl Wallet { .iter() .map(|u| -> bool { let txid = u.0.outpoint.txid; - let tx = match self.indexed_graph.graph().get_tx(txid) { + let tx = match self.tx_graph.get_tx(txid) { Some(tx) => tx, None => return false, }; - let confirmation_time: ConfirmationTime = match self - .indexed_graph - .graph() - .get_chain_position(&self.chain, chain_tip, txid) - { - Some(chain_position) => chain_position.cloned().into(), - None => return false, - }; + let confirmation_time: ConfirmationTime = + match self + .tx_graph + .get_chain_position(&self.chain, chain_tip, txid) + { + Some(chain_position) => chain_position.cloned().into(), + None => return false, + }; // Whether the UTXO is mature and, if needed, confirmed let mut spendable = true; @@ -2123,8 +2116,8 @@ impl Wallet { // Try to find the prev_script in our db to figure out if this is internal or external, // and the derivation index let &(keychain, child) = self - .indexed_graph - .index + .tx_graph + .indexer .index_of_spk(&utxo.txout.script_pubkey) .ok_or(CreateTxError::UnknownUtxo)?; @@ -2143,7 +2136,7 @@ impl Wallet { .map_err(MiniscriptPsbtError::Conversion)?; let prev_output = utxo.outpoint; - if let Some(prev_tx) = self.indexed_graph.graph().get_tx(prev_output.txid) { + if let Some(prev_tx) = self.tx_graph.get_tx(prev_output.txid) { if desc.is_witness() || desc.is_taproot() { psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone()); } @@ -2170,8 +2163,7 @@ impl Wallet { // Try to figure out the keychain and derivation for every input and output for (is_input, index, out) in utxos.into_iter() { - if let Some(&(keychain, child)) = - self.indexed_graph.index.index_of_spk(&out.script_pubkey) + if let Some(&(keychain, child)) = self.tx_graph.indexer.index_of_spk(&out.script_pubkey) { let desc = self.public_descriptor(keychain); let desc = desc @@ -2220,11 +2212,11 @@ impl Wallet { }; let index_changeset = self - .indexed_graph - .index + .tx_graph + .indexer .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.tx_graph.apply_update(update.graph).into()); self.stage.merge(changeset); Ok(()) } @@ -2244,8 +2236,8 @@ impl Wallet { } /// Get a reference to the inner [`TxGraph`]. - pub fn tx_graph(&self) -> &TxGraph { - self.indexed_graph.graph() + pub fn tx_graph(&self) -> &TxGraph> { + &self.tx_graph } /// Iterate over transactions in the wallet that are unseen and unanchored likely @@ -2258,7 +2250,7 @@ impl Wallet { /// Get a reference to the inner [`KeychainTxOutIndex`]. pub fn spk_index(&self) -> &KeychainTxOutIndex { - &self.indexed_graph.index + &self.tx_graph.indexer } /// Get a reference to the inner [`LocalChain`]. @@ -2315,11 +2307,7 @@ impl Wallet { .apply_header_connected_to(&block.header, height, connected_to)? .into(), ); - changeset.merge( - self.indexed_graph - .apply_block_relevant(block, height) - .into(), - ); + changeset.merge(self.tx_graph.apply_block_relevant(block, height).into()); self.stage.merge(changeset); Ok(()) } @@ -2341,7 +2329,7 @@ impl Wallet { unconfirmed_txs: impl IntoIterator, ) { let indexed_graph_changeset = self - .indexed_graph + .tx_graph .batch_insert_relevant_unconfirmed(unconfirmed_txs); self.stage.merge(indexed_graph_changeset.into()); } @@ -2356,7 +2344,7 @@ impl Wallet { /// start a blockchain sync with a spk based blockchain client. pub fn start_sync_with_revealed_spks(&self) -> SyncRequest { SyncRequest::from_chain_tip(self.chain.tip()) - .populate_with_revealed_spks(&self.indexed_graph.index, ..) + .populate_with_revealed_spks(&self.tx_graph.indexer, ..) } /// Create a [`FullScanRequest] for this wallet. @@ -2368,13 +2356,15 @@ impl Wallet { /// This operation is generally only used when importing or restoring a previously used wallet /// in which the list of used scripts is not known. pub fn start_full_scan(&self) -> FullScanRequest { - FullScanRequest::from_keychain_txout_index(self.chain.tip(), &self.indexed_graph.index) + FullScanRequest::from_keychain_txout_index(self.chain.tip(), &self.tx_graph.indexer) } } -impl AsRef> for Wallet { - fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph { - self.indexed_graph.graph() +impl AsRef>> for Wallet { + fn as_ref( + &self, + ) -> &tx_graph::TxGraph> { + &self.tx_graph } } diff --git a/example-crates/example_bitcoind_rpc_polling/src/main.rs b/example-crates/example_bitcoind_rpc_polling/src/main.rs index c71b18fed..9c16907d2 100644 --- a/example-crates/example_bitcoind_rpc_polling/src/main.rs +++ b/example-crates/example_bitcoind_rpc_polling/src/main.rs @@ -13,10 +13,10 @@ use bdk_bitcoind_rpc::{ }; use bdk_chain::{ bitcoin::{constants::genesis_block, Block, Transaction}, - indexed_tx_graph, indexer::keychain_txout, local_chain::{self, LocalChain}, - ConfirmationBlockTime, IndexedTxGraph, Merge, + tx_graph::{self, TxGraph}, + ConfirmationBlockTime, Merge, }; use example_cli::{ anyhow, @@ -38,7 +38,7 @@ const DB_COMMIT_DELAY: Duration = Duration::from_secs(60); type ChangeSet = ( local_chain::ChangeSet, - indexed_tx_graph::ChangeSet>, + tx_graph::ChangeSet>, ); #[derive(Debug)] @@ -125,12 +125,12 @@ fn main() -> anyhow::Result<()> { let (init_chain_changeset, init_graph_changeset) = init_changeset; let graph = Mutex::new({ - let mut graph = IndexedTxGraph::new(index); + let mut graph = TxGraph::new(index); graph.apply_changeset(init_graph_changeset); graph }); println!( - "[{:>10}s] loaded indexed tx graph from changeset", + "[{:>10}s] loaded tx graph from changeset", start.elapsed().as_secs_f32() ); @@ -212,10 +212,10 @@ fn main() -> anyhow::Result<()> { last_print = Instant::now(); let synced_to = chain.tip(); let balance = { - graph.graph().balance( + graph.balance( &*chain, synced_to.block_id(), - graph.index.outpoints().iter().cloned(), + graph.indexer.outpoints().iter().cloned(), |(k, _), _| k == &Keychain::Internal, ) }; @@ -340,10 +340,10 @@ fn main() -> anyhow::Result<()> { last_print = Some(Instant::now()); let synced_to = chain.tip(); let balance = { - graph.graph().balance( + graph.balance( &*chain, synced_to.block_id(), - graph.index.outpoints().iter().cloned(), + graph.indexer.outpoints().iter().cloned(), |(k, _), _| k == &Keychain::Internal, ) }; diff --git a/example-crates/example_cli/src/lib.rs b/example-crates/example_cli/src/lib.rs index 9327f7873..8886703cc 100644 --- a/example-crates/example_cli/src/lib.rs +++ b/example-crates/example_cli/src/lib.rs @@ -1,5 +1,6 @@ pub use anyhow; use anyhow::Context; +use bdk_chain::TxGraph; use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue}; use bdk_file_store::Store; use serde::{de::DeserializeOwned, Serialize}; @@ -13,24 +14,23 @@ use bdk_chain::{ sighash::{Prevouts, SighashCache}, transaction, Address, Amount, Network, Sequence, Transaction, TxIn, TxOut, }, - indexed_tx_graph::{self, IndexedTxGraph}, indexer::keychain_txout::{self, KeychainTxOutIndex}, local_chain, miniscript::{ descriptor::{DescriptorSecretKey, KeyMap}, Descriptor, DescriptorPublicKey, }, - Anchor, ChainOracle, DescriptorExt, FullTxOut, Merge, + tx_graph, Anchor, ChainOracle, DescriptorExt, FullTxOut, Merge, }; pub use bdk_file_store; pub use clap; use clap::{Parser, Subcommand}; -pub type KeychainTxGraph = IndexedTxGraph>; +pub type KeychainTxGraph = TxGraph>; pub type KeychainChangeSet = ( local_chain::ChangeSet, - indexed_tx_graph::ChangeSet>, + tx_graph::ChangeSet>, ); #[derive(Parser)] @@ -250,7 +250,7 @@ where }]; let internal_keychain = if graph - .index + .indexer .keychains() .any(|(k, _)| *k == Keychain::Internal) { @@ -260,14 +260,14 @@ where }; let ((change_index, change_script), change_changeset) = graph - .index + .indexer .next_unused_spk(&internal_keychain) .expect("Must exist"); changeset.merge(change_changeset); let change_plan = bdk_tmp_plan::plan_satisfaction( &graph - .index + .indexer .keychains() .find(|(k, _)| *k == &internal_keychain) .expect("must exist") @@ -286,7 +286,7 @@ where let cs_opts = CoinSelectorOpt { target_feerate: 0.5, min_drain_value: graph - .index + .indexer .keychains() .find(|(k, _)| *k == &internal_keychain) .expect("must exist") @@ -421,9 +421,8 @@ pub fn planned_utxos, ) -> Result>, O::Error> { let chain_tip = chain.get_chain_tip()?; - let outpoints = graph.index.outpoints(); + let outpoints = graph.indexer.outpoints(); graph - .graph() .try_filter_chain_unspents(chain, chain_tip, outpoints.iter().cloned()) .filter_map(|r| -> Option, _>> { let (k, i, full_txo) = match r { @@ -431,7 +430,7 @@ pub fn planned_utxos (k, i, full_txo), }; let desc = graph - .index + .indexer .keychains() .find(|(keychain, _)| *keychain == &k) .expect("keychain must exist") @@ -468,7 +467,7 @@ where Commands::ChainSpecific(_) => unreachable!("example code should handle this!"), Commands::Address { addr_cmd } => { let graph = &mut *graph.lock().unwrap(); - let index = &mut graph.index; + let indexer = &mut graph.indexer; match addr_cmd { AddressCmd::Next | AddressCmd::New => { @@ -478,12 +477,12 @@ where _ => unreachable!("only these two variants exist in match arm"), }; - let ((spk_i, spk), index_changeset) = - spk_chooser(index, &Keychain::External).expect("Must exist"); + let ((spk_i, spk), indexer_changeset) = + spk_chooser(indexer, &Keychain::External).expect("Must exist"); let db = &mut *db.lock().unwrap(); db.append_changeset(&C::from(( local_chain::ChangeSet::default(), - indexed_tx_graph::ChangeSet::from(index_changeset), + tx_graph::ChangeSet::from(indexer_changeset), )))?; let addr = Address::from_script(spk.as_script(), network) .context("failed to derive address")?; @@ -491,7 +490,7 @@ where Ok(()) } AddressCmd::Index => { - for (keychain, derivation_index) in index.last_revealed_indices() { + for (keychain, derivation_index) in indexer.last_revealed_indices() { println!("{:?}: {}", keychain, derivation_index); } Ok(()) @@ -501,14 +500,14 @@ where true => Keychain::Internal, false => Keychain::External, }; - for (spk_i, spk) in index.revealed_keychain_spks(&target_keychain) { + for (spk_i, spk) in indexer.revealed_keychain_spks(&target_keychain) { let address = Address::from_script(spk, network) .expect("should always be able to derive address"); println!( "{:?} {} used:{}", spk_i, address, - index.is_used(target_keychain, spk_i) + indexer.is_used(target_keychain, spk_i) ); } Ok(()) @@ -528,10 +527,10 @@ where } } - let balance = graph.graph().try_balance( + let balance = graph.try_balance( chain, chain.get_chain_tip()?, - graph.index.outpoints().iter().cloned(), + graph.indexer.outpoints().iter().cloned(), |(k, _), _| k == &Keychain::Internal, )?; @@ -561,7 +560,7 @@ where let graph = &*graph.lock().unwrap(); let chain = &*chain.lock().unwrap(); let chain_tip = chain.get_chain_tip()?; - let outpoints = graph.index.outpoints(); + let outpoints = graph.indexer.outpoints(); match txout_cmd { TxOutCmd::List { @@ -571,7 +570,6 @@ where unconfirmed, } => { let txouts = graph - .graph() .try_filter_chain_txouts(chain, chain_tip, outpoints.iter().cloned()) .filter(|r| match r { Ok((_, full_txo)) => match (spent, unspent) { @@ -631,14 +629,14 @@ where let db = &mut *db.lock().unwrap(); db.append_changeset(&C::from(( local_chain::ChangeSet::default(), - indexed_tx_graph::ChangeSet::from(index_changeset), + tx_graph::ChangeSet::from(index_changeset), )))?; } // We don't want other callers/threads to use this address while we're using it // but we also don't want to scan the tx we just created because it's not // technically in the blockchain yet. - graph.index.mark_used(change_keychain, index); + graph.indexer.mark_used(change_keychain, index); (tx, Some((change_keychain, index))) } else { (tx, None) @@ -663,7 +661,7 @@ where Err(e) => { if let Some((keychain, index)) = change_index { // We failed to broadcast, so allow our change address to be used in the future - graph.lock().unwrap().index.unmark_used(keychain, index); + graph.lock().unwrap().indexer.unmark_used(keychain, index); } Err(e) } diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index 31e8e7041..377e33ebd 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -6,11 +6,10 @@ use std::{ use bdk_chain::{ bitcoin::{constants::genesis_block, Address, Network, Txid}, collections::BTreeSet, - indexed_tx_graph::{self, IndexedTxGraph}, indexer::keychain_txout, local_chain::{self, LocalChain}, spk_client::{FullScanRequest, SyncRequest}, - ConfirmationBlockTime, Merge, + tx_graph, ConfirmationBlockTime, Merge, TxGraph, }; use bdk_electrum::{ electrum_client::{self, Client, ElectrumApi}, @@ -100,7 +99,7 @@ pub struct ScanOptions { type ChangeSet = ( local_chain::ChangeSet, - indexed_tx_graph::ChangeSet>, + tx_graph::ChangeSet>, ); fn main() -> anyhow::Result<()> { @@ -115,7 +114,7 @@ fn main() -> anyhow::Result<()> { let (disk_local_chain, disk_tx_graph) = init_changeset; let graph = Mutex::new({ - let mut graph = IndexedTxGraph::new(index); + let mut graph = TxGraph::new(index); graph.apply_changeset(disk_tx_graph); graph }); @@ -151,7 +150,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 (local_chain_changeset, tx_graph_changeset) = match electrum_cmd.clone() { ElectrumCommands::Scan { stop_gap, scan_options, @@ -165,7 +164,7 @@ fn main() -> anyhow::Result<()> { .set_spks_for_keychain( Keychain::External, graph - .index + .indexer .unbounded_spk_iter(&Keychain::External) .into_iter() .flatten(), @@ -173,7 +172,7 @@ fn main() -> anyhow::Result<()> { .set_spks_for_keychain( Keychain::Internal, graph - .index + .indexer .unbounded_spk_iter(&Keychain::Internal) .into_iter() .flatten(), @@ -191,14 +190,29 @@ fn main() -> anyhow::Result<()> { }) }; - let res = client + let mut update = client .full_scan::<_>(request, stop_gap, scan_options.batch_size, false) .context("scanning the blockchain")?; - ( - res.chain_update, - res.graph_update, - Some(res.last_active_indices), - ) + + // 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); + + 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 + // deriviation indices. Usually before a scan you are on a fresh wallet with no + // addresses derived so we need to derive up to last active addresses the scan found + // before adding the transactions. + (chain.apply_update(update.chain_update)?, { + let index_changeset = graph + .indexer + .reveal_to_target_multi(&update.last_active_indices); + let mut tx_graph_changeset = graph.apply_update(update.graph_update); + tx_graph_changeset.merge(index_changeset.into()); + tx_graph_changeset + }) } ElectrumCommands::Sync { mut unused_spks, @@ -208,147 +222,133 @@ fn main() -> anyhow::Result<()> { scan_options, .. } => { - // Get a short lock on the tracker to get the spks we're interested in - let graph = graph.lock().unwrap(); - let chain = chain.lock().unwrap(); - - if !(all_spks || unused_spks || utxos || unconfirmed) { - unused_spks = true; - unconfirmed = true; - utxos = true; - } else if all_spks { - unused_spks = false; - } - - let chain_tip = chain.tip(); - let mut request = SyncRequest::from_chain_tip(chain_tip.clone()); - - if all_spks { - let all_spks = graph - .index - .revealed_spks(..) - .map(|(index, spk)| (index, spk.to_owned())) - .collect::>(); - request = request.chain_spks(all_spks.into_iter().map(|((k, spk_i), spk)| { - eprint!("Scanning {}: {}", k, spk_i); - spk - })); - } - if unused_spks { - let unused_spks = graph - .index - .unused_spks() - .map(|(index, spk)| (index, spk.to_owned())) - .collect::>(); - request = - request.chain_spks(unused_spks.into_iter().map(move |((k, spk_i), spk)| { + let local_tip = chain.lock().expect("mutex must not be poisoned").tip(); + // Spks, outpoints and txids we want updates on will be accumulated here. + let mut request = SyncRequest::from_chain_tip(local_tip.clone()); + + { + // Get a short lock on the tracker to get the spks we're interested in + let graph = graph.lock().unwrap(); + let chain = chain.lock().unwrap(); + + if !(all_spks || unused_spks || utxos || unconfirmed) { + unused_spks = true; + unconfirmed = true; + utxos = true; + } else if all_spks { + unused_spks = false; + } + + let chain_tip = chain.tip(); + + if all_spks { + let all_spks = graph + .indexer + .revealed_spks(..) + .map(|(index, spk)| (index, spk.to_owned())) + .collect::>(); + request = request.chain_spks(all_spks.into_iter().map(|((k, spk_i), spk)| { + eprint!("Scanning {}: {}", k, spk_i); + spk + })); + } + if unused_spks { + let unused_spks = graph + .indexer + .unused_spks() + .map(|(index, spk)| (index, spk.to_owned())) + .collect::>(); + request = request.chain_spks(unused_spks.into_iter().map( + move |((k, spk_i), spk)| { + eprint!( + "Checking if address {} {}:{} has been used", + Address::from_script(&spk, args.network).unwrap(), + k, + spk_i, + ); + spk + }, + )); + } + + if utxos { + let init_outpoints = graph.indexer.outpoints(); + + let utxos = graph + .filter_chain_unspents( + &*chain, + chain_tip.block_id(), + init_outpoints.iter().cloned(), + ) + .map(|(_, utxo)| utxo) + .collect::>(); + request = request.chain_outpoints(utxos.into_iter().map(|utxo| { eprint!( - "Checking if address {} {}:{} has been used", - Address::from_script(&spk, args.network).unwrap(), - k, - spk_i, + "Checking if outpoint {} (value: {}) has been spent", + utxo.outpoint, utxo.txout.value ); - spk + utxo.outpoint })); - } + }; - if utxos { - let init_outpoints = graph.index.outpoints(); + if unconfirmed { + let unconfirmed_txids = graph + .list_canonical_txs(&*chain, chain_tip.block_id()) + .filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed()) + .map(|canonical_tx| canonical_tx.tx_node.txid) + .collect::>(); - let utxos = graph - .graph() - .filter_chain_unspents( - &*chain, - chain_tip.block_id(), - init_outpoints.iter().cloned(), - ) - .map(|(_, utxo)| utxo) - .collect::>(); - request = request.chain_outpoints(utxos.into_iter().map(|utxo| { - eprint!( - "Checking if outpoint {} (value: {}) has been spent", - utxo.outpoint, utxo.txout.value + request = request.chain_txids( + unconfirmed_txids + .into_iter() + .inspect(|txid| eprint!("Checking if {} is confirmed yet", txid)), ); - utxo.outpoint - })); - }; - - if unconfirmed { - let unconfirmed_txids = graph - .graph() - .list_canonical_txs(&*chain, chain_tip.block_id()) - .filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed()) - .map(|canonical_tx| canonical_tx.tx_node.txid) - .collect::>(); - - request = request.chain_txids( - unconfirmed_txids - .into_iter() - .inspect(|txid| eprint!("Checking if {} is confirmed yet", txid)), - ); + } + + let total_spks = request.spks.len(); + let total_txids = request.txids.len(); + let total_ops = request.outpoints.len(); + request = request + .inspect_spks({ + let mut visited = 0; + move |_| { + visited += 1; + eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_spks as f32) + } + }) + .inspect_txids({ + let mut visited = 0; + move |_| { + visited += 1; + eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_txids as f32) + } + }) + .inspect_outpoints({ + let mut visited = 0; + move |_| { + visited += 1; + eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_ops as f32) + } + }); } - let total_spks = request.spks.len(); - let total_txids = request.txids.len(); - let total_ops = request.outpoints.len(); - request = request - .inspect_spks({ - let mut visited = 0; - move |_| { - visited += 1; - eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_spks as f32) - } - }) - .inspect_txids({ - let mut visited = 0; - move |_| { - visited += 1; - eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_txids as f32) - } - }) - .inspect_outpoints({ - let mut visited = 0; - move |_| { - visited += 1; - eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_ops as f32) - } - }); - - let res = client + let mut update = client .sync(request, scan_options.batch_size, false) .context("scanning the blockchain")?; - // drop lock on graph and chain - drop((graph, chain)); - - (res.chain_update, res.graph_update, None) - } - }; - - let now = std::time::UNIX_EPOCH - .elapsed() - .expect("must get time") - .as_secs(); - let _ = graph_update.update_last_seen_unconfirmed(now); - - let db_changeset = { - let mut chain = chain.lock().unwrap(); - let mut graph = graph.lock().unwrap(); + let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); + let _ = update.graph_update.update_last_seen_unconfirmed(now); - let chain_changeset = chain.apply_update(chain_update)?; - - let mut indexed_tx_graph_changeset = - indexed_tx_graph::ChangeSet::::default(); - if let Some(keychain_update) = keychain_update { - let keychain_changeset = graph.index.reveal_to_target_multi(&keychain_update); - indexed_tx_graph_changeset.merge(keychain_changeset.into()); + ( + chain.lock().unwrap().apply_update(update.chain_update)?, + graph.lock().unwrap().apply_update(update.graph_update), + ) } - indexed_tx_graph_changeset.merge(graph.apply_update(graph_update)); - - (chain_changeset, indexed_tx_graph_changeset) }; + // We persist the changes let mut db = db.lock().unwrap(); - db.append_changeset(&db_changeset)?; + db.append_changeset(&(local_chain_changeset, tx_graph_changeset))?; + Ok(()) } diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index ffa2ea24e..98c94d58d 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -6,10 +6,10 @@ use std::{ use bdk_chain::{ bitcoin::{constants::genesis_block, Address, Network, Txid}, - indexed_tx_graph::{self, IndexedTxGraph}, indexer::keychain_txout, local_chain::{self, LocalChain}, spk_client::{FullScanRequest, SyncRequest}, + tx_graph::{self, TxGraph}, ConfirmationBlockTime, Merge, }; @@ -26,7 +26,7 @@ const DB_PATH: &str = ".bdk_esplora_example.db"; type ChangeSet = ( local_chain::ChangeSet, - indexed_tx_graph::ChangeSet>, + tx_graph::ChangeSet>, ); #[derive(Subcommand, Debug, Clone)] @@ -111,14 +111,14 @@ fn main() -> anyhow::Result<()> { let genesis_hash = genesis_block(args.network).block_hash(); - let (init_chain_changeset, init_indexed_tx_graph_changeset) = init_changeset; + let (init_chain_changeset, init_tx_graph_changeset) = init_changeset; - // Construct `IndexedTxGraph` and `LocalChain` with our initial changeset. They are wrapped in + // Construct `TxGraph` and `LocalChain` with our initial changeset. They are wrapped in // `Mutex` to display how they can be used in a multithreaded context. Technically the mutexes // aren't strictly needed here. let graph = Mutex::new({ - let mut graph = IndexedTxGraph::new(index); - graph.apply_changeset(init_indexed_tx_graph_changeset); + let mut graph = TxGraph::new(index); + graph.apply_changeset(init_tx_graph_changeset); graph }); let chain = Mutex::new({ @@ -151,7 +151,7 @@ fn main() -> anyhow::Result<()> { }; let client = esplora_cmd.esplora_args().client(args.network)?; - // Prepare the `IndexedTxGraph` and `LocalChain` updates based on whether we are scanning or + // Prepare the `TxGraph` and `LocalChain` updates based on whether we are scanning or // syncing. // // Scanning: We are iterating through spks of all keychains and scanning for transactions for @@ -162,7 +162,7 @@ fn main() -> anyhow::Result<()> { // // Syncing: We only check for specified spks, utxos and txids to update their confirmation // status or fetch missing transactions. - let (local_chain_changeset, indexed_tx_graph_changeset) = match &esplora_cmd { + let (local_chain_changeset, tx_graph_changeset) = match &esplora_cmd { EsploraCommands::Scan { stop_gap, scan_options, @@ -170,8 +170,8 @@ fn main() -> anyhow::Result<()> { } => { let request = { let chain_tip = chain.lock().expect("mutex must not be poisoned").tip(); - let indexed_graph = &*graph.lock().expect("mutex must not be poisoned"); - FullScanRequest::from_keychain_txout_index(chain_tip, &indexed_graph.index) + let tx_graph = &*graph.lock().expect("mutex must not be poisoned"); + FullScanRequest::from_keychain_txout_index(chain_tip, &tx_graph.indexer) .inspect_spks_for_all_keychains({ let mut once = BTreeSet::::new(); move |keychain, spk_i, _| { @@ -205,11 +205,11 @@ fn main() -> anyhow::Result<()> { // before adding the transactions. (chain.apply_update(update.chain_update)?, { let index_changeset = graph - .index + .indexer .reveal_to_target_multi(&update.last_active_indices); - let mut indexed_tx_graph_changeset = graph.apply_update(update.graph_update); - indexed_tx_graph_changeset.merge(index_changeset.into()); - indexed_tx_graph_changeset + let mut tx_graph_changeset = graph.apply_update(update.graph_update); + tx_graph_changeset.merge(index_changeset.into()); + tx_graph_changeset }) } EsploraCommands::Sync { @@ -238,12 +238,12 @@ fn main() -> anyhow::Result<()> { // Get a short lock on the structures to get spks, utxos, and txs that we are interested // in. { - let graph = graph.lock().unwrap(); + let tx_graph = graph.lock().unwrap(); let chain = chain.lock().unwrap(); if *all_spks { - let all_spks = graph - .index + let all_spks = tx_graph + .indexer .revealed_spks(..) .map(|((k, i), spk)| (k, i, spk.to_owned())) .collect::>(); @@ -255,8 +255,8 @@ fn main() -> anyhow::Result<()> { })); } if unused_spks { - let unused_spks = graph - .index + let unused_spks = tx_graph + .indexer .unused_spks() .map(|(index, spk)| (index, spk.to_owned())) .collect::>(); @@ -277,9 +277,8 @@ fn main() -> anyhow::Result<()> { // We want to search for whether the UTXO is spent, and spent by which // transaction. We provide the outpoint of the UTXO to // `EsploraExt::update_tx_graph_without_keychain`. - let init_outpoints = graph.index.outpoints(); - let utxos = graph - .graph() + let init_outpoints = tx_graph.indexer.outpoints(); + let utxos = tx_graph .filter_chain_unspents( &*chain, local_tip.block_id(), @@ -305,8 +304,7 @@ fn main() -> anyhow::Result<()> { // We want to search for whether the unconfirmed transaction is now confirmed. // We provide the unconfirmed txids to // `EsploraExt::update_tx_graph_without_keychain`. - let unconfirmed_txids = graph - .graph() + let unconfirmed_txids = tx_graph .list_canonical_txs(&*chain, local_tip.block_id()) .filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed()) .map(|canonical_tx| canonical_tx.tx_node.txid) @@ -361,6 +359,6 @@ fn main() -> anyhow::Result<()> { // We persist the changes let mut db = db.lock().unwrap(); - db.append_changeset(&(local_chain_changeset, indexed_tx_graph_changeset))?; + db.append_changeset(&(local_chain_changeset, tx_graph_changeset))?; Ok(()) }