Skip to content

Commit 9d97b29

Browse files
committed
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
1 parent 3edfa4c commit 9d97b29

File tree

6 files changed

+143
-69
lines changed

6 files changed

+143
-69
lines changed

crates/bitcoind_rpc/examples/filter_iter.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use anyhow::Context;
55
use bdk_bitcoind_rpc::bip158::{Event, EventInner, FilterIter};
66
use bdk_chain::bitcoin::{constants::genesis_block, secp256k1::Secp256k1, Network};
77
use bdk_chain::indexer::keychain_txout::KeychainTxOutIndex;
8+
use bdk_chain::keychain_txout::DEFAULT_LOOKAHEAD;
89
use bdk_chain::local_chain::LocalChain;
910
use bdk_chain::miniscript::Descriptor;
1011
use bdk_chain::{BlockId, ConfirmationBlockTime, IndexedTxGraph, SpkIterator};
@@ -31,11 +32,13 @@ fn main() -> anyhow::Result<()> {
3132
let (descriptor, _) = Descriptor::parse_descriptor(&secp, EXTERNAL)?;
3233
let (change_descriptor, _) = Descriptor::parse_descriptor(&secp, INTERNAL)?;
3334
let (mut chain, _) = LocalChain::from_genesis_hash(genesis_block(NETWORK).block_hash());
35+
3436
let mut graph = IndexedTxGraph::<ConfirmationBlockTime, KeychainTxOutIndex<&str>>::new({
35-
let mut index = KeychainTxOutIndex::default();
36-
index.insert_descriptor("external", descriptor.clone())?;
37-
index.insert_descriptor("internal", change_descriptor.clone())?;
38-
index
37+
// Disable indexer's spk cache as we are not persisting.
38+
let mut indexer = KeychainTxOutIndex::new(DEFAULT_LOOKAHEAD, false);
39+
indexer.insert_descriptor("external", descriptor.clone())?;
40+
indexer.insert_descriptor("internal", change_descriptor.clone())?;
41+
indexer
3942
});
4043

4144
// Assume a minimum birthday height

crates/bitcoind_rpc/tests/test_emitter.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,11 +157,11 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
157157

158158
let (mut chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
159159
let mut indexed_tx_graph = IndexedTxGraph::<BlockId, _>::new({
160-
let mut index = SpkTxOutIndex::<usize>::default();
161-
index.insert_spk(0, addr_0.script_pubkey());
162-
index.insert_spk(1, addr_1.script_pubkey());
163-
index.insert_spk(2, addr_2.script_pubkey());
164-
index
160+
let mut indexer = SpkTxOutIndex::<usize>::default();
161+
indexer.insert_spk(0, addr_0.script_pubkey());
162+
indexer.insert_spk(1, addr_1.script_pubkey());
163+
indexer.insert_spk(2, addr_2.script_pubkey());
164+
indexer
165165
});
166166

167167
let emitter = &mut Emitter::new(env.rpc_client(), chain.tip(), 0, NO_EXPECTED_MEMPOOL_TXIDS);

crates/chain/src/indexed_tx_graph.rs

Lines changed: 92 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,34 +15,19 @@ use crate::{
1515
Anchor, BlockId, ChainOracle, Indexer, Merge, TxPosInBlock,
1616
};
1717

18-
/// The [`IndexedTxGraph`] combines a [`TxGraph`] and an [`Indexer`] implementation.
18+
/// A [`TxGraph<A>`] paired with an indexer `I`, enforcing that every insertion into the graph is
19+
/// simultaneously fed through the indexer.
1920
///
20-
/// It ensures that [`TxGraph`] and [`Indexer`] are updated atomically.
21+
/// This guarantees that `tx_graph` and `index` remain in sync: any transaction or floating txout
22+
/// you add to `tx_graph` has already been processed by `index`.
2123
#[derive(Debug, Clone)]
2224
pub struct IndexedTxGraph<A, I> {
23-
/// Transaction index.
25+
/// The indexer used for filtering transactions and floating txouts that we are interested in.
2426
pub index: I,
2527
graph: TxGraph<A>,
2628
}
2729

28-
impl<A, I: Default> Default for IndexedTxGraph<A, I> {
29-
fn default() -> Self {
30-
Self {
31-
graph: Default::default(),
32-
index: Default::default(),
33-
}
34-
}
35-
}
36-
3730
impl<A, I> IndexedTxGraph<A, I> {
38-
/// Construct a new [`IndexedTxGraph`] with a given `index`.
39-
pub fn new(index: I) -> Self {
40-
Self {
41-
index,
42-
graph: TxGraph::default(),
43-
}
44-
}
45-
4631
/// Get a reference of the internal transaction graph.
4732
pub fn graph(&self) -> &TxGraph<A> {
4833
&self.graph
@@ -79,6 +64,87 @@ impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I>
7964
where
8065
I::ChangeSet: Default + Merge,
8166
{
67+
/// Create a new, empty [`IndexedTxGraph`].
68+
///
69+
/// The underlying `TxGraph` is initialized with `TxGraph::default()`, and the provided
70+
/// `index`er is used as‐is (since there are no existing transactions to process).
71+
pub fn new(index: I) -> Self {
72+
Self {
73+
index,
74+
graph: TxGraph::default(),
75+
}
76+
}
77+
78+
/// Reconstruct an [`IndexedTxGraph`] from persisted graph + indexer state.
79+
///
80+
/// 1. Rebuilds the `TxGraph` from `changeset.tx_graph`.
81+
/// 2. Calls your `indexer_from_changeset` closure on `changeset.indexer` to restore any state
82+
/// your indexer needs beyond its raw changeset.
83+
/// 3. Runs a full `.reindex()`, returning its `ChangeSet` to describe any additional updates
84+
/// applied.
85+
///
86+
/// # Errors
87+
///
88+
/// Returns `Err(E)` if `indexer_from_changeset` fails.
89+
///
90+
/// # Examples
91+
///
92+
/// ```rust,no_run
93+
/// use bdk_chain::IndexedTxGraph;
94+
/// # use bdk_chain::indexed_tx_graph::ChangeSet;
95+
/// # use bdk_chain::indexer::keychain_txout::{KeychainTxOutIndex, DEFAULT_LOOKAHEAD};
96+
/// # use bdk_core::BlockId;
97+
/// # use bdk_testenv::anyhow;
98+
/// # use miniscript::{Descriptor, DescriptorPublicKey};
99+
/// # use std::str::FromStr;
100+
/// # let persisted_changeset = ChangeSet::<BlockId, _>::default();
101+
/// # let persisted_desc = Some(Descriptor::<DescriptorPublicKey>::from_str("")?);
102+
/// # let persisted_change_desc = Some(Descriptor::<DescriptorPublicKey>::from_str("")?);
103+
///
104+
/// let (graph, reindex_cs) =
105+
/// IndexedTxGraph::from_changeset(persisted_changeset, move |idx_cs| -> anyhow::Result<_> {
106+
/// // e.g. KeychainTxOutIndex needs descriptors that weren’t in its CS
107+
/// let mut idx = KeychainTxOutIndex::from_changeset(DEFAULT_LOOKAHEAD, true, idx_cs);
108+
/// if let Some(desc) = persisted_desc {
109+
/// idx.insert_descriptor("external", desc)?;
110+
/// }
111+
/// if let Some(desc) = persisted_change_desc {
112+
/// idx.insert_descriptor("internal", desc)?;
113+
/// }
114+
/// Ok(idx)
115+
/// })?;
116+
/// # Ok::<(), anyhow::Error>(())
117+
/// ```
118+
pub fn from_changeset<F, E>(
119+
changeset: ChangeSet<A, I::ChangeSet>,
120+
indexer_from_changeset: F,
121+
) -> Result<(Self, ChangeSet<A, I::ChangeSet>), E>
122+
where
123+
F: FnOnce(I::ChangeSet) -> Result<I, E>,
124+
{
125+
let graph = TxGraph::<A>::from_changeset(changeset.tx_graph);
126+
let index = indexer_from_changeset(changeset.indexer)?;
127+
let mut out = Self { graph, index };
128+
let out_changeset = out.reindex();
129+
Ok((out, out_changeset))
130+
}
131+
132+
/// Synchronizes the indexer to reflect every entry in the transaction graph.
133+
///
134+
/// Iterates over **all** full transactions and floating outputs in `self.graph`, passing each
135+
/// into `self.index`. Any indexer-side changes produced (via `index_tx` or `index_txout`) are
136+
/// merged into a fresh `ChangeSet`, which is then returned.
137+
pub fn reindex(&mut self) -> ChangeSet<A, I::ChangeSet> {
138+
let mut changeset = ChangeSet::<A, I::ChangeSet>::default();
139+
for tx in self.graph.full_txs() {
140+
changeset.indexer.merge(self.index.index_tx(&tx));
141+
}
142+
for (op, txout) in self.graph.floating_txouts() {
143+
changeset.indexer.merge(self.index.index_txout(op, txout));
144+
}
145+
changeset
146+
}
147+
82148
fn index_tx_graph_changeset(
83149
&mut self,
84150
tx_graph_changeset: &tx_graph::ChangeSet<A>,
@@ -443,6 +509,12 @@ impl<A, IA: Default> From<tx_graph::ChangeSet<A>> for ChangeSet<A, IA> {
443509
}
444510
}
445511

512+
impl<A, IA> From<(tx_graph::ChangeSet<A>, IA)> for ChangeSet<A, IA> {
513+
fn from((tx_graph, indexer): (tx_graph::ChangeSet<A>, IA)) -> Self {
514+
Self { tx_graph, indexer }
515+
}
516+
}
517+
446518
#[cfg(feature = "miniscript")]
447519
impl<A> From<crate::keychain_txout::ChangeSet> for ChangeSet<A, crate::keychain_txout::ChangeSet> {
448520
fn from(indexer: crate::keychain_txout::ChangeSet) -> Self {

crates/chain/tests/test_indexed_tx_graph.rs

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,12 @@ fn insert_relevant_txs() {
3535
let spk_1 = descriptor.at_derivation_index(9).unwrap().script_pubkey();
3636
let lookahead = 10;
3737

38-
let mut graph = IndexedTxGraph::<ConfirmationBlockTime, KeychainTxOutIndex<()>>::new(
39-
KeychainTxOutIndex::new(lookahead, true),
40-
);
41-
let is_inserted = graph
42-
.index
43-
.insert_descriptor((), descriptor.clone())
44-
.unwrap();
45-
assert!(is_inserted);
38+
let mut graph = IndexedTxGraph::<ConfirmationBlockTime, KeychainTxOutIndex<()>>::new({
39+
let mut indexer = KeychainTxOutIndex::new(lookahead, true);
40+
let is_inserted = indexer.insert_descriptor((), descriptor.clone()).unwrap();
41+
assert!(is_inserted);
42+
indexer
43+
});
4644

4745
let tx_a = Transaction {
4846
output: vec![
@@ -160,18 +158,16 @@ fn test_list_owned_txouts() {
160158
let (desc_2, _) =
161159
Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTORS[3]).unwrap();
162160

163-
let mut graph = IndexedTxGraph::<ConfirmationBlockTime, KeychainTxOutIndex<String>>::new(
164-
KeychainTxOutIndex::new(10, true),
165-
);
166-
167-
assert!(graph
168-
.index
169-
.insert_descriptor("keychain_1".into(), desc_1)
170-
.unwrap());
171-
assert!(graph
172-
.index
173-
.insert_descriptor("keychain_2".into(), desc_2)
174-
.unwrap());
161+
let mut graph = IndexedTxGraph::<ConfirmationBlockTime, KeychainTxOutIndex<String>>::new({
162+
let mut indexer = KeychainTxOutIndex::new(10, true);
163+
assert!(indexer
164+
.insert_descriptor("keychain_1".into(), desc_1)
165+
.unwrap());
166+
assert!(indexer
167+
.insert_descriptor("keychain_2".into(), desc_2)
168+
.unwrap());
169+
indexer
170+
});
175171

176172
// Get trusted and untrusted addresses
177173

crates/esplora/tests/async_ext.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pub async fn detect_receive_tx_cancel() -> anyhow::Result<()> {
2929
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
3030
let client = Builder::new(base_url.as_str()).build_async()?;
3131

32-
let mut graph = IndexedTxGraph::<ConfirmationBlockTime, _>::new(SpkTxOutIndex::<()>::default());
32+
let mut graph = IndexedTxGraph::new(SpkTxOutIndex::<()>::default());
3333
let (chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
3434

3535
// Get receiving address.

examples/example_cli/src/lib.rs

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use bdk_chain::keychain_txout::DEFAULT_LOOKAHEAD;
12
use serde_json::json;
23
use std::cmp;
34
use std::collections::HashMap;
@@ -22,7 +23,6 @@ use bdk_chain::miniscript::{
2223
use bdk_chain::CanonicalizationParams;
2324
use bdk_chain::ConfirmationBlockTime;
2425
use bdk_chain::{
25-
indexed_tx_graph,
2626
indexer::keychain_txout::{self, KeychainTxOutIndex},
2727
local_chain::{self, LocalChain},
2828
tx_graph, ChainOracle, DescriptorExt, FullTxOut, IndexedTxGraph, Merge,
@@ -818,11 +818,10 @@ pub fn init_or_load<CS: clap::Subcommand, S: clap::Args>(
818818
Commands::Generate { network } => generate_bip86_helper(network).map(|_| None),
819819
// try load
820820
_ => {
821-
let (db, changeset) =
821+
let (mut db, changeset) =
822822
Store::<ChangeSet>::load(db_magic, db_path).context("could not open file store")?;
823823

824824
let changeset = changeset.expect("should not be empty");
825-
826825
let network = changeset.network.expect("changeset network");
827826

828827
let chain = Mutex::new({
@@ -832,23 +831,27 @@ pub fn init_or_load<CS: clap::Subcommand, S: clap::Args>(
832831
chain
833832
});
834833

835-
let graph = Mutex::new({
836-
// insert descriptors and apply loaded changeset
837-
let mut index = KeychainTxOutIndex::default();
838-
if let Some(desc) = changeset.descriptor {
839-
index.insert_descriptor(Keychain::External, desc)?;
840-
}
841-
if let Some(change_desc) = changeset.change_descriptor {
842-
index.insert_descriptor(Keychain::Internal, change_desc)?;
843-
}
844-
let mut graph = KeychainTxGraph::new(index);
845-
graph.apply_changeset(indexed_tx_graph::ChangeSet {
846-
tx_graph: changeset.tx_graph,
847-
indexer: changeset.indexer,
848-
});
849-
graph
850-
});
834+
let (graph, changeset) = IndexedTxGraph::from_changeset(
835+
(changeset.tx_graph, changeset.indexer).into(),
836+
|c| -> anyhow::Result<_> {
837+
let mut indexer =
838+
KeychainTxOutIndex::from_changeset(DEFAULT_LOOKAHEAD, true, c);
839+
if let Some(desc) = changeset.descriptor {
840+
indexer.insert_descriptor(Keychain::External, desc)?;
841+
}
842+
if let Some(change_desc) = changeset.change_descriptor {
843+
indexer.insert_descriptor(Keychain::Internal, change_desc)?;
844+
}
845+
Ok(indexer)
846+
},
847+
)?;
848+
db.append(&ChangeSet {
849+
indexer: changeset.indexer,
850+
tx_graph: changeset.tx_graph,
851+
..Default::default()
852+
})?;
851853

854+
let graph = Mutex::new(graph);
852855
let db = Mutex::new(db);
853856

854857
Ok(Some(Init {

0 commit comments

Comments
 (0)