From 23b568392f606ff833bafb8dfb1554f400e93fbd Mon Sep 17 00:00:00 2001 From: valued mammal Date: Sun, 2 Feb 2025 15:22:38 -0500 Subject: [PATCH 1/2] =?UTF-8?q?refactor(chain)!:=20remove=20IndexedTxGraph?= =?UTF-8?q?=20=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. - feat: allow changing the indexer on TxGraph Co-authored: LLFourn --- crates/bitcoind_rpc/examples/filter_iter.rs | 5 +- crates/bitcoind_rpc/tests/test_emitter.rs | 35 +- crates/chain/benches/canonicalization.rs | 12 +- crates/chain/src/canonical_iter.rs | 10 +- crates/chain/src/indexed_tx_graph.rs | 365 ------------- crates/chain/src/indexer.rs | 25 +- crates/chain/src/lib.rs | 2 - crates/chain/src/tx_graph.rs | 491 ++++++++++++++---- crates/chain/tests/test_indexed_tx_graph.rs | 48 +- crates/chain/tests/test_tx_graph.rs | 12 +- crates/electrum/tests/test_electrum.rs | 33 +- .../example_bitcoind_rpc_polling/src/main.rs | 13 +- example-crates/example_cli/src/lib.rs | 31 +- example-crates/example_electrum/src/main.rs | 37 +- example-crates/example_esplora/src/main.rs | 9 +- 15 files changed, 514 insertions(+), 614 deletions(-) delete mode 100644 crates/chain/src/indexed_tx_graph.rs diff --git a/crates/bitcoind_rpc/examples/filter_iter.rs b/crates/bitcoind_rpc/examples/filter_iter.rs index 55c3325d7..7dc88b60e 100644 --- a/crates/bitcoind_rpc/examples/filter_iter.rs +++ b/crates/bitcoind_rpc/examples/filter_iter.rs @@ -7,7 +7,7 @@ use bdk_chain::bitcoin::{constants::genesis_block, secp256k1::Secp256k1, Network use bdk_chain::indexer::keychain_txout::KeychainTxOutIndex; use bdk_chain::local_chain::LocalChain; use bdk_chain::miniscript::Descriptor; -use bdk_chain::{BlockId, ConfirmationBlockTime, IndexedTxGraph, SpkIterator}; +use bdk_chain::{BlockId, ConfirmationBlockTime, SpkIterator, TxGraph}; use bdk_testenv::anyhow; use bitcoin::Address; @@ -31,7 +31,7 @@ fn main() -> anyhow::Result<()> { let (descriptor, _) = Descriptor::parse_descriptor(&secp, EXTERNAL)?; let (change_descriptor, _) = Descriptor::parse_descriptor(&secp, INTERNAL)?; let (mut chain, _) = LocalChain::from_genesis_hash(genesis_block(NETWORK).block_hash()); - let mut graph = IndexedTxGraph::>::new({ + let mut graph = TxGraph::>::new({ let mut index = KeychainTxOutIndex::default(); index.insert_descriptor("external", descriptor.clone())?; index.insert_descriptor("internal", change_descriptor.clone())?; @@ -88,7 +88,6 @@ fn main() -> anyhow::Result<()> { println!("\ntook: {}s", start.elapsed().as_secs()); println!("Local tip: {}", chain.tip().height()); let unspent: Vec<_> = graph - .graph() .filter_chain_unspents( &chain, chain.tip().block_id(), diff --git a/crates/bitcoind_rpc/tests/test_emitter.rs b/crates/bitcoind_rpc/tests/test_emitter.rs index 14b0c9212..3185e6b34 100644 --- a/crates/bitcoind_rpc/tests/test_emitter.rs +++ b/crates/bitcoind_rpc/tests/test_emitter.rs @@ -5,7 +5,7 @@ use bdk_chain::{ bitcoin::{Address, Amount, Txid}, local_chain::{CheckPoint, LocalChain}, spk_txout::SpkTxOutIndex, - Balance, BlockId, IndexedTxGraph, Merge, + Balance, BlockId, Merge, TxGraph, }; use bdk_testenv::{anyhow, TestEnv}; use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash}; @@ -148,7 +148,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> { env.mine_blocks(101, None)?; 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()); @@ -161,8 +161,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 @@ -189,10 +189,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 - .tx_graph + tx_graph_changeset .txs .iter() .map(|tx| tx.compute_txid()) @@ -200,7 +199,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> { exp_txids, "changeset should have the 3 mempool transactions", ); - assert!(indexed_additions.tx_graph.anchors.is_empty()); + assert!(tx_graph_changeset.anchors.is_empty()); } // mine a block that confirms the 3 txs @@ -222,10 +221,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.tx_graph.txs.is_empty()); - assert!(indexed_additions.tx_graph.txouts.is_empty()); - assert_eq!(indexed_additions.tx_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(()) @@ -276,7 +275,7 @@ 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<()> { @@ -287,7 +286,7 @@ fn process_block( fn sync_from_emitter( recv_chain: &mut LocalChain, - recv_graph: &mut IndexedTxGraph>, + recv_graph: &mut TxGraph>, emitter: &mut Emitter, ) -> anyhow::Result<()> where @@ -302,13 +301,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 balance = recv_graph.balance(recv_chain, chain_tip, outpoints, |_, _| true); Ok(balance) } @@ -340,7 +337,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/benches/canonicalization.rs b/crates/chain/benches/canonicalization.rs index 6893e6df8..368d9036f 100644 --- a/crates/chain/benches/canonicalization.rs +++ b/crates/chain/benches/canonicalization.rs @@ -1,4 +1,4 @@ -use bdk_chain::{keychain_txout::KeychainTxOutIndex, local_chain::LocalChain, IndexedTxGraph}; +use bdk_chain::{keychain_txout::KeychainTxOutIndex, local_chain::LocalChain, TxGraph}; use bdk_core::{BlockId, CheckPoint}; use bdk_core::{ConfirmationBlockTime, TxUpdate}; use bdk_testenv::hash; @@ -11,7 +11,7 @@ use miniscript::{Descriptor, DescriptorPublicKey}; use std::sync::Arc; type Keychain = (); -type KeychainTxGraph = IndexedTxGraph>; +type KeychainTxGraph = TxGraph>; /// New tx guaranteed to have at least one output fn new_tx(lt: u32) -> Transaction { @@ -90,14 +90,12 @@ fn setup(f: F) -> (KeychainTxGraph, Lo } fn run_list_canonical_txs(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txs: usize) { - let txs = tx_graph - .graph() - .list_canonical_txs(chain, chain.tip().block_id()); + let txs = tx_graph.list_canonical_txs(chain, chain.tip().block_id()); assert_eq!(txs.count(), exp_txs); } fn run_filter_chain_txouts(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txos: usize) { - let utxos = tx_graph.graph().filter_chain_txouts( + let utxos = tx_graph.filter_chain_txouts( chain, chain.tip().block_id(), tx_graph.index.outpoints().clone(), @@ -106,7 +104,7 @@ fn run_filter_chain_txouts(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_t } fn run_filter_chain_unspents(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_utxos: usize) { - let utxos = tx_graph.graph().filter_chain_unspents( + let utxos = tx_graph.filter_chain_unspents( chain, chain.tip().block_id(), tx_graph.index.outpoints().clone(), diff --git a/crates/chain/src/canonical_iter.rs b/crates/chain/src/canonical_iter.rs index 99550ab7f..8f2b4d0d8 100644 --- a/crates/chain/src/canonical_iter.rs +++ b/crates/chain/src/canonical_iter.rs @@ -8,8 +8,8 @@ use bdk_core::BlockId; use bitcoin::{Transaction, Txid}; /// Iterates over canonical txs. -pub struct CanonicalIter<'g, A, C> { - tx_graph: &'g TxGraph, +pub struct CanonicalIter<'g, A, X, C> { + tx_graph: &'g TxGraph, chain: &'g C, chain_tip: BlockId, @@ -24,9 +24,9 @@ pub struct CanonicalIter<'g, A, C> { queue: VecDeque, } -impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> { +impl<'g, A: Anchor, X, C: ChainOracle> CanonicalIter<'g, A, X, C> { /// Constructs [`CanonicalIter`]. - pub fn new(tx_graph: &'g TxGraph, chain: &'g C, chain_tip: BlockId) -> Self { + pub fn new(tx_graph: &'g TxGraph, chain: &'g C, chain_tip: BlockId) -> Self { let anchors = tx_graph.all_anchors(); let pending_anchored = Box::new( tx_graph @@ -133,7 +133,7 @@ impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> { } } -impl Iterator for CanonicalIter<'_, A, C> { +impl Iterator for CanonicalIter<'_, A, X, C> { type Item = Result<(Txid, Arc, CanonicalReason), C::Error>; fn next(&mut self) -> Option { diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs deleted file mode 100644 index 45ed92aee..000000000 --- a/crates/chain/src/indexed_tx_graph.rs +++ /dev/null @@ -1,365 +0,0 @@ -//! Contains the [`IndexedTxGraph`] and associated types. Refer to the -//! [`IndexedTxGraph`] documentation for more. -use core::fmt::Debug; - -use alloc::{sync::Arc, vec::Vec}; -use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid}; - -use crate::{ - tx_graph::{self, TxGraph}, - Anchor, BlockId, Indexer, Merge, TxPosInBlock, -}; - -/// The [`IndexedTxGraph`] combines a [`TxGraph`] and an [`Indexer`] implementation. -/// -/// It ensures that [`TxGraph`] and [`Indexer`] are updated atomically. -#[derive(Debug, Clone)] -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.tx_graph.txs { - self.index.index_tx(tx); - } - for (&outpoint, txout) in &changeset.tx_graph.txouts { - self.index.index_txout(outpoint, txout); - } - - self.graph.apply_changeset(changeset.tx_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 { - tx_graph: 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 [`tx_graph::TxUpdate`] and the resultant changes is returned as [`ChangeSet`]. - pub fn apply_update(&mut self, update: tx_graph::TxUpdate) -> ChangeSet { - let tx_graph = self.graph.apply_update(update); - let indexer = self.index_tx_graph_changeset(&tx_graph); - ChangeSet { tx_graph, indexer } - } - - /// Insert a floating `txout` of given `outpoint`. - 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 { - tx_graph: graph, - indexer, - } - } - - /// Insert and index a transaction into the graph. - pub fn insert_tx>>(&mut self, tx: T) -> ChangeSet { - let tx_graph = self.graph.insert_tx(tx); - let indexer = self.index_tx_graph_changeset(&tx_graph); - ChangeSet { tx_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>>( - &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() - .map(|(tx, anchors)| (>>::into(tx), anchors)) - .collect::>(); - - let mut indexer = I::ChangeSet::default(); - for (tx, _) in &txs { - indexer.merge(self.index.index_tx(tx)); - } - - let mut tx_graph = tx_graph::ChangeSet::default(); - for (tx, anchors) in txs { - if self.index.is_tx_relevant(&tx) { - let txid = tx.compute_txid(); - tx_graph.merge(self.graph.insert_tx(tx.clone())); - for anchor in anchors { - tx_graph.merge(self.graph.insert_anchor(txid, anchor)); - } - } - } - - ChangeSet { tx_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>>( - &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() - .map(|(tx, last_seen)| (>>::into(tx), last_seen)) - .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 { - tx_graph: 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 { - tx_graph: graph, - indexer, - } - } -} - -/// Methods are available if the anchor (`A`) can be created from [`TxPosInBlock`]. -impl IndexedTxGraph -where - I::ChangeSet: Default + Merge, - for<'b> A: Anchor + From>, - I: Indexer, -{ - /// Batch insert all transactions of the given `block` of `height`, filtering out those that are - /// irrelevant. - /// - /// Each inserted transaction's anchor will be constructed using [`TxPosInBlock`]. - /// - /// 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 = TxPosInBlock { - block, - block_id, - tx_pos, - } - .into(); - changeset.tx_graph.merge(self.graph.insert_tx(tx.clone())); - changeset - .tx_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 using [`TxPosInBlock`]. - /// - /// 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 = TxPosInBlock { - block: &block, - block_id, - tx_pos, - } - .into(); - 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 { - tx_graph: graph, - indexer, - } - } -} - -impl AsRef> for IndexedTxGraph { - fn as_ref(&self) -> &TxGraph { - &self.graph - } -} - -/// Represents changes to an [`IndexedTxGraph`]. -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr( - feature = "serde", - derive(serde::Deserialize, serde::Serialize), - serde(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 tx_graph: tx_graph::ChangeSet, - /// [`Indexer`] changeset. - pub indexer: IA, -} - -impl Default for ChangeSet { - fn default() -> Self { - Self { - tx_graph: Default::default(), - indexer: Default::default(), - } - } -} - -impl Merge for ChangeSet { - fn merge(&mut self, other: Self) { - self.tx_graph.merge(other.tx_graph); - self.indexer.merge(other.indexer); - } - - fn is_empty(&self) -> bool { - self.tx_graph.is_empty() && self.indexer.is_empty() - } -} - -impl From> for ChangeSet { - fn from(graph: tx_graph::ChangeSet) -> Self { - Self { - tx_graph: graph, - ..Default::default() - } - } -} - -#[cfg(feature = "miniscript")] -impl From for ChangeSet { - fn from(indexer: crate::keychain_txout::ChangeSet) -> Self { - Self { - tx_graph: Default::default(), - indexer, - } - } -} diff --git a/crates/chain/src/indexer.rs b/crates/chain/src/indexer.rs index 22e839815..bc2dbc815 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: Default + crate::Merge; /// 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 92a6d5c4e..7a873d09f 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -31,8 +31,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 d40ee49d3..9a1867926 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 @@ -36,6 +34,8 @@ //! //! # Generics //! +//! ## Anchors +//! //! Anchors are represented as generics within `TxGraph`. To make use of all functionality of the //! `TxGraph`, anchors (`A`) should implement [`Anchor`]. //! @@ -46,6 +46,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 whenever new +//! transaction data is attached to the graph. Usually this is instantiated with indexers like +//! [`KeychainTxOutIndex`] to keep track of transaction outputs owned by an output descriptor. +//! +//! # Examples +//! //! ``` //! # use bdk_chain::BlockId; //! # use bdk_chain::tx_graph::TxGraph; @@ -75,41 +83,44 @@ //! # use std::sync::Arc; //! # let tx_a = tx_from_hex(RAW_TX_1); //! # let tx_b = tx_from_hex(RAW_TX_2); -//! let mut graph: TxGraph = TxGraph::default(); +//! let mut tx_graph: TxGraph = TxGraph::default(); //! //! let mut update = tx_graph::TxUpdate::default(); //! update.txs.push(Arc::new(tx_a)); //! update.txs.push(Arc::new(tx_b)); //! //! // apply the update graph -//! let changeset = graph.apply_update(update.clone()); +//! 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()); //! ``` //! [`insert_txout`]: TxGraph::insert_txout +//! [`KeychainTxOutIndex`]: crate::indexer::keychain_txout::KeychainTxOutIndex use crate::collections::*; use crate::BlockId; use crate::CanonicalIter; use crate::CanonicalReason; +use crate::Indexer; use crate::ObservedIn; +use crate::TxPosInBlock; use crate::{Anchor, Balance, ChainOracle, ChainPosition, FullTxOut, Merge}; use alloc::collections::vec_deque::VecDeque; use alloc::sync::Arc; use alloc::vec::Vec; use bdk_core::ConfirmationBlockTime; pub use bdk_core::TxUpdate; -use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxOut, Txid}; +use bitcoin::{Amount, Block, OutPoint, ScriptBuf, SignedAmount, Transaction, TxOut, Txid}; use core::fmt::{self, Formatter}; use core::{ convert::Infallible, ops::{Deref, RangeInclusive}, }; -impl From> for TxUpdate { - fn from(graph: TxGraph) -> Self { +impl From> for TxUpdate { + fn from(graph: TxGraph) -> Self { let mut tx_update = TxUpdate::default(); tx_update.txs = graph.full_txs().map(|tx_node| tx_node.tx).collect(); tx_update.txouts = graph @@ -140,7 +151,7 @@ impl From> for TxGraph { /// /// [module-level documentation]: crate::tx_graph #[derive(Clone, Debug, PartialEq)] -pub struct TxGraph { +pub struct TxGraph { txs: HashMap, spends: BTreeMap>, anchors: HashMap>, @@ -153,9 +164,16 @@ pub struct TxGraph { // FIXME: This can be removed once `HashSet::new` and `BTreeSet::new` are const fns. empty_outspends: HashSet, empty_anchors: BTreeSet, + + /// Indexer. This is called to index all transactions and outpoints added to the transaction + /// graph. + pub index: X, } -impl Default for TxGraph { +impl Default for TxGraph +where + X: Default, +{ fn default() -> Self { Self { txs: Default::default(), @@ -166,6 +184,7 @@ impl Default for TxGraph { txs_by_last_seen: Default::default(), empty_outspends: Default::default(), empty_anchors: Default::default(), + index: Default::default(), } } } @@ -201,6 +220,19 @@ enum TxNodeInternal { Partial(BTreeMap), } +impl TxNodeInternal { + /// Iterate over outpoint and txouts of a partial tx node. The caller is + /// responsible for matching the given `partial` with the correct `txid`. + fn iter_txouts( + txid: Txid, + partial: &BTreeMap, + ) -> impl Iterator { + partial + .iter() + .map(move |(&vout, txout)| (OutPoint { txid, vout }, txout)) + } +} + impl Default for TxNodeInternal { fn default() -> Self { Self::Partial(BTreeMap::new()) @@ -245,23 +277,22 @@ 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 .iter() .enumerate() - .map(|(vout, txout)| (OutPoint::new(*txid, vout as _), txout)) - .collect::>(), - TxNodeInternal::Partial(txouts) => txouts - .iter() - .map(|(vout, txout)| (OutPoint::new(*txid, *vout as _), txout)) + .map(|(vout, txout)| (OutPoint::new(txid, vout as _), txout)) .collect::>(), + TxNodeInternal::Partial(partial) => { + TxNodeInternal::iter_txouts(txid, partial).collect::>() + } }) } @@ -272,13 +303,11 @@ 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 - .iter() - .map(|(&vout, txout)| (OutPoint::new(*txid, vout), txout)), - ), + TxNodeInternal::Partial(partial) => { + Some(TxNodeInternal::iter_txouts(txid, partial)) + } }) .flatten() } @@ -289,7 +318,7 @@ impl TxGraph { TxNodeInternal::Whole(tx) => Some(TxNode { txid, tx: tx.clone(), - anchors: self.anchors.get(&txid).unwrap_or(&self.empty_anchors), + anchors: self.get_anchors(txid), last_seen_unconfirmed: self.last_seen.get(&txid).copied(), }), TxNodeInternal::Partial(_) => None, @@ -324,7 +353,7 @@ impl TxGraph { TxNodeInternal::Whole(tx) => Some(TxNode { txid, tx: tx.clone(), - anchors: self.anchors.get(&txid).unwrap_or(&self.empty_anchors), + anchors: self.get_anchors(txid), last_seen_unconfirmed: self.last_seen.get(&txid).copied(), }), _ => None, @@ -339,6 +368,11 @@ impl TxGraph { } } + /// 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`. @@ -428,7 +462,15 @@ impl TxGraph { } } -impl TxGraph { +impl TxGraph { + /// Return the anchors as elements of a reverse map + fn rev_anchors(&self) -> BTreeSet<(A, Txid)> { + self.anchors + .iter() + .flat_map(|(&txid, anchors)| anchors.iter().cloned().map(move |a| (a, txid))) + .collect() + } + /// Creates an iterator that filters and maps ancestor transactions. /// /// The iterator starts with the ancestors of the supplied `tx` (ancestor transactions of `tx` @@ -442,7 +484,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, O> + pub fn walk_ancestors<'g, T, F, O>(&'g self, tx: T, walk_map: F) -> TxAncestors<'g, A, X, F, O> where T: Into>, F: FnMut(usize, Arc) -> Option + 'g, @@ -464,7 +506,7 @@ impl TxGraph { &'g self, txid: Txid, walk_map: F, - ) -> TxDescendants<'g, A, F, O> + ) -> TxDescendants<'g, A, X, F, O> where F: FnMut(usize, Txid) -> Option + 'g, { @@ -472,7 +514,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). /// @@ -481,7 +523,7 @@ impl TxGraph { &'g self, tx: &'g Transaction, walk_map: F, - ) -> TxDescendants<'g, A, F, O> + ) -> TxDescendants<'g, A, X, F, O> where F: FnMut(usize, Txid) -> Option + 'g, { @@ -520,27 +562,45 @@ 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)); + let new_anchors = self.rev_anchors().into_iter().map(|(a, txid)| (f(a), txid)); + + let mut new_graph: TxGraph = TxGraph { + txs: self.txs, + spends: self.spends, + anchors: Default::default(), + last_seen: self.last_seen, + txs_by_highest_conf_heights: self.txs_by_highest_conf_heights, + txs_by_last_seen: self.txs_by_last_seen, + empty_anchors: Default::default(), + empty_outspends: Default::default(), + index: self.index, + }; + + for (new_anchor, txid) in new_anchors { + let _ = new_graph.insert_anchor(txid, new_anchor); + } + new_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); + /// Construct a new [`TxGraph`] from [`Indexer`]. + pub fn new(indexer: X) -> Self + where + X: Default, + { + Self { + index: indexer, + ..Default::default() } - new } /// Inserts the given [`TxOut`] at [`OutPoint`]. @@ -552,8 +612,8 @@ 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 changeset = ChangeSet::::default(); + pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet { + let mut changeset = ChangeSet::default(); let tx_node = self.txs.entry(outpoint.txid).or_default(); match tx_node { TxNodeInternal::Whole(_) => { @@ -576,16 +636,17 @@ impl TxGraph { } } } + 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: Arc = tx.into(); let txid = tx.compute_txid(); - let mut changeset = ChangeSet::::default(); + let mut changeset = ChangeSet::default(); let tx_node = self.txs.entry(txid).or_default(); match tx_node { @@ -612,6 +673,20 @@ impl TxGraph { } } + self.index_changeset(&mut changeset); + + changeset + } + + /// Inserts a batch 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 } @@ -623,8 +698,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 { let tx: Arc = tx.into(); changeset.merge(self.insert_seen_at(tx.compute_txid(), seen_at)); @@ -633,11 +708,88 @@ 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>>( + &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() + .map(|(tx, anchors)| (>>::into(tx), anchors)) + .collect::>(); + + let mut changeset = ChangeSet::default(); + + for (tx, _) in &txs { + changeset.merge(self.index.index_tx(tx).into()); + } + + for (tx, anchors) in txs { + if self.index.is_tx_relevant(&tx) { + let txid = tx.compute_txid(); + changeset.merge(self.insert_tx(tx.clone())); + for anchor in anchors { + changeset.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>>( + &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 txs = unconfirmed_txs + .into_iter() + .map(|(tx, last_seen)| (>>::into(tx), last_seen)) + .collect::>(); + + let mut changeset = ChangeSet::::default(); + for (tx, _) in &txs { + changeset.indexer.merge(self.index.index_tx(tx)); + } + + txs.retain(|(tx, _)| self.index.is_tx_relevant(tx)); + + changeset.merge(self.batch_insert_unconfirmed(txs)); + + 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 { + pub fn insert_anchor(&mut self, txid: Txid, anchor: A) -> ChangeSet { // These two variables are used to determine how to modify the `txid`'s entry in // `txs_by_highest_conf_heights`. // We want to remove `(old_top_h?, txid)` and insert `(new_top_h?, txid)`. @@ -665,7 +817,7 @@ impl TxGraph { } }; - let mut changeset = ChangeSet::::default(); + let mut changeset = ChangeSet::default(); if is_changed { let new_top_is_changed = match old_top_h { None => true, @@ -686,7 +838,7 @@ impl TxGraph { /// Inserts the given `seen_at` for `txid` into [`TxGraph`]. /// /// Note that [`TxGraph`] only keeps track of the latest `seen_at`. - pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet { + pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet { let mut old_last_seen = None; let is_changed = match self.last_seen.entry(txid) { hash_map::Entry::Occupied(mut e) => { @@ -704,7 +856,7 @@ impl TxGraph { } }; - let mut changeset = ChangeSet::::default(); + let mut changeset = ChangeSet::default(); if is_changed { if let Some(old_last_seen) = old_last_seen { self.txs_by_last_seen.remove(&(old_last_seen, txid)); @@ -719,8 +871,8 @@ impl TxGraph { /// /// The returned [`ChangeSet`] is the set difference between `update` and `self` (transactions that /// exist in `update` but not in `self`). - pub fn apply_update(&mut self, update: TxUpdate) -> ChangeSet { - let mut changeset = ChangeSet::::default(); + pub fn apply_update(&mut self, update: TxUpdate) -> ChangeSet { + let mut changeset = ChangeSet::::default(); for tx in update.txs { changeset.merge(self.insert_tx(tx)); } @@ -733,28 +885,91 @@ impl TxGraph { for (txid, seen_at) in update.seen_ats { changeset.merge(self.insert_seen_at(txid, seen_at)); } + self.index_changeset(&mut changeset); + changeset + } + + /// Changes the [`Indexer`] for a `TxGraph`. **This doesn't re-index the transactions in the + /// graph**. To do that that call [`reindex`]. + /// + /// Returns the new `TxGraph` and the old indexer. + /// + /// [`reindex`]: Self::reindex + pub fn swap_indexer(self, indexer: X2) -> (TxGraph, X) { + ( + TxGraph { + txs: self.txs, + spends: self.spends, + anchors: self.anchors, + last_seen: self.last_seen, + txs_by_highest_conf_heights: self.txs_by_highest_conf_heights, + txs_by_last_seen: self.txs_by_last_seen, + empty_outspends: self.empty_outspends, + empty_anchors: self.empty_anchors, + index: indexer, + }, + self.index, + ) + } + + /// Reindexes the transaction graph by calling the [`Indexer`] on every transaction. + /// + /// The returned changeset will only be non-empty in the `indexer` field. + pub fn reindex(&mut self) -> ChangeSet { + let mut changeset = ChangeSet::::default(); + for (&txid, node) in &self.txs { + match node { + TxNodeInternal::Whole(tx) => { + changeset.merge(self.index.index_tx(tx.as_ref()).into()) + } + TxNodeInternal::Partial(txouts) => { + for (op, txout) in TxNodeInternal::iter_txouts(txid, txouts) { + changeset.merge(self.index.index_txout(op, txout).into()); + } + } + }; + } changeset } /// Determines the [`ChangeSet`] between `self` and an empty [`TxGraph`]. - pub fn initial_changeset(&self) -> ChangeSet { + pub fn initial_changeset(&self) -> ChangeSet { ChangeSet { txs: self.full_txs().map(|tx_node| tx_node.tx).collect(), txouts: self .floating_txouts() .map(|(op, txout)| (op, txout.clone())) .collect(), - anchors: self - .anchors - .iter() - .flat_map(|(txid, anchors)| anchors.iter().map(|a| (a.clone(), *txid))) - .collect(), - last_seen: self.last_seen.iter().map(|(&k, &v)| (k, v)).collect(), + anchors: self.rev_anchors(), + last_seen: self.last_seen.clone().into_iter().collect(), + indexer: self.index.initial_changeset(), + } + } + + /// Indexes the txs and txouts of `changeset` and merges the resulting changes. + fn index_changeset(&mut self, changeset: &mut ChangeSet) { + let indexer = &mut changeset.indexer; + for tx in &changeset.txs { + indexer.merge(self.index.index_tx(tx)); + } + for (&outpoint, txout) in &changeset.txouts { + indexer.merge(self.index.index_txout(outpoint, txout)); } } /// Applies [`ChangeSet`] to [`TxGraph`]. - pub fn apply_changeset(&mut self, changeset: ChangeSet) { + 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 that first. + self.index.apply_changeset(changeset.indexer); + + for tx in &changeset.txs { + self.index.index_tx(tx); + } + for (&outpoint, txout) in &changeset.txouts { + self.index.index_txout(outpoint, txout); + } + for tx in changeset.txs { let _ = self.insert_tx(tx); } @@ -770,7 +985,7 @@ impl TxGraph { } } -impl TxGraph { +impl TxGraph { /// List graph transactions that are in `chain` with `chain_tip`. /// /// Each transaction is represented as a [`CanonicalTx`] that contains where the transaction is @@ -947,7 +1162,7 @@ impl TxGraph { &'a self, chain: &'a C, chain_tip: BlockId, - ) -> CanonicalIter<'a, A, C> { + ) -> CanonicalIter<'a, A, X, C> { CanonicalIter::new(self, chain, chain_tip) } @@ -1080,25 +1295,89 @@ impl TxGraph { } } +/// Methods are available if the anchor (`A`) can be created from [`TxPosInBlock`]. +impl TxGraph +where + for<'b> A: Anchor + From>, + X: Indexer, + X::ChangeSet: Default + Merge, +{ + /// Batch insert all transactions of the given `block` of `height`, filtering out those that are + /// irrelevant. + /// + /// Each inserted transaction's anchor will be constructed using [`TxPosInBlock`]. + /// + /// Relevancy is determined by the internal [`Indexer::is_tx_relevant`] implementation of `X`. + /// 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.merge(self.index.index_tx(tx).into()); + if self.index.is_tx_relevant(tx) { + let anchor = TxPosInBlock { + block, + block_id, + tx_pos, + }; + changeset.merge(self.insert_tx(tx.clone())); + changeset.merge(self.insert_anchor(tx.compute_txid(), anchor.into())); + } + } + changeset + } + + /// Batch insert all transactions of the given `block` of `height`. + /// + /// Each inserted transaction's anchor will be constructed using [`TxPosInBlock`]. + /// + /// To only insert relevant transactions, use [`apply_block_relevant`] instead. + /// + /// [`apply_block_relevant`]: Self::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 changeset = ChangeSet::default(); + for (tx_pos, tx) in block.txdata.iter().enumerate() { + changeset.merge(self.index.index_tx(tx).into()); + let anchor = TxPosInBlock { + block: &block, + block_id, + tx_pos, + }; + changeset.merge(self.insert_tx(tx.clone())); + changeset.merge(self.insert_anchor(tx.compute_txid(), anchor.into())); + } + changeset + } +} + /// The [`ChangeSet`] represents changes to a [`TxGraph`]. /// /// Since [`TxGraph`] is monotone, the "changeset" can only contain transactions to be added and /// not removed. /// -/// Refer to [module-level documentation] for more. -/// -/// [module-level documentation]: crate::tx_graph +/// Refer to [module-level documentation](self) for more. #[derive(Debug, Clone, PartialEq)] #[cfg_attr( feature = "serde", derive(serde::Deserialize, serde::Serialize), serde(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. @@ -1107,20 +1386,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(indexer: X) -> Self { + Self { + indexer, + 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 @@ -1154,7 +1448,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 @@ -1170,6 +1464,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 { @@ -1177,15 +1472,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, { @@ -1196,12 +1492,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 } } @@ -1213,23 +1510,23 @@ impl AsRef> for TxGraph { /// Returned by the [`walk_ancestors`] method of [`TxGraph`]. /// /// [`walk_ancestors`]: TxGraph::walk_ancestors -pub struct TxAncestors<'g, A, F, O> +pub struct TxAncestors<'g, A, X, F, O> where F: FnMut(usize, Arc) -> Option, { - graph: &'g TxGraph, + graph: &'g TxGraph, visited: HashSet, queue: VecDeque<(usize, Arc)>, filter_map: F, } -impl<'g, A, F, O> TxAncestors<'g, A, F, O> +impl<'g, A, X, F, O> TxAncestors<'g, A, X, F, O> where F: FnMut(usize, Arc) -> Option, { /// 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 { @@ -1243,7 +1540,7 @@ where /// 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 { @@ -1261,7 +1558,7 @@ where /// `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 @@ -1281,7 +1578,7 @@ where /// `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 @@ -1318,7 +1615,7 @@ where } } -impl Iterator for TxAncestors<'_, A, F, O> +impl Iterator for TxAncestors<'_, A, X, F, O> where F: FnMut(usize, Arc) -> Option, { @@ -1344,23 +1641,23 @@ where /// Returned by the [`walk_descendants`] method of [`TxGraph`]. /// /// [`walk_descendants`]: TxGraph::walk_descendants -pub struct TxDescendants<'g, A, F, O> +pub struct TxDescendants<'g, A, X, F, O> where F: FnMut(usize, Txid) -> Option, { - graph: &'g TxGraph, + graph: &'g TxGraph, visited: HashSet, queue: VecDeque<(usize, Txid)>, filter_map: F, } -impl<'g, A, F, O> TxDescendants<'g, A, F, O> +impl<'g, A, X, F, O> TxDescendants<'g, A, X, F, O> where F: FnMut(usize, Txid) -> Option, { /// 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(), @@ -1370,7 +1667,7 @@ where } /// 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(), @@ -1384,7 +1681,7 @@ where /// 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 @@ -1403,7 +1700,7 @@ where /// `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 @@ -1438,7 +1735,7 @@ where } } -impl Iterator for TxDescendants<'_, A, F, O> +impl Iterator for TxDescendants<'_, A, X, F, O> 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 1e28eb6a2..bce83b9d8 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -6,10 +6,8 @@ mod common; use std::{collections::BTreeSet, sync::Arc}; use bdk_chain::{ - indexed_tx_graph::{self, IndexedTxGraph}, - indexer::keychain_txout::KeychainTxOutIndex, - local_chain::LocalChain, - tx_graph, Balance, ChainPosition, ConfirmationBlockTime, DescriptorExt, + indexer::keychain_txout::KeychainTxOutIndex, local_chain::LocalChain, tx_graph, Balance, + ChainPosition, ConfirmationBlockTime, DescriptorExt, TxGraph, }; use bdk_testenv::{ block_id, hash, @@ -18,7 +16,7 @@ use bdk_testenv::{ use bitcoin::{secp256k1::Secp256k1, Amount, OutPoint, ScriptBuf, Transaction, TxIn, TxOut}; use miniscript::Descriptor; -/// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented +/// Ensure [`TxGraph::batch_insert_relevant`] 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. @@ -33,9 +31,8 @@ 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 .insert_descriptor((), descriptor.clone()) @@ -73,14 +70,12 @@ fn insert_relevant_txs() { let txs = [tx_c, tx_b, tx_a]; - let changeset = indexed_tx_graph::ChangeSet { - tx_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(), }, + ..Default::default() }; assert_eq!( @@ -89,17 +84,18 @@ fn insert_relevant_txs() { ); // The initial changeset will also contain info about the keychain we added - let initial_changeset = indexed_tx_graph::ChangeSet { - tx_graph: changeset.tx_graph, + let initial_changeset = tx_graph::ChangeSet { + txs: changeset.txs, indexer: keychain_txout::ChangeSet { last_revealed: changeset.indexer.last_revealed, }, + ..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,14 +129,14 @@ fn test_list_owned_txouts() { LocalChain::from_blocks((0..150).map(|i| (i as u32, hash!("random"))).collect()) .expect("must have genesis hash"); - // Initiate IndexedTxGraph + // Initiate TxGraph let (desc_1, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTORS[2]).unwrap(); let (desc_2, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTORS[3]).unwrap(); - let mut graph = IndexedTxGraph::>::new( + let mut graph = TxGraph::>::new( KeychainTxOutIndex::new(10), ); @@ -261,13 +257,12 @@ 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, @@ -276,7 +271,6 @@ fn test_list_owned_txouts() { .collect::>(); let utxos = graph - .graph() .filter_chain_unspents( &local_chain, chain_tip, @@ -284,7 +278,7 @@ fn test_list_owned_txouts() { ) .collect::>(); - let balance = graph.graph().balance( + let balance = graph.balance( &local_chain, chain_tip, graph.index.outpoints().iter().cloned(), @@ -446,6 +440,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() @@ -524,7 +519,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 check the canonical /// position of the tx: /// @@ -549,7 +544,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 @@ -561,11 +556,11 @@ 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 along with any anchors and + // The test will insert a transaction into the tx graph along with any anchors and // timestamps, then check the tx's canonical position is expected. fn run( chain: &LocalChain, - graph: &mut IndexedTxGraph>, + graph: &mut TxGraph>, test: TestCase, ) { let TestCase { @@ -588,7 +583,6 @@ fn test_get_chain_position() { // check chain position let chain_pos = graph - .graph() .list_canonical_txs(chain, chain.tip().block_id()) .find_map(|canon_tx| { if canon_tx.tx_node.txid == txid { diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index eef5e2239..c680d1687 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -115,7 +115,8 @@ fn insert_txouts() { txs: [Arc::new(update_tx.clone())].into(), txouts: update_ops.clone().into(), anchors: [(conf_anchor, update_tx.compute_txid()),].into(), - last_seen: [(hash!("tx2"), 1000000)].into() + last_seen: [(hash!("tx2"), 1000000)].into(), + ..Default::default() } ); @@ -168,7 +169,8 @@ fn insert_txouts() { txs: [Arc::new(update_tx.clone())].into(), txouts: update_ops.into_iter().chain(original_ops).collect(), anchors: [(conf_anchor, update_tx.compute_txid()),].into(), - last_seen: [(hash!("tx2"), 1000000)].into() + last_seen: [(hash!("tx2"), 1000000)].into(), + ..Default::default() } ); } @@ -588,7 +590,8 @@ fn test_walk_ancestors() { ..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(), @@ -1049,7 +1052,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/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index da15e9803..9360a92db 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -3,7 +3,7 @@ use bdk_chain::{ local_chain::LocalChain, spk_client::{FullScanRequest, SyncRequest, SyncResponse}, spk_txout::SpkTxOutIndex, - Balance, ConfirmationBlockTime, IndexedTxGraph, Indexer, Merge, TxGraph, + Balance, ConfirmationBlockTime, Indexer, Merge, TxGraph, }; use bdk_core::bitcoin::Network; use bdk_electrum::BdkElectrumClient; @@ -22,13 +22,11 @@ const BATCH_SIZE: usize = 5; 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 balance = recv_graph.balance(recv_chain, chain_tip, outpoints, |_, _| true); Ok(balance) } @@ -36,7 +34,7 @@ fn sync_with_electrum( client: &BdkElectrumClient, spks: Spks, chain: &mut LocalChain, - graph: &mut IndexedTxGraph, + graph: &mut TxGraph, ) -> anyhow::Result where I: Indexer, @@ -379,7 +377,7 @@ fn test_sync() -> 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 @@ -469,13 +467,10 @@ fn test_sync() -> anyhow::Result<()> { // Check to see if we have the floating txouts available from our transactions' previous outputs // in order to calculate transaction fees. - 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 @@ -522,7 +517,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 @@ -609,7 +604,7 @@ fn test_sync_with_coinbase() -> 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 @@ -644,7 +639,7 @@ fn test_check_fee_calculation() -> 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 @@ -705,7 +700,6 @@ fn test_check_fee_calculation() -> anyhow::Result<()> { // Check the graph update contains the right floating txout let graph_txout = recv_graph - .graph() .all_txouts() .find(|(_op, txout)| txout.value == prev_amt) .unwrap(); @@ -720,13 +714,10 @@ fn test_check_fee_calculation() -> 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"); // Check the fee calculated fee matches the initial fee amount assert_eq!(fee, FEE_AMOUNT); diff --git a/example-crates/example_bitcoind_rpc_polling/src/main.rs b/example-crates/example_bitcoind_rpc_polling/src/main.rs index 83cb25f8a..5aec59b50 100644 --- a/example-crates/example_bitcoind_rpc_polling/src/main.rs +++ b/example-crates/example_bitcoind_rpc_polling/src/main.rs @@ -159,8 +159,7 @@ fn main() -> anyhow::Result<()> { let graph_changeset = graph.apply_block_relevant(&emission.block, height); db_stage.merge(ChangeSet { local_chain: chain_changeset, - tx_graph: graph_changeset.tx_graph, - indexer: graph_changeset.indexer, + tx_graph: graph_changeset, ..Default::default() }); @@ -183,7 +182,7 @@ 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(), @@ -208,8 +207,7 @@ fn main() -> anyhow::Result<()> { { let db = &mut *db.lock().unwrap(); db_stage.merge(ChangeSet { - tx_graph: graph_changeset.tx_graph, - indexer: graph_changeset.indexer, + tx_graph: graph_changeset, ..Default::default() }); if let Some(changeset) = db_stage.take() { @@ -298,8 +296,7 @@ fn main() -> anyhow::Result<()> { db_stage.merge(ChangeSet { local_chain: chain_changeset, - tx_graph: graph_changeset.tx_graph, - indexer: graph_changeset.indexer, + tx_graph: graph_changeset, ..Default::default() }); @@ -320,7 +317,7 @@ 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(), diff --git a/example-crates/example_cli/src/lib.rs b/example-crates/example_cli/src/lib.rs index e965495e0..4f1706021 100644 --- a/example-crates/example_cli/src/lib.rs +++ b/example-crates/example_cli/src/lib.rs @@ -21,10 +21,9 @@ use bdk_chain::miniscript::{ }; use bdk_chain::ConfirmationBlockTime; use bdk_chain::{ - indexed_tx_graph, indexer::keychain_txout::{self, KeychainTxOutIndex}, local_chain::{self, LocalChain}, - tx_graph, ChainOracle, DescriptorExt, FullTxOut, IndexedTxGraph, Merge, + tx_graph, ChainOracle, DescriptorExt, FullTxOut, Merge, TxGraph, }; use bdk_coin_select::{ metrics::LowestFee, Candidate, ChangePolicy, CoinSelector, DrainWeights, FeeRate, Target, @@ -37,8 +36,8 @@ use rand::prelude::*; pub use anyhow; pub use clap; -/// Alias for a `IndexedTxGraph` with specific `Anchor` and `Indexer`. -pub type KeychainTxGraph = IndexedTxGraph>; +/// Alias for a `TxGraph` with specific `Anchor` and `Indexer`. +pub type KeychainTxGraph = TxGraph>; /// ChangeSet #[derive(Default, Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)] @@ -51,10 +50,8 @@ pub struct ChangeSet { pub network: Option, /// Changes to the [`LocalChain`]. pub local_chain: local_chain::ChangeSet, - /// Changes to [`TxGraph`](tx_graph::TxGraph). - pub tx_graph: tx_graph::ChangeSet, - /// Changes to [`KeychainTxOutIndex`]. - pub indexer: keychain_txout::ChangeSet, + /// Changes to [`TxGraph`]. + pub tx_graph: tx_graph::ChangeSet, } #[derive(Parser)] @@ -420,7 +417,6 @@ pub fn planned_utxos( let chain_tip = chain.get_chain_tip()?; let outpoints = graph.index.outpoints(); graph - .graph() .try_filter_chain_unspents(chain, chain_tip, outpoints.iter().cloned())? .filter_map(|((k, i), full_txo)| -> Option> { let desc = graph @@ -467,7 +463,7 @@ pub fn handle_commands( spk_chooser(index, Keychain::External).expect("Must exist"); let db = &mut *db.lock().unwrap(); db.append(&ChangeSet { - indexer: index_changeset, + tx_graph: index_changeset.into(), ..Default::default() })?; let addr = Address::from_script(spk.as_script(), network)?; @@ -512,7 +508,7 @@ pub fn handle_commands( } } - let balance = graph.graph().try_balance( + let balance = graph.try_balance( chain, chain.get_chain_tip()?, graph.index.outpoints().iter().cloned(), @@ -555,7 +551,6 @@ pub fn handle_commands( unconfirmed, } => { let txouts = graph - .graph() .try_filter_chain_txouts(chain, chain_tip, outpoints.iter().cloned())? .filter(|(_, full_txo)| match (spent, unspent) { (true, false) => full_txo.spent_by.is_some(), @@ -630,7 +625,7 @@ pub fn handle_commands( { let db = &mut *db.lock().unwrap(); db.append(&ChangeSet { - indexer, + tx_graph: indexer.into(), ..Default::default() })?; } @@ -720,8 +715,7 @@ pub fn handle_commands( // it's not a big deal since we can always find it again from the // blockchain. db.lock().unwrap().append(&ChangeSet { - tx_graph: changeset.tx_graph, - indexer: changeset.indexer, + tx_graph: changeset, ..Default::default() })?; } @@ -813,10 +807,7 @@ pub fn init_or_load( index.insert_descriptor(Keychain::Internal, change_desc)?; } let mut graph = KeychainTxGraph::new(index); - graph.apply_changeset(indexed_tx_graph::ChangeSet { - tx_graph: changeset.tx_graph, - indexer: changeset.indexer, - }); + graph.apply_changeset(changeset.tx_graph); graph }); @@ -925,7 +916,6 @@ impl Merge for ChangeSet { } Merge::merge(&mut self.local_chain, other.local_chain); Merge::merge(&mut self.tx_graph, other.tx_graph); - Merge::merge(&mut self.indexer, other.indexer); } fn is_empty(&self) -> bool { @@ -934,6 +924,5 @@ impl Merge for ChangeSet { && self.network.is_none() && self.local_chain.is_empty() && self.tx_graph.is_empty() - && self.indexer.is_empty() } } diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index 8e3110d68..90a6bb685 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -3,9 +3,8 @@ use std::io::{self, Write}; use bdk_chain::{ bitcoin::Network, collections::BTreeSet, - indexed_tx_graph, spk_client::{FullScanRequest, SyncRequest}, - ConfirmationBlockTime, Merge, + tx_graph, Merge, }; use bdk_electrum::{ electrum_client::{self, Client, ElectrumApi}, @@ -127,14 +126,7 @@ fn main() -> anyhow::Result<()> { let client = BdkElectrumClient::new(electrum_cmd.electrum_args().client(network)?); // 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() - .graph() - .full_txs() - .map(|tx_node| tx_node.tx), - ); + client.populate_tx_cache(graph.lock().unwrap().full_txs().map(|tx_node| tx_node.tx)); let (chain_update, tx_update, keychain_update) = match electrum_cmd.clone() { ElectrumCommands::Scan { @@ -177,13 +169,13 @@ fn main() -> anyhow::Result<()> { }) }; - let res = client + let resp = client .full_scan::<_>(request, stop_gap, scan_options.batch_size, false) .context("scanning the blockchain")?; ( - res.chain_update, - res.tx_update, - Some(res.last_active_indices), + resp.chain_update, + resp.tx_update, + Some(resp.last_active_indices), ) } ElectrumCommands::Sync { @@ -225,7 +217,6 @@ fn main() -> anyhow::Result<()> { let init_outpoints = graph.index.outpoints(); request = request.outpoints( graph - .graph() .filter_chain_unspents( &*chain, chain_tip.block_id(), @@ -237,21 +228,20 @@ fn main() -> anyhow::Result<()> { if unconfirmed { request = request.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), ); } - let res = client + let resp = 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.tx_update, None) + (resp.chain_update, resp.tx_update, None) } }; @@ -261,18 +251,15 @@ fn main() -> anyhow::Result<()> { let chain_changeset = chain.apply_update(chain_update.expect("request has chain tip"))?; - let mut indexed_tx_graph_changeset = - indexed_tx_graph::ChangeSet::::default(); + let mut tx_graph_changeset = 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()); + tx_graph_changeset.merge(graph.index.reveal_to_target_multi(&keychain_update).into()); } - indexed_tx_graph_changeset.merge(graph.apply_update(tx_update)); + tx_graph_changeset.merge(graph.apply_update(tx_update)); ChangeSet { local_chain: chain_changeset, - tx_graph: indexed_tx_graph_changeset.tx_graph, - indexer: indexed_tx_graph_changeset.indexer, + tx_graph: tx_graph_changeset, ..Default::default() } }; diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index 2c00751c2..f455579e4 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -126,7 +126,7 @@ fn main() -> anyhow::Result<()> { }; let client = esplora_cmd.esplora_args().client(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 @@ -137,7 +137,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, @@ -239,7 +239,6 @@ fn main() -> anyhow::Result<()> { let init_outpoints = graph.index.outpoints(); request = request.outpoints( graph - .graph() .filter_chain_unspents( &*chain, local_tip.block_id(), @@ -254,7 +253,6 @@ fn main() -> anyhow::Result<()> { // `EsploraExt::update_tx_graph_without_keychain`. request = request.txids( graph - .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), @@ -280,8 +278,7 @@ fn main() -> anyhow::Result<()> { let mut db = db.lock().unwrap(); db.append(&ChangeSet { local_chain: local_chain_changeset, - tx_graph: indexed_tx_graph_changeset.tx_graph, - indexer: indexed_tx_graph_changeset.indexer, + tx_graph: tx_graph_changeset, ..Default::default() })?; Ok(()) From 1c026d67e9e9e05d4b707707991f528976ce3b40 Mon Sep 17 00:00:00 2001 From: valued mammal Date: Wed, 5 Feb 2025 10:05:29 -0500 Subject: [PATCH 2/2] refactor(tx_graph): make `tx_graph::ChangeSet` nested again The serialization format of the nested structure is exactly the same as the as the old `indexed_tx_graph::ChangeSet`. The old `tx_graph::ChangeSet` is replaced with `tx_graph::TxChangeSet`. --- crates/bitcoind_rpc/tests/test_emitter.rs | 6 +- crates/chain/src/rusqlite_impl.rs | 12 +- crates/chain/src/tx_graph.rs | 195 +++++++++++++------- crates/chain/tests/test_indexed_tx_graph.rs | 15 +- crates/chain/tests/test_tx_graph.rs | 31 ++-- example-crates/example_cli/src/lib.rs | 13 +- example-crates/example_electrum/src/main.rs | 5 +- example-crates/example_esplora/src/main.rs | 11 +- 8 files changed, 185 insertions(+), 103 deletions(-) diff --git a/crates/bitcoind_rpc/tests/test_emitter.rs b/crates/bitcoind_rpc/tests/test_emitter.rs index 3185e6b34..ad842d7e3 100644 --- a/crates/bitcoind_rpc/tests/test_emitter.rs +++ b/crates/bitcoind_rpc/tests/test_emitter.rs @@ -189,7 +189,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> { assert!(emitter.next_block()?.is_none()); let mempool_txs = emitter.mempool()?; - let tx_graph_changeset = tx_graph.batch_insert_unconfirmed(mempool_txs); + let tx_graph_changeset = tx_graph.batch_insert_unconfirmed(mempool_txs).tx_data; assert_eq!( tx_graph_changeset .txs @@ -221,7 +221,9 @@ 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 tx_graph_changeset = tx_graph.apply_block_relevant(&emission.block, height); + let tx_graph_changeset = tx_graph + .apply_block_relevant(&emission.block, height) + .tx_data; assert!(tx_graph_changeset.txs.is_empty()); assert!(tx_graph_changeset.txouts.is_empty()); assert_eq!(tx_graph_changeset.anchors, exp_anchors); diff --git a/crates/chain/src/rusqlite_impl.rs b/crates/chain/src/rusqlite_impl.rs index 7b39f53c0..278269f88 100644 --- a/crates/chain/src/rusqlite_impl.rs +++ b/crates/chain/src/rusqlite_impl.rs @@ -199,8 +199,8 @@ fn to_sql_error(err: E) -> rusqlit rusqlite::Error::ToSqlConversionFailure(Box::new(err)) } -impl tx_graph::ChangeSet { - /// Schema name for [`tx_graph::ChangeSet`]. +impl tx_graph::TxChangeSet { + /// Schema name for [`tx_graph::TxChangeSet`]. pub const SCHEMA_NAME: &'static str = "bdk_txgraph"; /// Name of table that stores full transactions and `last_seen` timestamps. pub const TXS_TABLE_NAME: &'static str = "bdk_txs"; @@ -209,7 +209,7 @@ impl tx_graph::ChangeSet { /// Name of table that stores [`Anchor`]s. pub const ANCHORS_TABLE_NAME: &'static str = "bdk_anchors"; - /// Get v0 of sqlite [tx_graph::ChangeSet] schema + /// Get v0 of sqlite [`tx_graph::TxChangeSet`] schema pub fn schema_v0() -> String { // full transactions let create_txs_table = format!( @@ -247,7 +247,7 @@ impl tx_graph::ChangeSet { format!("{create_txs_table}; {create_txouts_table}; {create_anchors_table}") } - /// Get v1 of sqlite [tx_graph::ChangeSet] schema + /// Get v1 of sqlite [`tx_graph::TxChangeSet`] schema pub fn schema_v1() -> String { let add_confirmation_time_column = format!( "ALTER TABLE {} ADD COLUMN confirmation_time INTEGER DEFAULT -1 NOT NULL", @@ -567,7 +567,7 @@ mod test { #[test] fn can_persist_anchors_and_txs_independently() -> anyhow::Result<()> { - type ChangeSet = tx_graph::ChangeSet; + type ChangeSet = tx_graph::TxChangeSet; let mut conn = rusqlite::Connection::open_in_memory()?; // init tables @@ -629,7 +629,7 @@ mod test { #[test] fn v0_to_v1_schema_migration_is_backward_compatible() -> anyhow::Result<()> { - type ChangeSet = tx_graph::ChangeSet; + type ChangeSet = tx_graph::TxChangeSet; let mut conn = rusqlite::Connection::open_in_memory()?; // Create initial database with v0 sqlite schema diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 9a1867926..fd94bf5a0 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -631,7 +631,7 @@ impl TxGraph { ); } None => { - changeset.txouts.insert(outpoint, txout); + changeset.tx_data.txouts.insert(outpoint, txout); } } } @@ -669,7 +669,7 @@ impl TxGraph { .insert(txid); } *partial_tx = TxNodeInternal::Whole(tx.clone()); - changeset.txs.insert(tx); + changeset.tx_data.txs.insert(tx); } } @@ -734,7 +734,10 @@ impl TxGraph { let mut changeset = ChangeSet::default(); for (tx, _) in &txs { - changeset.merge(self.index.index_tx(tx).into()); + changeset.merge(ChangeSet { + indexer: self.index.index_tx(tx), + ..Default::default() + }); } for (tx, anchors) in txs { @@ -830,7 +833,7 @@ impl TxGraph { } self.txs_by_highest_conf_heights.insert((new_top_h, txid)); } - changeset.anchors.insert((anchor, txid)); + changeset.tx_data.anchors.insert((anchor, txid)); } changeset } @@ -862,7 +865,7 @@ impl TxGraph { self.txs_by_last_seen.remove(&(old_last_seen, txid)); } self.txs_by_last_seen.insert((seen_at, txid)); - changeset.last_seen.insert(txid, seen_at); + changeset.tx_data.last_seen.insert(txid, seen_at); } changeset } @@ -919,12 +922,16 @@ impl TxGraph { let mut changeset = ChangeSet::::default(); for (&txid, node) in &self.txs { match node { - TxNodeInternal::Whole(tx) => { - changeset.merge(self.index.index_tx(tx.as_ref()).into()) - } + TxNodeInternal::Whole(tx) => changeset.merge(ChangeSet { + indexer: self.index.index_tx(tx.as_ref()), + ..Default::default() + }), TxNodeInternal::Partial(txouts) => { for (op, txout) in TxNodeInternal::iter_txouts(txid, txouts) { - changeset.merge(self.index.index_txout(op, txout).into()); + changeset.merge(ChangeSet { + indexer: self.index.index_txout(op, txout), + ..Default::default() + }); } } }; @@ -934,7 +941,7 @@ impl TxGraph { /// Determines the [`ChangeSet`] between `self` and an empty [`TxGraph`]. pub fn initial_changeset(&self) -> ChangeSet { - ChangeSet { + let tx_data = TxChangeSet { txs: self.full_txs().map(|tx_node| tx_node.tx).collect(), txouts: self .floating_txouts() @@ -942,6 +949,9 @@ impl TxGraph { .collect(), anchors: self.rev_anchors(), last_seen: self.last_seen.clone().into_iter().collect(), + }; + ChangeSet { + tx_data, indexer: self.index.initial_changeset(), } } @@ -949,10 +959,10 @@ impl TxGraph { /// Indexes the txs and txouts of `changeset` and merges the resulting changes. fn index_changeset(&mut self, changeset: &mut ChangeSet) { let indexer = &mut changeset.indexer; - for tx in &changeset.txs { + for tx in &changeset.tx_data.txs { indexer.merge(self.index.index_tx(tx)); } - for (&outpoint, txout) in &changeset.txouts { + for (&outpoint, txout) in &changeset.tx_data.txouts { indexer.merge(self.index.index_txout(outpoint, txout)); } } @@ -963,23 +973,17 @@ impl TxGraph { // transactions so we do that first. self.index.apply_changeset(changeset.indexer); - for tx in &changeset.txs { - self.index.index_tx(tx); - } - for (&outpoint, txout) in &changeset.txouts { - self.index.index_txout(outpoint, txout); - } - - for tx in changeset.txs { + let tx_changeset = changeset.tx_data; + for tx in tx_changeset.txs { let _ = self.insert_tx(tx); } - for (outpoint, txout) in changeset.txouts { + for (outpoint, txout) in tx_changeset.txouts { let _ = self.insert_txout(outpoint, txout); } - for (anchor, txid) in changeset.anchors { + for (anchor, txid) in tx_changeset.anchors { let _ = self.insert_anchor(txid, anchor); } - for (txid, seen_at) in changeset.last_seen { + for (txid, seen_at) in tx_changeset.last_seen { let _ = self.insert_seen_at(txid, seen_at); } } @@ -1320,7 +1324,10 @@ where }; let mut changeset = ChangeSet::default(); for (tx_pos, tx) in block.txdata.iter().enumerate() { - changeset.merge(self.index.index_tx(tx).into()); + changeset.merge(ChangeSet { + indexer: self.index.index_tx(tx), + ..Default::default() + }); if self.index.is_tx_relevant(tx) { let anchor = TxPosInBlock { block, @@ -1348,7 +1355,10 @@ where }; let mut changeset = ChangeSet::default(); for (tx_pos, tx) in block.txdata.iter().enumerate() { - changeset.merge(self.index.index_tx(tx).into()); + changeset.merge(ChangeSet { + indexer: self.index.index_tx(tx), + ..Default::default() + }); let anchor = TxPosInBlock { block: &block, block_id, @@ -1361,23 +1371,36 @@ where } } -/// The [`ChangeSet`] represents changes to a [`TxGraph`]. +impl AsRef> for TxGraph { + fn as_ref(&self) -> &TxGraph { + self + } +} + +/// The [`TxChangeSet`] represents changes to transaction data in [`TxGraph`]. +/// +/// Transaction data includes full transactions, floating txouts (for fee/feerate calculations), +/// [`Anchor`]s, and timestamps of when the transaction was seen in the mempool (currently only +/// `last-seen`). /// -/// Since [`TxGraph`] is monotone, the "changeset" can only contain transactions to be added and +/// Transaction data is monotone. The `TxChangeSet` can only contain transactions to be added and /// not removed. /// -/// Refer to [module-level documentation](self) for more. +/// Refer to [`ChangeSet`] which also includes changes to the internal [`Indexer`] of [`TxGraph`]. +/// Refer to the [module-level documentation](crate::tx_graph) for more. +/// +/// [`TxGraph`]: crate::TxGraph +/// [`Indexer`]: crate::Indexer #[derive(Debug, Clone, PartialEq)] #[cfg_attr( feature = "serde", derive(serde::Deserialize, serde::Serialize), serde(bound( - deserialize = "A: Ord + serde::Deserialize<'de>, X: serde::Deserialize<'de>", - serialize = "A: Ord + serde::Serialize, X: serde::Serialize", + deserialize = "A: Ord + serde::Deserialize<'de>", + serialize = "A: Ord + serde::Serialize", )) )] -#[must_use] -pub struct ChangeSet { +pub struct TxChangeSet { /// Added transactions. pub txs: BTreeSet>, /// Added txouts. @@ -1386,35 +1409,20 @@ 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 TxChangeSet { 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(indexer: X) -> Self { - Self { - indexer, - txs: Default::default(), - txouts: Default::default(), - anchors: Default::default(), - last_seen: Default::default(), - } - } -} - -impl ChangeSet { +impl TxChangeSet { /// Iterates over all outpoints contained within [`ChangeSet`]. pub fn txouts(&self) -> impl Iterator { self.txs @@ -1448,7 +1456,27 @@ impl ChangeSet { } } -impl Merge for ChangeSet { +impl TxChangeSet { + /// Transform the [`TxChangeSet`] 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) -> TxChangeSet + where + F: FnMut(A) -> A2, + { + TxChangeSet { + txs: self.txs, + txouts: self.txouts, + anchors: BTreeSet::<(A2, Txid)>::from_iter( + self.anchors.into_iter().map(|(a, txid)| (f(a), txid)), + ), + last_seen: self.last_seen, + } + } +} + +impl Merge for TxChangeSet { 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 @@ -1464,7 +1492,6 @@ 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 { @@ -1472,37 +1499,77 @@ impl Merge for ChangeSet { && self.txouts.is_empty() && self.anchors.is_empty() && self.last_seen.is_empty() - && self.indexer.is_empty() } } -impl ChangeSet { +/// The [`ChangeSet`] represents changes to a [`TxGraph`] and it's internal [`Indexer`]. +/// +/// Refer to the [module-level documentation](crate::tx_graph) for more. +/// +/// [`TxGraph`]: crate::TxGraph +/// [`Indexer`]: crate::Indexer +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(bound( + deserialize = "A: Ord + serde::Deserialize<'de>, XC: serde::Deserialize<'de>", + serialize = "A: Ord + serde::Serialize, XC: serde::Serialize", + )) +)] +#[must_use] +pub struct ChangeSet { + /// Changes to transaction data. + #[cfg_attr(feature = "serde", serde(alias = "tx_graph"))] + pub tx_data: TxChangeSet, + /// Changes to the indexer. + pub indexer: XC, +} + +impl Default for ChangeSet { + fn default() -> Self { + Self { + tx_data: Default::default(), + indexer: Default::default(), + } + } +} + +impl From> for ChangeSet { + fn from(tx_graph: TxChangeSet) -> Self { + Self { + tx_data: tx_graph, + ..Default::default() + } + } +} + +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, f: F) -> ChangeSet where F: FnMut(A) -> A2, { ChangeSet { - txs: self.txs, - txouts: self.txouts, - anchors: BTreeSet::<(A2, Txid)>::from_iter( - self.anchors.into_iter().map(|(a, txid)| (f(a), txid)), - ), - last_seen: self.last_seen, + tx_data: self.tx_data.map_anchors(f), indexer: self.indexer, } } } -impl AsRef> for TxGraph { - fn as_ref(&self) -> &TxGraph { - self +impl Merge for ChangeSet { + fn merge(&mut self, other: Self) { + self.tx_data.merge(other.tx_data); + self.indexer.merge(other.indexer); } -} + fn is_empty(&self) -> bool { + self.tx_data.is_empty() && self.indexer.is_empty() + } +} /// An iterator that traverses ancestors of a given root transaction. /// /// The iterator excludes partial transactions. diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index bce83b9d8..db4861c40 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -6,8 +6,10 @@ mod common; use std::{collections::BTreeSet, sync::Arc}; use bdk_chain::{ - indexer::keychain_txout::KeychainTxOutIndex, local_chain::LocalChain, tx_graph, Balance, - ChainPosition, ConfirmationBlockTime, DescriptorExt, TxGraph, + indexer::keychain_txout::KeychainTxOutIndex, + local_chain::LocalChain, + tx_graph::{self, TxChangeSet}, + Balance, ChainPosition, ConfirmationBlockTime, DescriptorExt, TxGraph, }; use bdk_testenv::{ block_id, hash, @@ -71,11 +73,13 @@ fn insert_relevant_txs() { let txs = [tx_c, tx_b, tx_a]; let changeset = tx_graph::ChangeSet { - txs: txs.iter().cloned().map(Arc::new).collect(), + tx_data: TxChangeSet { + txs: txs.iter().cloned().map(Arc::new).collect(), + ..Default::default() + }, indexer: keychain_txout::ChangeSet { last_revealed: [(descriptor.descriptor_id(), 9_u32)].into(), }, - ..Default::default() }; assert_eq!( @@ -85,11 +89,10 @@ fn insert_relevant_txs() { // The initial changeset will also contain info about the keychain we added let initial_changeset = tx_graph::ChangeSet { - txs: changeset.txs, + tx_data: changeset.tx_data, indexer: keychain_txout::ChangeSet { last_revealed: changeset.indexer.last_revealed, }, - ..Default::default() }; assert_eq!(graph.initial_changeset(), initial_changeset); diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index c680d1687..0887d68a2 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -5,8 +5,7 @@ mod common; use bdk_chain::{collections::*, BlockId, ConfirmationBlockTime}; use bdk_chain::{ local_chain::LocalChain, - tx_graph::{self, CalculateFeeError}, - tx_graph::{ChangeSet, TxGraph}, + tx_graph::{self, CalculateFeeError, TxChangeSet, TxGraph}, Anchor, ChainOracle, ChainPosition, Merge, }; use bdk_testenv::{block_id, hash, utils::new_tx}; @@ -77,8 +76,8 @@ fn insert_txouts() { let mut graph = TxGraph::::default(); for (outpoint, txout) in &original_ops { assert_eq!( - graph.insert_txout(*outpoint, txout.clone()), - ChangeSet { + graph.insert_txout(*outpoint, txout.clone()).tx_data, + TxChangeSet { txouts: [(*outpoint, txout.clone())].into(), ..Default::default() } @@ -110,13 +109,12 @@ fn insert_txouts() { let changeset = graph.apply_update(update); assert_eq!( - changeset, - ChangeSet { + changeset.tx_data, + TxChangeSet { txs: [Arc::new(update_tx.clone())].into(), txouts: update_ops.clone().into(), anchors: [(conf_anchor, update_tx.compute_txid()),].into(), - last_seen: [(hash!("tx2"), 1000000)].into(), - ..Default::default() + last_seen: [(hash!("tx2"), 1000000)].into() } ); @@ -164,13 +162,12 @@ fn insert_txouts() { // Check that the initial_changeset is correct assert_eq!( - graph.initial_changeset(), - ChangeSet { + graph.initial_changeset().tx_data, + TxChangeSet { txs: [Arc::new(update_tx.clone())].into(), txouts: update_ops.into_iter().chain(original_ops).collect(), anchors: [(conf_anchor, update_tx.compute_txid()),].into(), - last_seen: [(hash!("tx2"), 1000000)].into(), - ..Default::default() + last_seen: [(hash!("tx2"), 1000000)].into() } ); } @@ -277,7 +274,7 @@ fn insert_tx_displaces_txouts() { let changeset = tx_graph.insert_txout(outpoint, txout.clone()); assert!(!changeset.is_empty()); - let changeset = tx_graph.insert_tx(tx.clone()); + let changeset = tx_graph.insert_tx(tx.clone()).tx_data; assert_eq!(changeset.txs.len(), 1); assert!(changeset.txouts.is_empty()); assert!(tx_graph.get_tx(txid).is_some()); @@ -1027,12 +1024,12 @@ fn test_changeset_last_seen_merge() { ]; for (original_ls, update_ls) in test_cases { - let mut original = ChangeSet::<()> { + let mut original = TxChangeSet::<()> { last_seen: original_ls.map(|ls| (txid, ls)).into_iter().collect(), ..Default::default() }; assert!(!original.is_empty() || original_ls.is_none()); - let update = ChangeSet::<()> { + let update = TxChangeSet::<()> { last_seen: update_ls.map(|ls| (txid, ls)).into_iter().collect(), ..Default::default() }; @@ -1102,7 +1099,7 @@ fn insert_anchor_without_tx() { // insert anchor with no corresponding tx let mut changeset = graph.insert_anchor(txid, anchor); - assert!(changeset.anchors.contains(&(anchor, txid))); + assert!(changeset.tx_data.anchors.contains(&(anchor, txid))); // recover from changeset let mut recovered = TxGraph::default(); recovered.apply_changeset(changeset.clone()); @@ -1111,7 +1108,7 @@ fn insert_anchor_without_tx() { // now insert tx let tx = Arc::new(tx); let graph_changeset = graph.insert_tx(tx.clone()); - assert!(graph_changeset.txs.contains(&tx)); + assert!(graph_changeset.tx_data.txs.contains(&tx)); changeset.merge(graph_changeset); // recover from changeset again let mut recovered = TxGraph::default(); diff --git a/example-crates/example_cli/src/lib.rs b/example-crates/example_cli/src/lib.rs index 4f1706021..dab20ceaf 100644 --- a/example-crates/example_cli/src/lib.rs +++ b/example-crates/example_cli/src/lib.rs @@ -1,3 +1,4 @@ +use bdk_chain::tx_graph; use serde_json::json; use std::cmp; use std::collections::HashMap; @@ -23,7 +24,7 @@ use bdk_chain::ConfirmationBlockTime; use bdk_chain::{ indexer::keychain_txout::{self, KeychainTxOutIndex}, local_chain::{self, LocalChain}, - tx_graph, ChainOracle, DescriptorExt, FullTxOut, Merge, TxGraph, + ChainOracle, DescriptorExt, FullTxOut, Merge, TxGraph, }; use bdk_coin_select::{ metrics::LowestFee, Candidate, ChangePolicy, CoinSelector, DrainWeights, FeeRate, Target, @@ -463,7 +464,10 @@ pub fn handle_commands( spk_chooser(index, Keychain::External).expect("Must exist"); let db = &mut *db.lock().unwrap(); db.append(&ChangeSet { - tx_graph: index_changeset.into(), + tx_graph: tx_graph::ChangeSet { + indexer: index_changeset, + ..Default::default() + }, ..Default::default() })?; let addr = Address::from_script(spk.as_script(), network)?; @@ -625,7 +629,10 @@ pub fn handle_commands( { let db = &mut *db.lock().unwrap(); db.append(&ChangeSet { - tx_graph: indexer.into(), + tx_graph: tx_graph::ChangeSet { + indexer, + ..Default::default() + }, ..Default::default() })?; } diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index 90a6bb685..f6cbf0401 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -253,7 +253,10 @@ fn main() -> anyhow::Result<()> { let mut tx_graph_changeset = tx_graph::ChangeSet::default(); if let Some(keychain_update) = keychain_update { - tx_graph_changeset.merge(graph.index.reveal_to_target_multi(&keychain_update).into()); + tx_graph_changeset.merge(tx_graph::ChangeSet { + indexer: graph.index.reveal_to_target_multi(&keychain_update), + ..Default::default() + }); } tx_graph_changeset.merge(graph.apply_update(tx_update)); diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index f455579e4..b7217b77d 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -8,7 +8,7 @@ use bdk_chain::{ bitcoin::Network, keychain_txout::FullScanRequestBuilderExt, spk_client::{FullScanRequest, SyncRequest}, - Merge, + tx_graph, Merge, }; use bdk_esplora::{esplora_client, EsploraExt}; use example_cli::{ @@ -183,9 +183,12 @@ fn main() -> anyhow::Result<()> { let index_changeset = graph .index .reveal_to_target_multi(&update.last_active_indices); - let mut indexed_tx_graph_changeset = graph.apply_update(update.tx_update); - indexed_tx_graph_changeset.merge(index_changeset.into()); - indexed_tx_graph_changeset + let mut tx_graph_changeset = graph.apply_update(update.tx_update); + tx_graph_changeset.merge(tx_graph::ChangeSet { + indexer: index_changeset, + ..Default::default() + }); + tx_graph_changeset }, ) }