feat(dash-spv): prune spent single-use CoinJoin addresses from the filter scan query - #949
Conversation
…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>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe 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. ChangesScan query pruning
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
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (8)
dash-spv/src/sync/filters/manager.rskey-wallet-manager/src/process_block.rskey-wallet-manager/src/test_utils/mock_wallet.rskey-wallet-manager/src/wallet_interface.rskey-wallet/src/managed_account/managed_core_funds_account.rskey-wallet/src/tests/mod.rskey-wallet/src/tests/scan_script_pubkeys_tests.rskey-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs
Codecov Report❌ Patch coverage is 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
|
…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>
|
Added a criterion benchmark ( Results on Apple Silicon (single-threaded, default features):
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: 🤖 Generated with Claude Code |
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
key-wallet-manager/Cargo.tomlkey-wallet-manager/benches/filter_scan.rs
… 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>
…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>
Fixes #948.
Problem
check_compact_filters_for_elementsmatches 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 walletsMgrows 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 ofO(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:
ManagedCoreFundsAccount::unspent_or_unused_script_pubkeys()(keep a script if its address is unused — gap window, including reserved — or still holds a UTXO), andWalletInfoInterface::scan_script_pubkeys()with the full monitored set as default; theManagedWalletInfooverride applies the pruning to CoinJoin accounts only.WalletInterface::scan_script_pubkeys_for(wallet_id), defaulting tomonitored_script_pubkeys_for, so existing implementations are unaffected.scan_batchbuilds 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
scan_batchtest proving a block paying only a pruned address is not downloaded while the monitored set still contains it.cargo test -p dash-spvwith dashd regtest integration tests (sync, restart, disconnect, transaction) — all pass.cargo test -p dash-spv-ffi --test dashd_sync— 7/7 pass.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
Bug Fixes
Performance