Skip to content

feat(dash-spv): prune spent single-use CoinJoin addresses from the filter scan query - #949

Merged
xdustinface merged 3 commits into
devfrom
claude/rust-dashcore-948-667282
Aug 11, 2026
Merged

feat(dash-spv): prune spent single-use CoinJoin addresses from the filter scan query#949
xdustinface merged 3 commits into
devfrom
claude/rust-dashcore-948-667282

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Aug 10, 2026

Copy link
Copy Markdown
Member

Fixes #948.

Problem

check_compact_filters_for_elements matches the union of every monitored script against every filter, and BIP158 keys each filter's SipHashes off the block hash, so the whole query set is re-hashed and re-sorted per filter — O(M·hash + M log M) with nothing cacheable across filters. For CoinJoin wallets M grows monotonically through the scan (every mixing round pays a fresh single-use address), so late-scan filters cost several times more than early ones, exactly in the wallet's dense activity region. Profiling in the issue shows the filters phase is matching-bound (~88% of on-CPU samples), not block-bound.

Change

CoinJoin addresses are single-use by protocol — reuse would link mixing rounds — so once an address is used and all its outputs are spent, nothing ever pays it again in practice. This PR drops such spent-and-empty CoinJoin addresses from the forward-scan filter query, keeping the query roughly O(active UTXOs + gap lookahead) instead of O(total historical addresses). This is the conservative variant proposed in the issue: only CoinJoin-account scripts are pruned, since their single-use property is protocol-driven; every other account type keeps its full monitored set.

Layering:

  • key-wallet: ManagedCoreFundsAccount::unspent_or_unused_script_pubkeys() (keep a script if its address is unused — gap window, including reserved — or still holds a UTXO), and WalletInfoInterface::scan_script_pubkeys() with the full monitored set as default; the ManagedWalletInfo override applies the pruning to CoinJoin accounts only.
  • key-wallet-manager: WalletInterface::scan_script_pubkeys_for(wallet_id), defaulting to monitored_script_pubkeys_for, so existing implementations are unaffected.
  • dash-spv: scan_batch builds its union query and per-wallet attribution queries from the scan set. rescan_batch (freshly derived scripts, never dead), block processing, gap-limit maintenance, and the bloom/mempool paths all keep using the full monitored set — pruning only narrows which blocks the filter scan downloads.

The theoretical "someone paid an old CoinJoin address after it was emptied" case is deliberately accepted per the issue: it does not occur under the CoinJoin protocol, and only the filter-scan query is affected.

Testing

  • New unit tests at each layer: pruning semantics on the funds account, CoinJoin-only scoping at the wallet-info level (used-and-empty Standard addresses stay), manager-level plumbing, and a scan_batch test proving a block paying only a pruned address is not downloaded while the monitored set still contains it.
  • Full cargo test -p dash-spv with dashd regtest integration tests (sync, restart, disconnect, transaction) — all pass.
  • cargo test -p dash-spv-ffi --test dashd_sync — 7/7 pass.
  • Workspace-strict clippy (contrib/run_clippy.py), cargo fmt, and pre-commit hooks all clean.

Related (not addressed here)

The issue's rayon dispatch overhead (~32%) and per-filter query re-sort (~12%) observations are separate follow-ups.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved compact-filter scanning by excluding spent, single-use addresses that no longer need monitoring.
    • Continued scanning unused addresses and addresses associated with unspent funds.
    • Preserved full monitoring for block processing and other account types.
  • Bug Fixes

    • Prevented unnecessary block-download events caused by stale address matches.
    • Restored scan coverage when previously used addresses receive funds again.
  • Performance

    • Reduced compact-filter query sizes for wallets with extensive address histories.

…lter scan query

During filter sync, check_compact_filters_for_elements re-hashes and
re-sorts the whole query set per filter (BIP158 keys SipHashes off the
block hash), so per-filter cost grows with the monitored script count.
For CoinJoin wallets that count grows monotonically through the scan —
every mixing round pays a fresh single-use address — so late-scan
filters cost several times more than early ones, concentrated in the
wallet's dense activity region.

