]> Untitled Git - bdk/commitdiff
feat(chain)!: Clean up ergonomics of `IndexedTxGraph`
author志宇 <hello@evanlinjin.me>
Fri, 23 May 2025 08:27:55 +0000 (18:27 +1000)
committer志宇 <hello@evanlinjin.me>
Fri, 23 May 2025 10:34:07 +0000 (20:34 +1000)
* `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
crates/chain/src/indexed_tx_graph.rs
crates/chain/tests/test_indexed_tx_graph.rs
examples/example_cli/src/lib.rs

index 21300e700e13b9de6768a1317581b1548e819cad..b4ca0bffd1d9deb1f328aea3bba333c3174908f2 100644 (file)
@@ -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::<ConfirmationBlockTime, KeychainTxOutIndex<&str>>::new({
         let mut index = KeychainTxOutIndex::default();
         index.insert_descriptor("external", descriptor.clone())?;
index 16339624ce4b6bcb0d88992d406984d2c30b7efa..9ba3395eb6b1cd4bcc7beed28518b21410bec0de 100644 (file)
@@ -15,12 +15,14 @@ use crate::{
     Anchor, BlockId, ChainOracle, Indexer, Merge, TxPosInBlock,
 };
 
-/// The [`IndexedTxGraph`] combines a [`TxGraph`] and an [`Indexer`] implementation.
+/// A [`TxGraph<A>`] 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<A, I> {
-    /// Transaction index.
+    /// The indexer used for filtering transactions and floating txouts that we are interested in.
     pub index: I,
     graph: TxGraph<A>,
 }
@@ -35,14 +37,6 @@ impl<A, I: Default> Default for IndexedTxGraph<A, I> {
 }
 
 impl<A, I> IndexedTxGraph<A, I> {
-    /// 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<A> {
         &self.graph
@@ -79,6 +73,87 @@ impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I>
 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::<BlockId, _>::default();
+    /// # let persisted_desc = Some(Descriptor::<DescriptorPublicKey>::from_str("")?);
+    /// # let persisted_change_desc = Some(Descriptor::<DescriptorPublicKey>::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<F, E>(
+        changeset: ChangeSet<A, I::ChangeSet>,
+        indexer_from_changeset: F,
+    ) -> Result<(Self, ChangeSet<A, I::ChangeSet>), E>
+    where
+        F: FnOnce(I::ChangeSet) -> Result<I, E>,
+    {
+        let graph = TxGraph::<A>::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<A, I::ChangeSet> {
+        let mut changeset = ChangeSet::<A, I::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<A>,
@@ -443,6 +518,12 @@ impl<A, IA: Default> From<tx_graph::ChangeSet<A>> for ChangeSet<A, IA> {
     }
 }
 
+impl<A, IA> From<(tx_graph::ChangeSet<A>, IA)> for ChangeSet<A, IA> {
+    fn from((tx_graph, indexer): (tx_graph::ChangeSet<A>, IA)) -> Self {
+        Self { tx_graph, indexer }
+    }
+}
+
 #[cfg(feature = "miniscript")]
 impl<A> From<crate::keychain_txout::ChangeSet> for ChangeSet<A, crate::keychain_txout::ChangeSet> {
     fn from(indexer: crate::keychain_txout::ChangeSet) -> Self {
index 5f23af5598e35299cfd0f475cb693b196960a217..ee95dde7906eab82d1d3daf0c98aedbfa0ccd59a 100644 (file)
@@ -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::<ConfirmationBlockTime, KeychainTxOutIndex<()>>::new(
-        KeychainTxOutIndex::new(lookahead, true),
-    );
-    let is_inserted = graph
-        .index
-        .insert_descriptor((), descriptor.clone())
-        .unwrap();
-    assert!(is_inserted);
+    let mut graph = IndexedTxGraph::<ConfirmationBlockTime, KeychainTxOutIndex<()>>::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::<ConfirmationBlockTime, KeychainTxOutIndex<String>>::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::<ConfirmationBlockTime, KeychainTxOutIndex<String>>::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
 
index d432d12b66aa5f9a3a8b20fc8afb5f31d05eb3e9..d70ee1250e5083ec50ebb2cdf03ad10ce672263b 100644 (file)
@@ -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<CS: clap::Subcommand, S: clap::Args>(
         Commands::Generate { network } => generate_bip86_helper(network).map(|_| None),
         // try load
         _ => {
-            let (db, changeset) =
+            let (mut db, changeset) =
                 Store::<ChangeSet>::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<CS: clap::Subcommand, S: clap::Args>(
                 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 {