Skip to content

Comments

Persist spks derived from KeychainTxOutIndex#1963

Merged
ValuedMammal merged 8 commits intobitcoindevkit:masterfrom
evanlinjin:feature/spk_cache
May 27, 2025
Merged

Persist spks derived from KeychainTxOutIndex#1963
ValuedMammal merged 8 commits intobitcoindevkit:masterfrom
evanlinjin:feature/spk_cache

Conversation

@evanlinjin
Copy link
Member

@evanlinjin evanlinjin commented May 22, 2025

Replaces #1960
Fixes #1953
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

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:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo +nightly fmt and cargo clippy before committing

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature

@evanlinjin evanlinjin force-pushed the feature/spk_cache branch 4 times, most recently from 7afd8b6 to c279c99 Compare May 22, 2025 08:25
@evanlinjin evanlinjin force-pushed the feature/spk_cache branch from c279c99 to d299dae Compare May 22, 2025 08:41
@evanlinjin evanlinjin requested a review from ValuedMammal May 22, 2025 10:50
@evanlinjin evanlinjin added the api A breaking API change label May 22, 2025
@evanlinjin evanlinjin moved this to In Progress in BDK Chain May 22, 2025
@notmandatory notmandatory added this to the Wallet 2.0.0 milestone May 22, 2025
@evanlinjin evanlinjin force-pushed the feature/spk_cache branch from 90b6813 to 76875e7 Compare May 23, 2025 00:51
* When merging changesets, assert that spk of a given descriptor id &
  derivation index does not get changed.
* When reading spk from cache, check the spk by deriving it.
@evanlinjin evanlinjin force-pushed the feature/spk_cache branch from fb838d9 to d761265 Compare May 23, 2025 01:36
This incentivies constructing `KeychainTxOutIndex` from a changeset
before inserting descriptors (to make use of the spk cache).
@evanlinjin evanlinjin force-pushed the feature/spk_cache branch from 9d97b29 to 4bc8cc0 Compare May 23, 2025 08:35
Also added staging changes to `ChangeSet::spk_cache`. This way, we can
avoid returning `ChangeSet`s for `apply_changeset` and
`insert_descriptor`.

* `KeychainTxOutIndex::new` now takes in an additional `use_spk_cache`
  parameter.
* Fixed `reveal_to_target` method to actually return `None` if the
  keychain does not exist.
@evanlinjin evanlinjin force-pushed the feature/spk_cache branch from 4bc8cc0 to bbbc054 Compare May 23, 2025 08:36
* `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
@evanlinjin evanlinjin force-pushed the feature/spk_cache branch from bbbc054 to 3126cd2 Compare May 23, 2025 10:34
@evanlinjin evanlinjin force-pushed the feature/spk_cache branch from 8fef7c7 to 603f133 Compare May 23, 2025 11:52
@evanlinjin evanlinjin marked this pull request as ready for review May 23, 2025 12:12
Do not reference last revealed table, in case none are revealed.
Correct SQL column name.
Copy link
Collaborator

@ValuedMammal ValuedMammal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 62767f0

@ValuedMammal ValuedMammal requested a review from LagginTimes May 26, 2025 15:50
@notmandatory notmandatory moved this from In Progress to Needs Review in BDK Chain May 26, 2025
@notmandatory
Copy link
Member

notmandatory commented May 27, 2025

Might have a problem with the sqlite store. I'm manually testing and looks like it's storing the same script for all indexes. I'll keep looking but wanted to give a headsup not to merge this yet.
Nevermind, it's working fine, my sqlite select just wan't showing the blog data.

Copy link
Member

@notmandatory notmandatory left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 62767f0

tested with bdk-cli with cache enabled and sqlite schema updated and new table is being populated.

@ValuedMammal ValuedMammal merged commit e217542 into bitcoindevkit:master May 27, 2025
19 checks passed
@github-project-automation github-project-automation bot moved this from Needs Review to Done in BDK Chain May 27, 2025
@ValuedMammal ValuedMammal mentioned this pull request May 27, 2025
39 tasks
///
/// 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// e.g. KeychainTxOutIndex needs descriptors that weren’t in its change set.


impl<K> KeychainTxOutIndex<K> {
/// Construct a [`KeychainTxOutIndex`] with the given `lookahead`.
/// Construct a [`KeychainTxOutIndex`] with the given `lookahead` and `use_spk_cache` boolean.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/// Construct a [`KeychainTxOutIndex`] with the given `lookahead` and `persist_spks` boolean.

/// // Derive 20 future addresses per chain and persist + reload script pubkeys via ChangeSets:
/// let idx = KeychainTxOutIndex::<&'static str>::new(20, true);
///
/// // Derive 10 future addresses per chain without persistence:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per "keychain" ?

impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// Construct `KeychainTxOutIndex<K>` from the given `changeset`.
///
/// Shorthand for called [`new`] and then [`apply_changeset`].
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shorthand for "calling"...

///
/// It tracks:
/// 1. `last_revealed`: the highest derivation index revealed per descriptor.
/// 2. `spks`: the cache of derived script pubkeys to persist across runs.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/// 2. `spk_cache`: ...

ValuedMammal added a commit to ValuedMammal/bdk that referenced this pull request May 30, 2025
This commit addresses some leftover comments from bitcoindevkit#1963.
ValuedMammal added a commit to ValuedMammal/bdk that referenced this pull request Feb 10, 2026
`spk_cache_stage` was being used to hold newly derived
SPKs before being transferred to the `ChangeSet`. As a result
it became necessary to call `_empty_stage_into_changeset` whenever
the internal state changed, requiring the developer to know
which methods depended on the functionality, e.g. `scan_txout`
and `reveal_next_spk`.

Further, this altered the behavior of the `ChangeSet` in
surprising ways. For example revealing 1 SPK might return
additional scripts in the `ChangeSet` if they had
previously been staged.

The rationale in bitcoindevkit#1963 was to avoid returning a `ChangeSet` from
`insert_descriptor`. We can do this by providing another
method that returns a tuple of `(bool, ChangeSet)` so that
descriptor insertion results in updating the `ChangeSet` directly.
Any code using the original `insert_descriptor` can still access the
`ChangeSet` using `initial_changeset` (which may include SPKs
from other descriptors), or we can expose an accessor to get a
reference to the `spk_cache`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api A breaking API change

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

Implement IndexedTxGraph::reindex Generating SPK Index upon loading wallet is slow for large wallets with lots of addresses

3 participants