CoinJoin addresses are single-use by protocol (reuse would link mixing
rounds): once an address is used and holds no unspent output, nothing
ever pays it again, so it contributes nothing to a forward scan. Drop
such addresses from the scan query, keeping it roughly bounded by
active UTXOs + gap lookahead instead of total historical addresses.

- key-wallet: ManagedCoreFundsAccount::unspent_or_unused_script_pubkeys
  plus WalletInfoInterface::scan_script_pubkeys (default = full
  monitored set; ManagedWalletInfo prunes CoinJoin accounts only)
- key-wallet-manager: WalletInterface::scan_script_pubkeys_for
  (default = monitored_script_pubkeys_for)
- dash-spv: scan_batch queries the scan set; rescan_batch (freshly
  derived scripts) and block processing keep the full monitored set

Fixes #948

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 414dc26e-7f66-481a-afc2-fc468bd39102

📥 Commits

Reviewing files that changed from the base of the PR and between 14b4123 and 53fd906.

📒 Files selected for processing (1)
  • key-wallet-manager/benches/filter_scan.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • key-wallet-manager/benches/filter_scan.rs

📝 Walkthrough

Walkthrough

The change adds wallet-specific compact-filter scan queries. Used, empty CoinJoin scripts are excluded. Unused and funded scripts remain. Full monitored scripts remain available for block processing.

Changes

Scan query pruning

Layer / File(s) Summary
Wallet scan-script selection
key-wallet/src/wallet/managed_wallet_info/..., key-wallet/src/managed_account/..., key-wallet/src/tests/...
ManagedWalletInfo and ManagedCoreFundsAccount select unspent or unused CoinJoin scripts. Tests cover CoinJoin and standard-address behavior.
Wallet interface and test wiring
key-wallet-manager/src/wallet_interface.rs, key-wallet-manager/src/process_block.rs, key-wallet-manager/src/test_utils/mock_wallet.rs
WalletInterface exposes per-wallet scan-script selection. MultiMockWallet supports scan-address overrides. Manager tests cover known and unknown wallets.
Compact-filter integration and benchmarking
dash-spv/src/sync/filters/manager.rs, key-wallet-manager/Cargo.toml, key-wallet-manager/benches/filter_scan.rs
scan_batch uses the scan-script set. Regression tests and a Criterion benchmark compare pruned and monitored queries.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FilterManager
  participant WalletInterface
  participant ManagedWalletInfo
  FilterManager->>WalletInterface: scan_script_pubkeys_for(wallet_id)
  WalletInterface->>ManagedWalletInfo: scan_script_pubkeys()
  ManagedWalletInfo-->>WalletInterface: unspent or unused scripts
  WalletInterface-->>FilterManager: scan script set
  FilterManager->>FilterManager: match compact filters
Loading

Suggested labels: ready-for-review

Suggested reviewers: zocolini

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes pruning spent single-use CoinJoin addresses from the filter scan query.
Linked Issues check ✅ Passed The changes implement issue #948 by pruning spent-and-empty CoinJoin scripts while preserving live and non-CoinJoin scripts.
Out of Scope Changes check ✅ Passed The code, tests, plumbing, and benchmark directly support the linked issue and stated optimization objective.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/rust-dashcore-948-667282

Comment @coderabbitai help to get the list of available commands.

