From 3126cd214536db6c9636f4e7b6b92019fcee535e Mon Sep 17 00:00:00 2001 From: =?utf8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 23 May 2025 18:27:55 +1000 Subject: [PATCH] feat(chain)!: Clean up ergonomics of `IndexedTxGraph` * `new` is now intended to construct a fresh indexed-tx-graph * `from_changeset` is added for constructing indexed-tx-graph from a previously persisted changeset * added `reindex` for calling after indexer mutations that require it * reintroduce `Default` impl --- crates/bitcoind_rpc/examples/filter_iter.rs | 1 + crates/chain/src/indexed_tx_graph.rs | 103 +++++++++++++++++--- crates/chain/tests/test_indexed_tx_graph.rs | 36 +++---- examples/example_cli/src/lib.rs | 41 ++++---- 4 files changed, 131 insertions(+), 50 deletions(-) diff --git a/crates/bitcoind_rpc/examples/filter_iter.rs b/crates/bitcoind_rpc/examples/filter_iter.rs index 21300e70..b4ca0bff 100644 --- a/crates/bitcoind_rpc/examples/filter_iter.rs +++ b/crates/bitcoind_rpc/examples/filter_iter.rs @@ -31,6 +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 index = KeychainTxOutIndex::default(); index.insert_descriptor("external", descriptor.clone())?; diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 16339624..9ba3395e 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -15,12 +15,14 @@ use crate::{ Anchor, BlockId, ChainOracle, Indexer, Merge, TxPosInBlock, }; -/// The [`IndexedTxGraph`] combines a [`TxGraph`] and an [`Indexer`] implementation. +/// A [`TxGraph`] paired with an indexer `I`, enforcing that every insertion into the graph is +/// simultaneously fed through the indexer. /// -/// It ensures that [`TxGraph`] and [`Indexer`] are updated atomically. +/// This guarantees that `tx_graph` and `index` remain in sync: any transaction or floating txout +/// you add to `tx_graph` has already been processed by `index`. #[derive(Debug, Clone)] pub struct IndexedTxGraph { - /// Transaction index. + /// The indexer used for filtering transactions and floating txouts that we are interested in. pub index: I, graph: TxGraph, } @@ -35,14 +37,6 @@ impl Default for IndexedTxGraph { } 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 @@ -79,6 +73,87 @@ impl IndexedTxGraph where I::ChangeSet: Default + Merge, { + /// Create a new, empty [`IndexedTxGraph`]. + /// + /// The underlying `TxGraph` is initialized with `TxGraph::default()`, and the provided + /// `index`er is used as‐is (since there are no existing transactions to process). + pub fn new(index: I) -> Self { + Self { + index, + graph: TxGraph::default(), + } + } + + /// Reconstruct an [`IndexedTxGraph`] from persisted graph + indexer state. + /// + /// 1. Rebuilds the `TxGraph` from `changeset.tx_graph`. + /// 2. Calls your `indexer_from_changeset` closure on `changeset.indexer` to restore any state + /// your indexer needs beyond its raw changeset. + /// 3. Runs a full `.reindex()`, returning its `ChangeSet` to describe any additional updates + /// applied. + /// + /// # Errors + /// + /// Returns `Err(E)` if `indexer_from_changeset` fails. + /// + /// # Examples + /// + /// ```rust,no_run + /// use bdk_chain::IndexedTxGraph; + /// # use bdk_chain::indexed_tx_graph::ChangeSet; + /// # use bdk_chain::indexer::keychain_txout::{KeychainTxOutIndex, DEFAULT_LOOKAHEAD}; + /// # use bdk_core::BlockId; + /// # use bdk_testenv::anyhow; + /// # use miniscript::{Descriptor, DescriptorPublicKey}; + /// # use std::str::FromStr; + /// # let persisted_changeset = ChangeSet::::default(); + /// # let persisted_desc = Some(Descriptor::::from_str("")?); + /// # let persisted_change_desc = Some(Descriptor::::from_str("")?); + /// + /// let (graph, reindex_cs) = + /// IndexedTxGraph::from_changeset(persisted_changeset, move |idx_cs| -> anyhow::Result<_> { + /// // e.g. KeychainTxOutIndex needs descriptors that weren’t in its CS + /// let mut idx = KeychainTxOutIndex::from_changeset(DEFAULT_LOOKAHEAD, true, idx_cs); + /// if let Some(desc) = persisted_desc { + /// idx.insert_descriptor("external", desc)?; + /// } + /// if let Some(desc) = persisted_change_desc { + /// idx.insert_descriptor("internal", desc)?; + /// } + /// Ok(idx) + /// })?; + /// # Ok::<(), anyhow::Error>(()) + /// ``` + pub fn from_changeset( + changeset: ChangeSet, + indexer_from_changeset: F, + ) -> Result<(Self, ChangeSet), E> + where + F: FnOnce(I::ChangeSet) -> Result, + { + let graph = TxGraph::::from_changeset(changeset.tx_graph); + let index = indexer_from_changeset(changeset.indexer)?; + let mut out = Self { graph, index }; + let out_changeset = out.reindex(); + Ok((out, out_changeset)) + } + + /// Synchronizes the indexer to reflect every entry in the transaction graph. + /// + /// Iterates over **all** full transactions and floating outputs in `self.graph`, passing each + /// into `self.index`. Any indexer-side changes produced (via `index_tx` or `index_txout`) are + /// merged into a fresh `ChangeSet`, which is then returned. + pub fn reindex(&mut self) -> ChangeSet { + let mut changeset = ChangeSet::::default(); + for tx in self.graph.full_txs() { + changeset.indexer.merge(self.index.index_tx(&tx)); + } + for (op, txout) in self.graph.floating_txouts() { + changeset.indexer.merge(self.index.index_txout(op, txout)); + } + changeset + } + fn index_tx_graph_changeset( &mut self, tx_graph_changeset: &tx_graph::ChangeSet, @@ -443,6 +518,12 @@ impl From> for ChangeSet { } } +impl From<(tx_graph::ChangeSet, IA)> for ChangeSet { + fn from((tx_graph, indexer): (tx_graph::ChangeSet, IA)) -> Self { + Self { tx_graph, indexer } + } +} + #[cfg(feature = "miniscript")] impl From for ChangeSet { fn from(indexer: crate::keychain_txout::ChangeSet) -> Self { diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index 5f23af55..ee95dde7 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -35,14 +35,12 @@ fn insert_relevant_txs() { let spk_1 = descriptor.at_derivation_index(9).unwrap().script_pubkey(); let lookahead = 10; - let mut graph = IndexedTxGraph::>::new( - KeychainTxOutIndex::new(lookahead, true), - ); - let is_inserted = graph - .index - .insert_descriptor((), descriptor.clone()) - .unwrap(); - assert!(is_inserted); + let mut graph = IndexedTxGraph::>::new({ + let mut indexer = KeychainTxOutIndex::new(lookahead, true); + let is_inserted = indexer.insert_descriptor((), descriptor.clone()).unwrap(); + assert!(is_inserted); + indexer + }); let tx_a = Transaction { output: vec![ @@ -160,18 +158,16 @@ fn test_list_owned_txouts() { let (desc_2, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTORS[3]).unwrap(); - let mut graph = IndexedTxGraph::>::new( - KeychainTxOutIndex::new(10, true), - ); - - assert!(graph - .index - .insert_descriptor("keychain_1".into(), desc_1) - .unwrap()); - assert!(graph - .index - .insert_descriptor("keychain_2".into(), desc_2) - .unwrap()); + let mut graph = IndexedTxGraph::>::new({ + let mut indexer = KeychainTxOutIndex::new(10, true); + assert!(indexer + .insert_descriptor("keychain_1".into(), desc_1) + .unwrap()); + assert!(indexer + .insert_descriptor("keychain_2".into(), desc_2) + .unwrap()); + indexer + }); // Get trusted and untrusted addresses diff --git a/examples/example_cli/src/lib.rs b/examples/example_cli/src/lib.rs index d432d12b..d70ee125 100644 --- a/examples/example_cli/src/lib.rs +++ b/examples/example_cli/src/lib.rs @@ -1,3 +1,4 @@ +use bdk_chain::keychain_txout::DEFAULT_LOOKAHEAD; use serde_json::json; use std::cmp; use std::collections::HashMap; @@ -22,7 +23,6 @@ use bdk_chain::miniscript::{ use bdk_chain::CanonicalizationParams; 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, @@ -818,11 +818,10 @@ pub fn init_or_load( Commands::Generate { network } => generate_bip86_helper(network).map(|_| None), // try load _ => { - let (db, changeset) = + let (mut db, changeset) = Store::::load(db_magic, db_path).context("could not open file store")?; let changeset = changeset.expect("should not be empty"); - let network = changeset.network.expect("changeset network"); let chain = Mutex::new({ @@ -832,23 +831,27 @@ pub fn init_or_load( chain }); - let graph = Mutex::new({ - // insert descriptors and apply loaded changeset - let mut index = KeychainTxOutIndex::default(); - if let Some(desc) = changeset.descriptor { - index.insert_descriptor(Keychain::External, desc)?; - } - if let Some(change_desc) = changeset.change_descriptor { - 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 - }); + let (graph, changeset) = IndexedTxGraph::from_changeset( + (changeset.tx_graph, changeset.indexer).into(), + |c| -> anyhow::Result<_> { + let mut indexer = + KeychainTxOutIndex::from_changeset(DEFAULT_LOOKAHEAD, true, c); + if let Some(desc) = changeset.descriptor { + indexer.insert_descriptor(Keychain::External, desc)?; + } + if let Some(change_desc) = changeset.change_descriptor { + indexer.insert_descriptor(Keychain::Internal, change_desc)?; + } + Ok(indexer) + }, + )?; + db.append(&ChangeSet { + indexer: changeset.indexer, + tx_graph: changeset.tx_graph, + ..Default::default() + })?; + let graph = Mutex::new(graph); let db = Mutex::new(db); Ok(Some(Init { -- 2.49.0