Skip to content

Commit e217542

Browse files
committed
Merge #1963: Persist spks derived from KeychainTxOutIndex
62767f0 fix(rusqlite_impl): Fix derived spks create table statement (valued mammal) 603f133 docs(chain): Adds docs for persisting spks in `KeychainTxOutIndex` (志宇) 3126cd2 feat(chain)!: Clean up ergonomics of `IndexedTxGraph` (志宇) a055581 feat(chain): `KeychainTxOutIndex`: Make spk cache optional (志宇) 19e3b5d feat(chain): Add `KeychainTxOutIndex::from_changeset` constructor (志宇) d761265 feat(chain): `KeychainTxOutIndex`: Debug build checks (志宇) 76875e7 fix(chain)!: API and logical issues in `KeychainTxOutIndex` (志宇) d299dae feat(chain)!: Persist spks derived from `KeychainTxOutIndex` (志宇) Pull request description: Replaces #1960 Fixes #1964 ### Description Users with large wallet and/or complex descriptors may experience slow startup of `KeychainTxOutIndex`. This PR addresses this problem by providing the option to persist derived spks so that they no longer need to be re-derived on startup. The `IndexedTxGraph` API has been changed for better ergonomics. Compared to #1960, this is a more longterm solution that does not depend on multi-threading logic. ### Changelog notice ```md Changed - `KeychainTxOutIndex::new` to take in an additional parameter `persist_spks` to control whether derived spks are cached and persisted across restarts. The default of `persist_spks` is false. - `KeychainTxOutIndex` methods (`lookahead_to_target, `next_unused_spk`, `reveal_next_spk`) now return changesets as they may derive spks to be persisted. - The `InsertDescriptorError` type now wraps descriptors in `Box` to reduce enum size. Added - `spk_cache` field to `indexer::keychain_txout::ChangeSet` which persists derived spks. - `IndexedTxGraph::from_changeset` - allows constructing from `indexed_tx_graph::ChangeSet` and only indexing when ready. - `IndexedTxGraph::reindex` method. Fixed - `KeychainTxOutIndex::reveal_to_target` so that it actually returns `None` if the `keychain` does not exist. ``` ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo +nightly fmt` and `cargo clippy` before committing #### New Features: * [ ] I've added tests for the new feature * [x] I've added docs for the new feature ACKs for top commit: ValuedMammal: ACK 62767f0 notmandatory: ACK 62767f0 Tree-SHA512: dc1aa4308ffcc6d121e0d7a1ca4ff9f641ed5db63204747fde47ac02e45dae9b65da95554541705a98b69e59f741c043485a26db736966417061a4c9d220ba29
2 parents cb66f00 + 62767f0 commit e217542

File tree

9 files changed

+507
-137
lines changed

9 files changed

+507
-137
lines changed