@QuantumExplorer QuantumExplorer changed the title perf(dash-spv): prune spent single-use CoinJoin addresses from the filter scan query feat(dash-spv): prune spent single-use CoinJoin addresses from the filter scan query Aug 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs`:
- Around line 93-105: Update the pull request title to use the supported
Conventional Commit prefix `feat:`, resulting in `feat: pruned compact-filter
scan queries`.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0792ff1e-ddaf-4768-a117-ae4752808fe5

📥 Commits

Reviewing files that changed from the base of the PR and between 5a80bd7 and 1f7414f.

📒 Files selected for processing (8)
  • dash-spv/src/sync/filters/manager.rs
  • key-wallet-manager/src/process_block.rs
  • key-wallet-manager/src/test_utils/mock_wallet.rs
  • key-wallet-manager/src/wallet_interface.rs
  • key-wallet/src/managed_account/managed_core_funds_account.rs
  • key-wallet/src/tests/mod.rs
  • key-wallet/src/tests/scan_script_pubkeys_tests.rs
  • key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs

Comment thread key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 10, 2026
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.22222% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.42%. Comparing base (5a80bd7) to head (53fd906).

Files with missing lines Patch % Lines
key-wallet-manager/src/wallet_interface.rs 0.00% 3 Missing ⚠️
...allet/managed_wallet_info/wallet_info_interface.rs 80.00% 3 Missing ⚠️
dash-spv/src/sync/filters/manager.rs 97.67% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #949      +/-   ##
==========================================
+ Coverage   75.40%   75.42%   +0.02%     
==========================================
  Files         328      328              
  Lines       78541    78630      +89     
==========================================
+ Hits        59222    59305      +83     
- Misses      19319    19325       +6     
Flag Coverage Δ
core 77.29% <ø> (ø)
ffi 49.04% <ø> (ø)
rpc 20.00% <ø> (ø)
spv 91.37% <97.67%> (-0.10%) ⬇️
wallet 77.48% <87.23%> (+0.11%) ⬆️
Files with missing lines Coverage Δ
key-wallet-manager/src/process_block.rs 92.30% <100.00%> (+1.68%) ⬆️
.../src/managed_account/managed_core_funds_account.rs 80.04% <100.00%> (+0.46%) ⬆️
dash-spv/src/sync/filters/manager.rs 97.93% <97.67%> (-0.10%) ⬇️
key-wallet-manager/src/wallet_interface.rs 9.37% <0.00%> (-0.97%) ⬇️
...allet/managed_wallet_info/wallet_info_interface.rs 79.79% <80.00%> (+2.58%) ⬆️

... and 6 files with indirect coverage changes

@github-actions github-actions Bot added the ready-for-review CodeRabbit has approved this PR label Aug 11, 2026
…CoinJoin wallets

Criterion bench that mimics a wallet mid-recovery after many CoinJoin
rounds: `used` spent single-use addresses on the external branch, 200
still-funded denominations, and the default gap lookahead. One 512-filter
scan batch is matched with the full monitored query (pre-#948) and the
pruned scan query.

Measured on Apple Silicon (single-threaded, default features):

  used=500   monitored 855 scripts  5.21ms | pruned 555 scripts 3.33ms (1.6x)
  used=2000  monitored 2355 scripts 15.2ms | pruned 555 scripts 3.54ms (4.3x)
  used=6000  monitored 6355 scripts 45.8ms | pruned 555 scripts 3.30ms (13.9x)

The pruned query stays flat as mixing history grows, while the monitored
query's per-batch cost scales super-linearly with total historical
addresses — the effect profiled in #948.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@QuantumExplorer

Copy link
Copy Markdown
Member Author

Added a criterion benchmark (key-wallet-manager/benches/filter_scan.rs) that mimics a mixing-heavy CoinJoin wallet mid-recovery: used spent single-use addresses accumulated across mixing rounds, 200 still-funded denominations, and the default gap-limit lookahead. It matches one 512-filter scan batch with the full monitored query (pre-#948 behavior) vs the pruned scan query.

Results on Apple Silicon (single-threaded, default features):

used CoinJoin addresses monitored query pruned query monitored pruned speedup
500 855 scripts 555 scripts 5.21 ms 3.33 ms 1.6×
2,000 2,355 scripts 555 scripts 15.25 ms 3.54 ms 4.3×
6,000 6,355 scripts 555 scripts 45.8 ms 3.30 ms 13.9×

The pruned query stays flat as mixing history grows, while the monitored query's per-batch cost scales super-linearly with total historical addresses (per-filter SipHash re-key + re-sort) — the effect profiled in #948. Run it with:

cargo bench -p key-wallet-manager --bench filter_scan

🤖 Generated with Claude Code

@github-actions github-actions Bot removed the ready-for-review CodeRabbit has approved this PR label Aug 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@key-wallet-manager/benches/filter_scan.rs`:
- Around line 37-39: Remove the hardcoded MNEMONIC constant and update the
benchmark setup in the surrounding filter-scan benchmark to generate mnemonic
entropy with a cryptographically secure random number generator before b.iter
begins. Preserve the benchmark’s wallet derivation and timed matching path while
ensuring each setup generates fresh secure entropy instead of deterministic
keys.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 86420851-0af5-4f76-8c52-37cdb61e7a54

