Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 0 additions & 21 deletions dash-spv/src/sync/legacy/filters/matching.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,6 @@ use crate::network::NetworkManager;
use crate::storage::StorageManager;

impl<S: StorageManager, N: NetworkManager> super::manager::FilterSyncManager<S, N> {
pub async fn check_filter_for_matches<
W: key_wallet_manager::wallet_interface::WalletInterface,
>(
&self,
filter_data: &[u8],
block_hash: &BlockHash,
wallet: &mut W,
) -> SyncResult<bool> {
// Create the BlockFilter from the raw data
let filter = dashcore::bip158::BlockFilter::new(filter_data);

// Use wallet's check_compact_filter method
let matches = wallet.check_compact_filter(&filter, block_hash).await;
if matches {
tracing::info!("🎯 Filter match found for block {}", block_hash);
Ok(true)
} else {
Ok(false)
}
}

/// Check if filter matches any of the provided scripts using BIP158 GCS filter.
#[allow(dead_code)]
fn filter_matches_scripts(
Expand Down
26 changes: 11 additions & 15 deletions dash-spv/src/sync/legacy/message_handlers.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
//! Message handlers for synchronization phases.

use std::ops::DerefMut;
use std::time::Instant;

use dashcore::bip158::BlockFilter;
use dashcore::block::Block;
use dashcore::network::message::NetworkMessage;
use dashcore::network::message_blockdata::Inventory;
use std::collections::HashMap;
use std::time::Instant;

use super::manager::SyncManager;
use super::phases::SyncPhase;
use crate::error::{SyncError, SyncResult};
use crate::network::NetworkManager;
use crate::storage::StorageManager;
use key_wallet_manager::wallet_interface::WalletInterface;

use super::manager::SyncManager;
use super::phases::SyncPhase;
use key_wallet_manager::wallet_manager::{check_compact_filters_for_addresses, FilterMatchKey};

impl<S: StorageManager, N: NetworkManager, W: WalletInterface> SyncManager<S, N, W> {
/// Handle incoming network messages with phase filtering
Expand Down Expand Up @@ -496,8 +496,6 @@ impl<S: StorageManager, N: NetworkManager, W: WalletInterface> SyncManager<S, N,
}
}

let mut wallet = self.wallet.write().await;

// Check filter against wallet if available
// First, verify filter data matches expected filter header chain
let height = storage
Expand Down Expand Up @@ -531,20 +529,18 @@ impl<S: StorageManager, N: NetworkManager, W: WalletInterface> SyncManager<S, N,
.await
.map_err(|e| SyncError::Storage(format!("Failed to store filter: {}", e)))?;

let matches = self
.filter_sync
.check_filter_for_matches(&cfilter.filter, &cfilter.block_hash, wallet.deref_mut())
.await?;

drop(wallet);
let key = FilterMatchKey::new(height, cfilter.block_hash);
let input = HashMap::from([(key, BlockFilter::new(&cfilter.filter))]);
let addresses = self.wallet.read().await.monitored_addresses();
let matches = check_compact_filters_for_addresses(&input, addresses);

{
let mut stats_lock = self.stats.write().await;
stats_lock.filters_received += 1;
stats_lock.last_filter_received_time = Some(std::time::Instant::now());
}

if matches {
if !matches.is_empty() {
// Update filter match statistics
{
let mut stats = self.stats.write().await;
Expand Down
1 change: 1 addition & 0 deletions key-wallet-manager/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ serde = { version = "1.0", default-features = false, features = ["derive"], opti
async-trait = "0.1"
bincode = { version = "2.0.1", optional = true }
zeroize = { version = "1.8", features = ["derive"] }
rayon = "1.11"
tokio = { version = "1.32", features = ["full"] }

[dev-dependencies]
Expand Down
28 changes: 8 additions & 20 deletions key-wallet-manager/src/test_utils/wallet.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
use crate::{wallet_interface::WalletInterface, BlockProcessingResult};
use dashcore::{Address, Block, Transaction, Txid};
use std::{collections::BTreeMap, sync::Arc};

use dashcore::{Block, Transaction, Txid};
use tokio::sync::Mutex;

use crate::{wallet_interface::WalletInterface, BlockProcessingResult};

// Type alias for transaction effects map
type TransactionEffectsMap = Arc<Mutex<BTreeMap<Txid, (i64, Vec<String>)>>>;

Expand Down Expand Up @@ -57,15 +55,6 @@ impl WalletInterface for MockWallet {
processed.push(tx.txid());
}

async fn check_compact_filter(
&mut self,
_filter: &dashcore::bip158::BlockFilter,
_block_hash: &dashcore::BlockHash,
) -> bool {
// Return true for all filters in test
true
}

async fn describe(&self) -> String {
"MockWallet (test implementation)".to_string()
}
Expand All @@ -74,6 +63,10 @@ impl WalletInterface for MockWallet {
let map = self.effects.lock().await;
map.get(&tx.txid()).cloned()
}

fn monitored_addresses(&self) -> Vec<Address> {
Vec::new()
}
}

/// Mock wallet that returns false for filter checks
Expand All @@ -94,13 +87,8 @@ impl WalletInterface for NonMatchingMockWallet {

async fn process_mempool_transaction(&mut self, _tx: &Transaction) {}

async fn check_compact_filter(
&mut self,
_filter: &dashcore::bip158::BlockFilter,
_block_hash: &dashcore::BlockHash,
) -> bool {
// Always return false - filter doesn't match
false
fn monitored_addresses(&self) -> Vec<Address> {
Vec::new()
}

async fn describe(&self) -> String {
Expand Down
10 changes: 2 additions & 8 deletions key-wallet-manager/src/wallet_interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use alloc::string::String;
use alloc::vec::Vec;
use async_trait::async_trait;
use dashcore::bip158::BlockFilter;
use dashcore::prelude::CoreBlockHeight;
use dashcore::{Address, Block, Transaction, Txid};

Expand Down Expand Up @@ -47,13 +46,8 @@ pub trait WalletInterface: Send + Sync + 'static {
/// Called when a transaction is seen in the mempool
async fn process_mempool_transaction(&mut self, tx: &Transaction);

/// Check if a compact filter matches any watched items
/// Returns true if the block should be downloaded
async fn check_compact_filter(
&mut self,
filter: &BlockFilter,
block_hash: &dashcore::BlockHash,
) -> bool;
/// Get all addresses the wallet is monitoring for incoming transactions
fn monitored_addresses(&self) -> Vec<Address>;

/// Return the wallet's per-transaction net change and involved addresses if known.
/// Returns (net_amount, addresses) where net_amount is received - sent in satoshis.
Expand Down
166 changes: 166 additions & 0 deletions key-wallet-manager/src/wallet_manager/matching.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
use alloc::vec::Vec;
use dashcore::bip158::BlockFilter;
use dashcore::prelude::CoreBlockHeight;
use dashcore::{Address, BlockHash};
use rayon::prelude::{IntoParallelIterator, ParallelIterator};
use std::collections::{BTreeSet, HashMap};

#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct FilterMatchKey {
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

impl FilterMatchKey {
pub fn new(height: CoreBlockHeight, hash: BlockHash) -> Self {
Self {
height,
hash,
}
}
pub fn height(&self) -> CoreBlockHeight {
self.height
}
pub fn hash(&self) -> &BlockHash {
&self.hash
}
}

/// 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.

) -> BTreeSet<FilterMatchKey> {
let script_pubkey_bytes: Vec<Vec<u8>> =
addresses.iter().map(|address| address.script_pubkey().to_bytes()).collect();

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

#[cfg(test)]
mod tests {
use super::*;
use crate::Network;
use dashcore::{Block, Transaction};

#[test]
fn test_empty_input_returns_empty() {
let result = check_compact_filters_for_addresses(&HashMap::new(), vec![]);
assert!(result.is_empty());
}

#[test]
fn test_empty_addresses_returns_empty() {
let address = Address::dummy(Network::Regtest, 1);
let tx = Transaction::dummy(&address, 0..0, &[1]);
let block = Block::dummy(100, vec![tx]);
let filter = BlockFilter::dummy(&block);
let key = FilterMatchKey::new(100, block.block_hash());

let mut input = HashMap::new();
input.insert(key.clone(), filter);

let output = check_compact_filters_for_addresses(&input, vec![]);
assert!(!output.contains(&key));
}

#[test]
fn test_matching_filter() {
let address = Address::dummy(Network::Regtest, 1);
let tx = Transaction::dummy(&address, 0..0, &[1]);
let block = Block::dummy(100, vec![tx]);
let filter = BlockFilter::dummy(&block);
let key = FilterMatchKey::new(100, block.block_hash());

let mut input = HashMap::new();
input.insert(key.clone(), filter);

let output = check_compact_filters_for_addresses(&input, vec![address]);
assert!(output.contains(&key));
}

#[test]
fn test_non_matching_filter() {
let address = Address::dummy(Network::Regtest, 1);
let address_other = Address::dummy(Network::Regtest, 2);

let tx = Transaction::dummy(&address_other, 0..0, &[1]);
let block = Block::dummy(100, vec![tx]);
let filter = BlockFilter::dummy(&block);
let key = FilterMatchKey::new(100, block.block_hash());

let mut input = HashMap::new();
input.insert(key.clone(), filter);

let output = check_compact_filters_for_addresses(&input, vec![address]);
assert!(!output.contains(&key));
}

#[test]
fn test_batch_mixed_results() {
let unrelated_address = Address::dummy(Network::Regtest, 0);
let address_1 = Address::dummy(Network::Regtest, 1);
let address_2 = Address::dummy(Network::Regtest, 2);

let tx_1 = Transaction::dummy(&address_1, 0..0, &[1]);
let block_1 = Block::dummy(100, vec![tx_1]);
let filter_1 = BlockFilter::dummy(&block_1);
let key_1 = FilterMatchKey::new(100, block_1.block_hash());

let tx_2 = Transaction::dummy(&address_2, 0..0, &[2]);
let block_2 = Block::dummy(200, vec![tx_2]);
let filter_2 = BlockFilter::dummy(&block_2);
let key_2 = FilterMatchKey::new(200, block_2.block_hash());

let tx_3 = Transaction::dummy(&unrelated_address, 0..0, &[10]);
let block_3 = Block::dummy(300, vec![tx_3]);
let filter_3 = BlockFilter::dummy(&block_3);
let key_3 = FilterMatchKey::new(300, block_3.block_hash());

let mut input = HashMap::new();
input.insert(key_1.clone(), filter_1);
input.insert(key_2.clone(), filter_2);
input.insert(key_3.clone(), filter_3);

let output = check_compact_filters_for_addresses(&input, vec![address_1, address_2]);
assert_eq!(output.len(), 2);
assert!(output.contains(&key_1));
assert!(output.contains(&key_2));
assert!(!output.contains(&key_3));
}

#[test]
fn test_output_sorted_by_height() {
let address = Address::dummy(Network::Regtest, 1);

// Create blocks at different heights (inserted in non-sorted order)
let heights = [500, 100, 300, 200, 400];
let mut input = HashMap::new();

for (i, &height) in heights.iter().enumerate() {
let tx = Transaction::dummy(&address, 0..0, &[i as u64]);
let block = Block::dummy(height, vec![tx]);
let filter = BlockFilter::dummy(&block);
let key = FilterMatchKey::new(height, block.block_hash());
input.insert(key, filter);
}

let output = check_compact_filters_for_addresses(&input, vec![address]);

// Verify output is sorted by height (ascending)
let heights_out: Vec<CoreBlockHeight> = output.iter().map(|k| k.height()).collect();
let mut sorted_heights = heights_out.clone();
sorted_heights.sort();

assert_eq!(heights_out, sorted_heights);
assert_eq!(heights_out, vec![100, 200, 300, 400, 500]);
}
}
2 changes: 2 additions & 0 deletions key-wallet-manager/src/wallet_manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
//! each of which can have multiple accounts. This follows the architecture
//! pattern where a manager oversees multiple distinct wallets.

mod matching;
mod process_block;
mod transaction_building;

pub use crate::wallet_manager::matching::{check_compact_filters_for_addresses, FilterMatchKey};
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
Expand Down
29 changes: 3 additions & 26 deletions key-wallet-manager/src/wallet_manager/process_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ use alloc::string::String;
use alloc::vec::Vec;
use async_trait::async_trait;
use core::fmt::Write as _;
use dashcore::bip158::BlockFilter;
use dashcore::prelude::CoreBlockHeight;
use dashcore::{Block, BlockHash, Transaction};
use dashcore::{Address, Block, Transaction};
use key_wallet::transaction_checking::transaction_router::TransactionRouter;
use key_wallet::transaction_checking::TransactionContext;
use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface;
Expand Down Expand Up @@ -58,30 +57,8 @@ impl<T: WalletInfoInterface + Send + Sync + 'static> WalletInterface for WalletM
.await;
}

async fn check_compact_filter(&mut self, filter: &BlockFilter, block_hash: &BlockHash) -> bool {
// Collect all scripts we're watching
let mut script_bytes = Vec::new();

// Get all wallet addresses for this network
for info in self.wallet_infos.values() {
let monitored = info.monitored_addresses();
for address in monitored {
script_bytes.push(address.script_pubkey().as_bytes().to_vec());
}
}

// If we don't watch any scripts for this network, there can be no match.
// Note: BlockFilterReader::match_any returns true for an empty query set,
// so we must guard this case explicitly to avoid false positives.
let hit = if script_bytes.is_empty() {
false
} else {
filter
.match_any(block_hash, &mut script_bytes.iter().map(|s| s.as_slice()))
.unwrap_or(false)
};

hit
fn monitored_addresses(&self) -> Vec<Address> {
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 transaction_effect(&self, tx: &Transaction) -> Option<(i64, Vec<String>)> {
Expand Down
Loading