crates/bitcoind_rpc/examples/filter_iter.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ fn main() -> anyhow::Result<()> {
3131
let (descriptor, _) = Descriptor::parse_descriptor(&secp, EXTERNAL)?;
3232
let (change_descriptor, _) = Descriptor::parse_descriptor(&secp, INTERNAL)?;
3333
let (mut chain, _) = LocalChain::from_genesis_hash(genesis_block(NETWORK).block_hash());
34+
3435
let mut graph = IndexedTxGraph::<ConfirmationBlockTime, KeychainTxOutIndex<&str>>::new({
3536
let mut index = KeychainTxOutIndex::default();
3637
index.insert_descriptor("external", descriptor.clone())?;

crates/chain/benches/canonicalization.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ fn setup<F: Fn(&mut KeychainTxGraph, &LocalChain)>(f: F) -> (KeychainTxGraph, Lo
8282

8383
let (desc, _) =
8484
<Descriptor<DescriptorPublicKey>>::parse_descriptor(&Secp256k1::new(), DESC).unwrap();
85-
let mut index = KeychainTxOutIndex::new(10);
85+
let mut index = KeychainTxOutIndex::new(10, true);
8686
index.insert_descriptor((), desc).unwrap();
8787
let mut tx_graph = KeychainTxGraph::new(index);
8888

crates/chain/src/indexed_tx_graph.rs

Lines changed: 92 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ 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
}
@@ -35,14 +37,6 @@ impl<A, I: Default> Default for IndexedTxGraph<A, I> {
3537
}
3638

3739
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-
4640
/// Get a reference of the internal transaction graph.
4741
pub fn graph(&self) -> &TxGraph<A> {
4842
&self.graph
@@ -79,6 +73,87 @@ impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I>
7973
where
8074
I::ChangeSet: Default + Merge,
8175
{
76+
/// Create a new, empty [`IndexedTxGraph`].
77+
///
78+
/// The underlying `TxGraph` is initialized with `TxGraph::default()`, and the provided
79+
/// `index`er is used as‐is (since there are no existing transactions to process).
80+
pub fn new(index: I) -> Self {
81+
Self {
82+
index,
83+
graph: TxGraph::default(),
84+
}
85+
}
86+
87+
/// Reconstruct an [`IndexedTxGraph`] from persisted graph + indexer state.
88+
///
89+
/// 1. Rebuilds the `TxGraph` from `changeset.tx_graph`.
90+
/// 2. Calls your `indexer_from_changeset` closure on `changeset.indexer` to restore any state
91+
/// your indexer needs beyond its raw changeset.
92+
/// 3. Runs a full `.reindex()`, returning its `ChangeSet` to describe any additional updates
93+
/// applied.
94+
///
95+
/// # Errors
96+
///
97+
/// Returns `Err(E)` if `indexer_from_changeset` fails.
98+
///
99+
/// # Examples
100+
///
101+
/// ```rust,no_run
102+
/// use bdk_chain::IndexedTxGraph;
103+
/// # use bdk_chain::indexed_tx_graph::ChangeSet;
104+
/// # use bdk_chain::indexer::keychain_txout::{KeychainTxOutIndex, DEFAULT_LOOKAHEAD};
105+
/// # use bdk_core::BlockId;
106+
/// # use bdk_testenv::anyhow;
107+
/// # use miniscript::{Descriptor, DescriptorPublicKey};
108+
/// # use std::str::FromStr;
109+
/// # let persisted_changeset = ChangeSet::<BlockId, _>::default();
110+
/// # let persisted_desc = Some(Descriptor::<DescriptorPublicKey>::from_str("")?);
111+
/// # let persisted_change_desc = Some(Descriptor::<DescriptorPublicKey>::from_str("")?);
112+
///
113+
/// let (graph, reindex_cs) =
114+
/// IndexedTxGraph::from_changeset(persisted_changeset, move |idx_cs| -> anyhow::Result<_> {
115+
/// // e.g. KeychainTxOutIndex needs descriptors that weren’t in its CS
116+
/// let mut idx = KeychainTxOutIndex::from_changeset(DEFAULT_LOOKAHEAD, true, idx_cs);
117+
/// if let Some(desc) = persisted_desc {
118+
/// idx.insert_descriptor("external", desc)?;
119+
/// }
120+
/// if let Some(desc) = persisted_change_desc {
121+
/// idx.insert_descriptor("internal", desc)?;
122+
/// }
123+
/// Ok(idx)
124+
/// })?;
125+
/// # Ok::<(), anyhow::Error>(())
126+
/// ```
127+
pub fn from_changeset<F, E>(
128+
changeset: ChangeSet<A, I::ChangeSet>,
129+
indexer_from_changeset: F,
130+
) -> Result<(Self, ChangeSet<A, I::ChangeSet>), E>
131+
where
132+
F: FnOnce(I::ChangeSet) -> Result<I, E>,
133+
{
134+
let graph = TxGraph::<A>::from_changeset(changeset.tx_graph);
135+
let index = indexer_from_changeset(changeset.indexer)?;
136+
let mut out = Self { graph, index };
137+
let out_changeset = out.reindex();
138+
Ok((out, out_changeset))
139+
}
140+
141+
/// Synchronizes the indexer to reflect every entry in the transaction graph.
142+
///
143+
/// Iterates over **all** full transactions and floating outputs in `self.graph`, passing each
144+
/// into `self.index`. Any indexer-side changes produced (via `index_tx` or `index_txout`) are
145+
/// merged into a fresh `ChangeSet`, which is then returned.
146+
pub fn reindex(&mut self) -> ChangeSet<A, I::ChangeSet> {
147+
let mut changeset = ChangeSet::<A, I::ChangeSet>::default();
148+
for tx in self.graph.full_txs() {
149+
changeset.indexer.merge(self.index.index_tx(&tx));
150+
}
151+
for (op, txout) in self.graph.floating_txouts() {
152+
changeset.indexer.merge(self.index.index_txout(op, txout));
153+
}
154+
changeset
155+
}
156+
82157
fn index_tx_graph_changeset(
83158
&mut self,
84159
tx_graph_changeset: &tx_graph::ChangeSet<A>,
@@ -443,6 +518,12 @@ impl<A, IA: Default> From<tx_graph::ChangeSet<A>> for ChangeSet<A, IA> {
443518
}
444519
}
445520

521+
impl<A, IA> From<(tx_graph::ChangeSet<A>, IA)> for ChangeSet<A, IA> {
522+
fn from((tx_graph, indexer): (tx_graph::ChangeSet<A>, IA)) -> Self {
523+
Self { tx_graph, indexer }
524+
}
525+
}
526+
446527
#[cfg(feature = "miniscript")]
447528
impl<A> From<crate::keychain_txout::ChangeSet> for ChangeSet<A, crate::keychain_txout::ChangeSet> {
448529
fn from(indexer: crate::keychain_txout::ChangeSet) -> Self {

0 commit comments

Comments
 (0)