Skip to content

Conversation

@xdustinface
Copy link
Collaborator

@xdustinface xdustinface commented Dec 23, 2025

Introduce parallel filter matching for the wallet, not currently in use in this PR but preparation for follow up sync improvements. This leverages rayon to match filters in parallel. This brings batch processing of 5000 filters in #257 from ~16s down to ~2s. I did some benchmarking on this and it appears to be the filter size which slows things down.

Summary by CodeRabbit

  • New Features

    • Added address-based compact-filter matching with parallel processing for faster filter checks.
    • Wallet interface now exposes monitored addresses plus optional metadata (description, earliest required height, transaction effect).
  • Behavior Changes

    • Replaced direct wallet-based filter checks with address-driven matching; syncing now handles filter matches by explicit match results and phase-aware processing.
  • Tests

    • Removed integration tests covering wallet filter-checking and filter caching.
  • Chores

    • Added a parallel-processing dependency to improve performance.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 23, 2025

📝 Walkthrough

Walkthrough

Refactors compact-filter matching from a wallet-driven async check to an address-driven, parallel matching module; updates WalletInterface, wallet manager, SPV message handling, tests, and adds rayon dependency and new matching module with unit tests.

Changes

Cohort / File(s) Summary
Trait & Interface Updates
key-wallet-manager/src/wallet_interface.rs, key-wallet-manager/src/test_utils/wallet.rs
Removed async check_compact_filter from WalletInterface; added fn monitored_addresses(&self) -> Vec<Address> and three optional default async methods (transaction_effect, earliest_required_height, describe); updated test mock implementations to implement monitored_addresses and removed mock check_compact_filter.
New Parallel Matching Module
key-wallet-manager/src/wallet_manager/matching.rs
Added FilterMatchKey (height + hash) and check_compact_filters_for_addresses(input: &HashMap<FilterMatchKey, BlockFilter>, addresses: Vec<Address>) -> BTreeSet<FilterMatchKey> that parallel-iterates filters (rayon) and returns matching keys; includes unit tests.
Module Exports & Dependency
key-wallet-manager/src/wallet_manager/mod.rs, key-wallet-manager/Cargo.toml
Added mod matching and pub use exports for FilterMatchKey and check_compact_filters_for_addresses; added rayon = "1.11" dependency.
WalletManager Implementation
key-wallet-manager/src/wallet_manager/process_block.rs
Removed check_compact_filter implementation from WalletManager; added fn monitored_addresses(&self) -> Vec<Address> that delegates to self.monitored_addresses(); replaced BlockFilter/BlockHash imports with Address where applicable.
SPV Sync Legacy Filter Handling
dash-spv/src/sync/legacy/filters/matching.rs, dash-spv/src/sync/legacy/message_handlers.rs
Removed wallet-driven check_filter_for_matches(...) helper; refactored message handling to build a HashMap<FilterMatchKey, BlockFilter> and call check_compact_filters_for_addresses, act on returned set of matches (non-empty) instead of boolean, and exposed handle_message() as pub.
Tests Cleanup
key-wallet-manager/tests/spv_integration_tests.rs
Removed test_filter_checking and test_filter_caching integration tests and related BlockFilter import; other tests unchanged.

Sequence Diagram(s)

sequenceDiagram
    participant SyncMgr as SyncManager
    participant WalletMgr as WalletManager
    participant Matching as FilterMatching
    participant BlockStore as BlockStorage

    Note over SyncMgr,WalletMgr: Old flow (removed): wallet-owned filter check
    SyncMgr->>WalletMgr: check_compact_filter(filter, block_hash)
    activate WalletMgr
    WalletMgr->>WalletMgr: examine monitored addresses & match
    WalletMgr-->>SyncMgr: bool (match/no-match)
    deactivate WalletMgr

    Note over SyncMgr,Matching: New flow (added): address-driven, parallel matching
    SyncMgr->>WalletMgr: monitored_addresses()
    activate WalletMgr
    WalletMgr-->>SyncMgr: Vec<Address>
    deactivate WalletMgr

    SyncMgr->>Matching: check_compact_filters_for_addresses(filters_map, addresses)
    activate Matching
    Matching->>Matching: parallel iterate filters (rayon)
    Matching->>Matching: for each filter: match_any(address_scripts, hash)
    Matching-->>SyncMgr: BTreeSet<FilterMatchKey> (matches)
    deactivate Matching

    alt matches found
        SyncMgr->>BlockStore: request blocks for matched heights
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • ZocoLini