📥 Commits

Reviewing files that changed from the base of the PR and between 1f7414f and 14b4123.

📒 Files selected for processing (2)
  • key-wallet-manager/Cargo.toml
  • key-wallet-manager/benches/filter_scan.rs

Comment thread key-wallet-manager/benches/filter_scan.rs Outdated
… mnemonic

Drop the hardcoded BIP39 test mnemonic from the filter-scan bench; the
workload is defined by pool/UTXO counts, not key material, so a fresh
random mnemonic per run keeps timings comparable while following the
no-hardcoded-keys guideline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@xdustinface
xdustinface merged commit 37b1a36 into dev Aug 11, 2026
38 of 39 checks passed
@xdustinface
xdustinface deleted the claude/rust-dashcore-948-667282 branch August 11, 2026 10:26
ZocoLini pushed a commit that referenced this pull request Aug 11, 2026
…lter scan query (#949)

* perf(dash-spv): prune spent single-use CoinJoin addresses from the filter scan query

During filter sync, check_compact_filters_for_elements re-hashes and
re-sorts the whole query set per filter (BIP158 keys SipHashes off the
block hash), so per-filter cost grows with the monitored script count.
For CoinJoin wallets that count grows monotonically through the scan —
every mixing round pays a fresh single-use address — so late-scan
filters cost several times more than early ones, concentrated in the
wallet's dense activity region.

CoinJoin addresses are single-use by protocol (reuse would link mixing
rounds): once an address is used and holds no unspent output, nothing
ever pays it again, so it contributes nothing to a forward scan. Drop
such addresses from the scan query, keeping it roughly bounded by
active UTXOs + gap lookahead instead of total historical addresses.

- key-wallet: ManagedCoreFundsAccount::unspent_or_unused_script_pubkeys
  plus WalletInfoInterface::scan_script_pubkeys (default = full
  monitored set; ManagedWalletInfo prunes CoinJoin accounts only)
- key-wallet-manager: WalletInterface::scan_script_pubkeys_for
  (default = monitored_script_pubkeys_for)
- dash-spv: scan_batch queries the scan set; rescan_batch (freshly
  derived scripts) and block processing keep the full monitored set

Fixes #948

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(key-wallet-manager): benchmark filter matching for mixing-heavy CoinJoin wallets

Criterion bench that mimics a wallet mid-recovery after many CoinJoin
rounds: `used` spent single-use addresses on the external branch, 200
still-funded denominations, and the default gap lookahead. One 512-filter
scan batch is matched with the full monitored query (pre-#948) and the
pruned scan query.

Measured on Apple Silicon (single-threaded, default features):

  used=500   monitored 855 scripts  5.21ms | pruned 555 scripts 3.33ms (1.6x)
  used=2000  monitored 2355 scripts 15.2ms | pruned 555 scripts 3.54ms (4.3x)
  used=6000  monitored 6355 scripts 45.8ms | pruned 555 scripts 3.30ms (13.9x)

The pruned query stays flat as mixing history grows, while the monitored
query's per-batch cost scales super-linearly with total historical
addresses — the effect profiled in #948.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(key-wallet-manager): generate the benchmark wallet from a random mnemonic

Drop the hardcoded BIP39 test mnemonic from the filter-scan bench; the
workload is defined by pool/UTXO counts, not key material, so a fresh
random mnemonic per run keeps timings comparable while following the
no-hardcoded-keys guideline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-review CodeRabbit has approved this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dash-spv: prune spent single-use (CoinJoin) addresses from the filter query set during scan

2 participants