Poem

🐰 I hopped through code both day and night,

Matched addresses fast with rayon's light,
Filters fell back as addresses took stage,
New keys and maps now book the page,
Hooray — compact checks turned into a sage!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.28% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: implement parallel filter matching' directly and clearly describes the main change: introducing parallel filter matching functionality via the rayon crate, which is the core objective of this PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@xdustinface xdustinface force-pushed the feat/parallel-filter-matching branch from f70a140 to f4cd072 Compare December 23, 2025 07:36
@github-actions github-actions bot added the merge-conflict The PR conflicts with the target branch. label Dec 24, 2025
@github-actions
Copy link

This PR has merge conflicts with the base branch. Please rebase or merge the base branch into your branch to resolve them.

@xdustinface xdustinface changed the title feat: Implement parallel filter matching feat: implement parallel filter matching Dec 24, 2025
@xdustinface xdustinface force-pushed the feat/parallel-filter-matching branch from f4cd072 to b00b66c Compare December 30, 2025 12:50
@github-actions github-actions bot removed the merge-conflict The PR conflicts with the target branch. label Dec 30, 2025
@xdustinface xdustinface changed the base branch from v0.41-dev to v0.42-dev December 30, 2025 22:35
@github-actions github-actions bot added the merge-conflict The PR conflicts with the target branch. label Jan 8, 2026
@github-actions
Copy link

github-actions bot commented Jan 8, 2026

This PR has merge conflicts with the base branch. Please rebase or merge the base branch into your branch to resolve them.

@xdustinface xdustinface force-pushed the feat/parallel-filter-matching branch from b00b66c to bf5bb3a Compare January 16, 2026 23:19
@github-actions github-actions bot removed the merge-conflict The PR conflicts with the target branch. label Jan 16, 2026
@xdustinface xdustinface force-pushed the feat/parallel-filter-matching branch from bf5bb3a to b7b44b3 Compare January 16, 2026 23:54
@xdustinface xdustinface marked this pull request as ready for review January 17, 2026 03:03
@xdustinface xdustinface requested a review from ZocoLini January 17, 2026 03:03
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@key-wallet-manager/Cargo.toml`:
- Line 28: The crate currently unconditionally depends on rayon (rayon =
"1.11"), which breaks no_std builds; make the dependency optional and tie it to
the crate's std feature by changing the Cargo.toml entry to an optional
dependency (rayon = { version = "1.11", optional = true }) and add a features
table that maps the std feature to enable rayon (e.g. features = { std =
["rayon"] }); also audit uses of Rayon types/traits in code (functions/modules
that reference rayon) and wrap those with cfg(feature = "std") so compilation
succeeds for no_std builds.

In `@key-wallet-manager/src/wallet_manager/matching.rs`:
- Around line 122-124: The test creates Block::dummy instances (block_2,
block_3) at height 100 but constructs FilterMatchKey with mismatched heights
(FilterMatchKey::new(200, ...) and FilterMatchKey::new(300, ...)); update either
the Block::dummy calls to use heights 200 and 300 respectively or change the
FilterMatchKey::new calls to use 100 so the block heights and the FilterMatchKey
metadata (seen in FilterMatchOutput sorting) are consistent — locate
block_2/block_3 and their corresponding FilterMatchKey::new invocations to apply
the change.

In `@key-wallet-manager/tests/check_compact_filters_tests.rs`:
- Around line 34-35: The tests create Regtest dummy addresses but the wallet
under test uses Network::Testnet; update the Address::dummy calls to use
Network::Testnet so addresses match the wallet network (e.g., change
Address::dummy(Network::Regtest, 100) / Address::dummy(Network::Regtest, 101)
and the similar instances at the other locations to
Address::dummy(Network::Testnet, ...)); ensure the variables (unrelated_1,
unrelated_2 and the other dummy address variables around lines 65 and 75) are
constructed with Network::Testnet to keep network consistency.
- Around line 13-17: Replace the non-deterministic wallet creation that calls
WalletManager::<ManagedWalletInfo>::create_wallet_with_random_mnemonic with a
deterministic creation using create_wallet_from_mnemonic (or a deterministic
helper) passing a fixed test mnemonic like the standard test vector; update the
five test cases that currently use create_wallet_with_random_mnemonic (and
WalletAccountCreationOptions::Default) to call
WalletManager::create_wallet_from_mnemonic with the fixed mnemonic and the same
options so tests are reproducible and stable in CI.
🧹 Nitpick comments (2)
key-wallet-manager/src/wallet_manager/process_block.rs (1)

86-88: Consider running the rayon work in a blocking task.

This async method performs CPU-bound rayon matching synchronously, which can block the async executor thread. A spawn_blocking wrapper would keep the runtime responsive.

♻️ Suggested change
 async fn check_compact_filters(&self, input: FilterMatchInput) -> FilterMatchOutput {
-    check_compact_filters_for_addresses(input, self.monitored_addresses())
+    let addresses = self.monitored_addresses();
+    match tokio::task::spawn_blocking(move || {
+        check_compact_filters_for_addresses(input, addresses)
+    })
+    .await
+    {
+        Ok(output) => output,
+        Err(_) => FilterMatchOutput::default(),
+    }
 }
key-wallet-manager/src/wallet_manager/matching.rs (1)

32-49: Optional fast-path for empty inputs

You can skip allocation and Rayon scheduling when input or addresses is empty.

♻️ Proposed tweak
 pub fn check_compact_filters_for_addresses(
     input: FilterMatchInput,
     addresses: Vec<Address>,
 ) -> FilterMatchOutput {
+    if input.is_empty() || addresses.is_empty() {
+        return FilterMatchOutput::new();
+    }
     let script_pubkey_bytes: Vec<Vec<u8>> =
         addresses.iter().map(|address| address.script_pubkey().to_bytes()).collect();

Copy link
Collaborator

@ZocoLini ZocoLini left a comment

Choose a reason for hiding this comment

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

About the performance improvement, since it is cause by parallelizing a collection processing, would be more useful to comment what is the performance improvement per core used than throwing a number without saying the number of cores

pub async fn check_filters_for_matches<
W: key_wallet_manager::wallet_interface::WalletInterface,
>(
&self,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Whats the idea of this function?? Dont see any usage of it and having a method function that wraps other function without using the self field feels pointless

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Dropped.


async fn check_compact_filters(&self, input: FilterMatchInput) -> FilterMatchOutput {
check_compact_filters_for_addresses(input, self.monitored_addresses())
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

why dont you include the matching.rs module logic inside this method?? That way we can also take rid of the unit tests you wrote in matching.rs since they are testing the same logic the new integrations tests are testing

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Need to match for specific addresses too so moving the logic in here wouldnt work. But i dropped this and added monitored_addresses to the wallet interface instead so we can use the standalone function where we need to match filters.

}

async fn check_compact_filters(&self, input: FilterMatchInput) -> FilterMatchOutput {
check_compact_filters_for_addresses(input, self.monitored_addresses())
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since this is not used yet, can you mark it with a TODO or something to make it explicit and what is this method waiting for??

height: CoreBlockHeight,
hash: BlockHash,
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

I cannot give suggestions about this struct since I don't know how is it gonna be used

filter
.match_any(key.hash(), script_pubkey_bytes.iter().map(|v| v.as_slice()))
.unwrap_or(false)
.then_some(key)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure about what I am about to say but would like to comment it anyway just in case. I think this method can only return result if the internal Reader fails to read, but since everything is in memory I expect to always succeed. I feel like we can be missing possible bugs by silently unwrapping to false since thats the only reason a Err would be returned. At the same time I feel like it is impossible to have a bug that makes this return an Err making all this comment irrelevant

@github-actions github-actions bot added the merge-conflict The PR conflicts with the target branch. label Jan 23, 2026
@github-actions
Copy link

This PR has merge conflicts with the base branch. Please rebase or merge the base branch into your branch to resolve them.

@xdustinface xdustinface force-pushed the feat/parallel-filter-matching branch from b7b44b3 to 564af04 Compare January 26, 2026 14:30
@github-actions github-actions bot removed the merge-conflict The PR conflicts with the target branch. label Jan 26, 2026
@xdustinface xdustinface requested a review from ZocoLini January 26, 2026 14:33
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
key-wallet-manager/src/wallet_interface.rs (1)

62-70: Docs contradict the return type in earliest_required_height.

The documentation states the default returns None and references a "specified network," but the method signature returns CoreBlockHeight with no network parameter and defaults to 0. Update the documentation to match the actual implementation:

  • Remove the reference to "specified network" (no such parameter exists)
  • Clarify that the default returns 0 as a sentinel value, not None
  • Document that callers should handle 0 as "unknown" height
✍️ Suggested doc fix
-    /// Return the earliest block height that should be scanned for this wallet on the
-    /// specified network. Implementations can use the wallet's birth height or other
+    /// Return the earliest block height that should be scanned for this wallet.
+    /// Implementations can use the wallet's birth height or other
     /// metadata to provide a more precise rescan starting point.
     ///
-    /// The default implementation returns `None`, which signals that the caller should
-    /// fall back to its existing behaviour.
+    /// The default implementation returns `0` as a sentinel for "unknown"; callers
+    /// should fall back to their existing behaviour in that case.
     async fn earliest_required_height(&self) -> CoreBlockHeight {
         0
     }
♻️ Duplicate comments (2)
key-wallet-manager/Cargo.toml (1)

28-28: Gate rayon behind the std feature if no_std is supported.

If this crate still advertises no_std builds (e.g., #![cfg_attr(not(feature="std"), no_std)]), an unconditional rayon dependency will fail compilation because rayon is std-only. Either gate rayon behind the std feature or drop no_std support explicitly.

💡 Suggested Cargo.toml adjustment
 [features]
-default = ["std", "bincode"]
-std = ["key-wallet/std", "dashcore/std", "dashcore_hashes/std", "secp256k1/std"]
+default = ["std", "bincode"]
+std = ["key-wallet/std", "dashcore/std", "dashcore_hashes/std", "secp256k1/std", "rayon"]

 [dependencies]
-rayon = "1.11"
+rayon = { version = "1.11", optional = true }
#!/bin/bash
# Verify whether key-wallet-manager still declares no_std and relies on std feature gating
rg -n "cfg_attr\\(not\\(feature *= *\"std\"\\), *no_std\\)|no_std" --type rust
rg -n "\\[features\\]"
key-wallet-manager/src/wallet_manager/matching.rs (1)

37-43: Avoid swallowing match_any errors.

Line 41–42 uses unwrap_or(false), which turns filter errors into “no match” and can silently skip relevant blocks. Consider returning a Result (or at least surfacing/logging errors and handling them conservatively) so failures are visible.

#!/bin/bash
# Inspect BlockFilter::match_any error semantics to decide how to surface failures
rg -n "fn match_any" --type rust
rg -n "bip158|BlockFilter" --type rust
🧹 Nitpick comments (1)
key-wallet-manager/src/wallet_interface.rs (1)

49-50: Clarify coverage and cost expectations for monitored_addresses.

This list drives filter matching; the “incoming” wording is ambiguous and may cause implementers to omit internal/change addresses or recompute heavily. Consider tightening the doc to avoid mis-implementations.

✍️ Doc tweak
-    /// Get all addresses the wallet is monitoring for incoming transactions
+    /// Get all wallet addresses to monitor (external + internal/change outputs).
+    /// Implementations should avoid expensive recomputation (prefer cached lists).
     fn monitored_addresses(&self) -> Vec<Address>;

@xdustinface xdustinface force-pushed the feat/parallel-filter-matching branch from 564af04 to cc6937d Compare January 26, 2026 14:55
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
key-wallet-manager/src/wallet_interface.rs (1)

62-68: Doc/behavior mismatch for earliest_required_height.

Lines 62–67 say the default returns None, but the signature returns CoreBlockHeight and the default implementation returns 0. Update the docs to reflect 0 (genesis) or change the signature to Option<CoreBlockHeight> if None is intended.

Doc-only fix
-    /// The default implementation returns `None`, which signals that the caller should
-    /// fall back to its existing behaviour.
+    /// The default implementation returns `0` (genesis), which signals that the caller should
+    /// fall back to its existing behaviour if a more precise height is unavailable.
♻️ Duplicate comments (2)
key-wallet-manager/Cargo.toml (1)

28-28: Gate rayon behind the std feature (or drop no_std).

Line 28 adds a std-only dependency. If key-wallet-manager still advertises no_std, this will break no_std builds. Make rayon optional and tie it to the std feature (and gate matching.rs accordingly), or explicitly remove no_std support. As per coding guidelines, use conditional compilation for optional features.

Proposed Cargo.toml update
 [features]
-default = ["std", "bincode"]
-std = ["key-wallet/std", "dashcore/std", "dashcore_hashes/std", "secp256k1/std"]
+default = ["std", "bincode"]
+std = ["key-wallet/std", "dashcore/std", "dashcore_hashes/std", "secp256k1/std", "rayon"]

 [dependencies]
- ray on = "1.11"
+rayon = { version = "1.11", optional = true }
key-wallet-manager/src/wallet_manager/matching.rs (1)

37-43: Don’t silently treat match_any errors as “no match”.

At Line 41–42, unwrap_or(false) hides filter read/parse errors. A corrupted filter would be treated as non-matching and could suppress relevant block downloads. Consider surfacing errors (e.g., return Result and let callers resync/request the block) or at least log and treat errors as matches to avoid false negatives. As per coding guidelines, propagate errors appropriately.

/// Check compact filters for addresses and return the keys that matched.
pub fn check_compact_filters_for_addresses(
input: &HashMap<FilterMatchKey, BlockFilter>,
addresses: Vec<Address>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

since we are only using the hash inside FilterMatchKey it feels overkill to have a new struct for that. I also see kinda excesive a HashMap when we are only iterating over the entries, isn't possible to do:

pub fn check_compact_filters_for_addresses(
    input: &[BlockFilter],
    addresses: &[Address],
) -> BTreeSet<&BlockFilter> {
    let script_pubkey_bytes: Vec<Vec<u8>> =
        addresses.iter().map(|address| address.script_pubkey().to_bytes()).collect();

    input
        .into_par_iter()
        .filter_map(|(filter)| {
            filter
                .match_any(filter.block_hash, script_pubkey_bytes.iter().map(|v| v.as_slice()))
                .unwrap_or(false)
                .then_some(key.clone())
        })
        .collect()
}

And if we need the height at some point after this method call, we can query the storage using the filter_block_hash.

About the returned collections, since idk the usage of it I cant tell if a Vec<&BlockFilter> can do the job

Copy link
Collaborator Author

@xdustinface xdustinface Jan 26, 2026

Choose a reason for hiding this comment

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

It's done like this to get sorted filter match outputs to request/process blocks in height order. Sure there might be other ways to do this but thats how its handled now in the sync rewrite. You can later come up with something else if you need to.

@xdustinface xdustinface requested a review from ZocoLini January 26, 2026 16:53
@xdustinface xdustinface merged commit 0530c9b into v0.42-dev Jan 26, 2026
138 of 143 checks passed
@xdustinface xdustinface deleted the feat/parallel-filter-matching branch January 26, 2026 16:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants