Skip to content

Commit de5eef0

Browse files
Merge branch 'fix/sweep-only-added-inputs' of https://github.com/dashpay/rust-dashcore into fix/sweep-only-added-inputs
2 parents 67ada22 + 1dc0ec9 commit de5eef0

9 files changed

Lines changed: 115 additions & 238 deletions

File tree

‎dash-spv/src/sync/filters/manager.rs‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -970,11 +970,7 @@ impl<H: BlockHeaderStorage, FH: FilterHeaderStorage, F: FilterStorage, W: Wallet
970970
let mut wallet_states: Vec<WalletScanState> = Vec::new();
971971
for wallet_id in &behind {
972972
let synced = wallet.wallet_synced_height(wallet_id);
973-
// The scan query, not the full monitored set: spent single-use
974-
// (CoinJoin) addresses are pruned so the per-filter match cost
975-
// stays bounded by active UTXOs + gap lookahead instead of
976-
// growing with every historical mixing round
977-
// (dashpay/rust-dashcore#948).
973+
// The scan query, which equals the monitored set today.
978974
let scripts = wallet.scan_script_pubkeys_for(wallet_id);
979975
// Bare owner/voting key hashes a compact filter carries beyond the
980976
// wallet's scriptPubKeys.
@@ -2188,8 +2184,7 @@ mod tests {
21882184

21892185
/// `scan_batch` matches filters against the wallet's scan query
21902186
/// (`scan_script_pubkeys_for`), not the full monitored set: a monitored
2191-
/// script pruned from the scan query — a spent single-use CoinJoin
2192-
/// address (dashpay/rust-dashcore#948) — must not pull its block in.
2187+
/// script the scan query does not carry must not pull its block in.
21932188
#[tokio::test]
21942189
async fn test_scan_batch_uses_pruned_scan_query() {
21952190
let wallet_id: WalletId = [0x03; 32];

‎key-wallet-manager/benches/filter_scan.rs‎

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
1-
//! Compact-filter matching cost: full monitored set vs the pruned
2-
//! forward-scan set for a mixing-heavy CoinJoin wallet
3-
//! (dashpay/rust-dashcore#948).
1+
//! How compact-filter matching cost grows with the size of a mixing-heavy
2+
//! CoinJoin wallet's query.
43
//!
54
//! Mimics a wallet mid-recovery after many mixing rounds. Every CoinJoin
65
//! round pays a fresh single-use address, so the account accumulates `used`
76
//! spent addresses, keeps a small set of still-funded denominations
87
//! ([`LIVE_UTXOS`]), and watches the usual gap-limit lookahead on top. One
98
//! scan batch of BIP158 filters is then matched with
10-
//! `monitored_script_pubkeys_for` (the pre-#948 query, which drags every
11-
//! historical address through SipHash + sort per filter) and with
12-
//! `scan_script_pubkeys_for` (the pruned query, bounded by live UTXOs + gap
13-
//! lookahead).
9+
//! `monitored_script_pubkeys_for`, the query the scan runs.
1410
//!
1511
//! BIP158 keys each filter's SipHashes off the block hash, so the whole
1612
//! query set is re-hashed and re-sorted per filter — which is exactly why
@@ -142,29 +138,13 @@ fn bench_filter_scan(c: &mut Criterion) {
142138
for used in USED_ADDRESSES {
143139
let (manager, wallet_id) = wallet_with_mixing_history(used);
144140
let monitored = manager.monitored_script_pubkeys_for(&wallet_id);
145-
let pruned = manager.scan_script_pubkeys_for(&wallet_id);
146-
assert!(
147-
pruned.len() < monitored.len(),
148-
"the scan query must shrink once CoinJoin addresses are spent"
149-
);
150-
println!(
151-
"used={used}: monitored query = {} scripts, pruned scan query = {} scripts",
152-
monitored.len(),
153-
pruned.len()
154-
);
141+
println!("used={used}: query = {} scripts", monitored.len());
155142

156-
for (name, scripts) in [("monitored", &monitored), ("pruned", &pruned)] {
157-
group.bench_with_input(BenchmarkId::new(name, used), scripts, |b, scripts| {
158-
b.iter(|| {
159-
check_compact_filters_for_elements(
160-
black_box(&filters),
161-
black_box(scripts),
162-
&[],
163-
0,
164-
)
165-
})
166-
});
167-
}
143+
group.bench_with_input(BenchmarkId::new("monitored", used), &monitored, |b, scripts| {
144+
b.iter(|| {
145+
check_compact_filters_for_elements(black_box(&filters), black_box(scripts), &[], 0)
146+
})
147+
});
168148
}
169149

170150
group.finish();

‎key-wallet-manager/src/process_block.rs‎

Lines changed: 87 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -592,19 +592,59 @@ impl<T: WalletInfoInterface + Send + Sync + 'static> WalletManager<T> {
592592
#[cfg(test)]
593593
mod tests {
594594
use super::*;
595+
use crate::matching::{check_compact_filters_for_elements, FilterMatchKey};
595596
use crate::test_helpers::*;
597+
use dashcore::bip158::BlockFilter;
596598
use dashcore::block::{Header, Version};
597599
use dashcore::hashes::Hash;
598600
use dashcore::pow::CompactTarget;
599601
use dashcore::{
600602
BlockHash, Network, OutPoint, ScriptBuf, TxIn, TxMerkleNode, TxOut, Txid, Witness,
601603
};
604+
use key_wallet::account::ManagedAccountTrait as _;
602605
use key_wallet::account::StandardAccountType;
603606
use key_wallet::mnemonic::Language;
604607
use key_wallet::wallet::initialization::WalletAccountCreationOptions;
605608
use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePreference;
606609
use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo;
607610
use key_wallet::{AccountType, Mnemonic};
611+
use std::collections::HashMap;
612+
613+
fn coinjoin_account<'a>(
614+
manager: &'a WalletManager<ManagedWalletInfo>,
615+
wallet_id: &WalletId,
616+
) -> &'a key_wallet::managed_account::ManagedCoreFundsAccount {
617+
manager
618+
.get_wallet_info(wallet_id)
619+
.expect("wallet info")
620+
.accounts
621+
.coinjoin_accounts
622+
.get(&0)
623+
.expect("CoinJoin account 0")
624+
}
625+
626+
fn spend_first_output_of(tx: &Transaction) -> Transaction {
627+
Transaction {
628+
version: 2,
629+
lock_time: 0,
630+
input: vec![TxIn {
631+
previous_output: OutPoint {
632+
txid: tx.txid(),
633+
vout: 0,
634+
},
635+
script_sig: ScriptBuf::new(),
636+
sequence: u32::MAX,
637+
witness: Witness::default(),
638+
}],
639+
output: vec![TxOut {
640+
value: tx.output[0].value,
641+
script_pubkey: ScriptBuf::new_p2pkh(&dashcore::PubkeyHash::from_byte_array(
642+
[0x77; 20],
643+
)),
644+
}],
645+
special_transaction_payload: None,
646+
}
647+
}
608648

609649
fn make_block(txdata: Vec<Transaction>) -> Block {
610650
Block {
@@ -820,32 +860,57 @@ mod tests {
820860
);
821861
}
822862

863+
/// A CoinJoin address used and left with no unspent output was once
864+
/// dropped from the filter scan as unpayable. Mainnet pays such addresses
865+
/// again, and the scan then never matched the block carrying the payment.
823866
#[tokio::test]
824-
async fn test_scan_script_pubkeys_for_prunes_spent_coinjoin_addresses() {
825-
use key_wallet::account::ManagedAccountTrait;
826-
867+
async fn test_filter_scan_matches_a_second_payment_to_an_emptied_coinjoin_address() {
827868
let (mut manager, wallet_id, _addr) = setup_manager_with_wallet();
869+
let wallets = BTreeSet::from([wallet_id]);
870+
871+
let coinjoin_addr = coinjoin_account(&manager, &wallet_id)
872+
.all_addresses()
873+
.first()
874+
.cloned()
875+
.expect("CoinJoin address");
876+
877+
let received = create_tx_paying_to(&coinjoin_addr, 0x11);
878+
let block = Block::dummy(100, vec![received.clone()]);
879+
manager.process_block_for_wallets(&block, block.block_hash(), 100, &wallets).await;
880+
881+
let spend = spend_first_output_of(&received);
882+
let block = Block::dummy(101, vec![spend]);
883+
manager.process_block_for_wallets(&block, block.block_hash(), 101, &wallets).await;
828884

829-
// Untouched wallet: the scan set equals the monitored set.
830-
let monitored = manager.monitored_script_pubkeys_for(&wallet_id);
831-
assert_eq!(manager.scan_script_pubkeys_for(&wallet_id), monitored);
832-
833-
// Mark a CoinJoin address used with no unspent output — a spent
834-
// single-use address. The scan query drops it; the monitored set
835-
// keeps it.
836-
let info = manager.get_wallet_info_mut(&wallet_id).expect("wallet info");
837-
let coinjoin = info.accounts.coinjoin_accounts.get_mut(&0).expect("CoinJoin account 0");
838-
let spent_addr = coinjoin.all_addresses().first().cloned().expect("CoinJoin address");
839-
assert!(coinjoin.mark_address_used(&spent_addr));
840-
841-
let monitored = manager.monitored_script_pubkeys_for(&wallet_id);
842-
let scan = manager.scan_script_pubkeys_for(&wallet_id);
843-
assert!(monitored.contains(&spent_addr.script_pubkey()));
844-
assert!(!scan.contains(&spent_addr.script_pubkey()));
845-
assert_eq!(scan.len(), monitored.len() - 1);
846-
847-
// Unknown wallet id yields an empty scan set.
885+
// Used, and holding nothing: the state that used to drop it.
886+
assert!(coinjoin_account(&manager, &wallet_id).all_addresses().contains(&coinjoin_addr));
887+
assert_eq!(manager.get_wallet_balance(&wallet_id).expect("balance").total(), 0);
888+
assert_eq!(
889+
manager.scan_script_pubkeys_for(&wallet_id),
890+
manager.monitored_script_pubkeys_for(&wallet_id)
891+
);
848892
assert!(manager.scan_script_pubkeys_for(&[0xff; 32]).is_empty());
893+
894+
// The block carrying the second payment must match the scan query.
895+
let later = Block::dummy(102, vec![create_tx_paying_to(&coinjoin_addr, 0x33)]);
896+
let filters = HashMap::from([(
897+
FilterMatchKey::new(102, later.block_hash()),
898+
BlockFilter::dummy(&later),
899+
)]);
900+
let matched = check_compact_filters_for_elements(
901+
&filters,
902+
&manager.scan_script_pubkeys_for(&wallet_id),
903+
&[],
904+
0,
905+
);
906+
assert_eq!(matched.len(), 1, "the scan query must still watch the emptied address");
907+
908+
manager.process_block_for_wallets(&later, later.block_hash(), 102, &wallets).await;
909+
assert_eq!(
910+
manager.get_wallet_balance(&wallet_id).expect("balance").confirmed(),
911+
TX_AMOUNT,
912+
"the second payment must be credited"
913+
);
849914
}
850915

851916
#[tokio::test]

‎key-wallet-manager/src/test_utils/mock_wallet.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,8 +391,8 @@ pub struct MultiMockWallet {
391391
wallets: std::collections::BTreeMap<WalletId, MockWalletState>,
392392
/// Per-wallet override for `scan_script_pubkeys_for`. Wallets absent here
393393
/// fall back to the monitored set, mirroring the trait default. Lets tests
394-
/// hand the filter scan a pruned query while the monitored set stays full
395-
/// (dashpay/rust-dashcore#948).
394+
/// hand the filter scan a query that does not cover everything the wallet
395+
/// watches.
396396
scan_addresses: std::collections::BTreeMap<WalletId, Vec<Address>>,
397397
event_sender: broadcast::Sender<WalletEvent>,
398398
/// Track every block processed for assertions.

‎key-wallet-manager/src/wallet_interface.rs‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,12 @@ pub trait WalletInterface: Send + Sync + 'static {
9191
/// Get the scriptPubKeys `wallet_id` wants matched during a forward
9292
/// compact-filter scan.
9393
///
94-
/// Defaults to [`Self::monitored_script_pubkeys_for`]. Implementations may
95-
/// return a subset when some monitored scripts can no longer be paid in
96-
/// practice — the managed-wallet implementation drops CoinJoin addresses
97-
/// whose outputs are all spent, since those are single-use by protocol and
98-
/// their monotonic growth dominates per-filter matching cost late in a
99-
/// mixing-heavy recovery scan (dashpay/rust-dashcore#948). Block
100-
/// processing still checks transactions against the full monitored set, so
101-
/// pruning only narrows which blocks the filter scan downloads.
94+
/// Defaults to [`Self::monitored_script_pubkeys_for`], and nothing narrows
95+
/// it today — see [`scan_script_pubkeys`]. Block processing always uses
96+
/// the full monitored set, so this only narrows which blocks get
97+
/// downloaded.
98+
///
99+
/// [`scan_script_pubkeys`]: key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface::scan_script_pubkeys
102100
fn scan_script_pubkeys_for(&self, wallet_id: &WalletId) -> Vec<ScriptBuf> {
103101
self.monitored_script_pubkeys_for(wallet_id)
104102
}

‎key-wallet/src/managed_account/managed_core_funds_account.rs‎

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use crate::wallet::balance::WalletCoreBalance;
3030
use crate::{ExtendedPubKey, Network};
3131
use dashcore::blockdata::transaction::OutPoint;
3232
use dashcore::prelude::CoreBlockHeight;
33-
use dashcore::{Address, ScriptBuf, Transaction, Txid};
33+
use dashcore::{Address, Transaction, Txid};
3434
#[cfg(feature = "serde")]
3535
use serde::{Deserialize, Serialize};
3636
use std::collections::BTreeMap;
@@ -206,28 +206,6 @@ impl ManagedCoreFundsAccount {
206206
}
207207
}
208208

209-
/// Cached scriptPubKeys for every address that could still receive or hold
210-
/// funds under a single-use address discipline: addresses not yet used
211-
/// (the gap-limit lookahead, including reserved ones) plus used addresses
212-
/// that still hold at least one unspent output.
213-
///
214-
/// A used address whose outputs are all spent is omitted. That is only
215-
/// sound for account types whose addresses are single-use by protocol
216-
/// (CoinJoin — reuse would link mixing rounds), where nothing ever pays a
217-
/// spent-and-emptied address again; callers must not apply this to
218-
/// account types where address reuse is merely discouraged.
219-
pub fn unspent_or_unused_script_pubkeys(&self) -> Vec<ScriptBuf> {
220-
let funded: HashSet<&ScriptBuf> =
221-
self.utxos.values().map(|utxo| &utxo.txout.script_pubkey).collect();
222-
self.managed_account_type()
223-
.address_pools()
224-
.iter()
225-
.flat_map(|pool| pool.addresses.values())
226-
.filter(|info| !info.is_used() || funded.contains(&info.script_pubkey))
227-
.map(|info| info.script_pubkey.clone())
228-
.collect()
229-
}
230-
231209
/// Add new UTXOs for received outputs, remove spent ones.
232210
///
233211
/// Skips any output whose outpoint is already in `observed_spent` — it is
@@ -1372,6 +1350,7 @@ mod conflict_sweep_walk_tests {
13721350
use crate::transaction_checking::BlockInfo;
13731351
use dashcore::ephemerealdata::instant_lock::InstantLock;
13741352
use dashcore::hashes::Hash;
1353+
use dashcore::ScriptBuf;
13751354
use dashcore::{BlockHash, TxIn, TxOut, Witness};
13761355

13771356
fn outpoint(seed: u32, vout: u32) -> OutPoint {

‎key-wallet/src/tests/mod.rs‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ mod performance_tests;
2828

2929
mod provider_key_derivation_tests;
3030

31-
mod scan_script_pubkeys_tests;
32-
3331
mod special_transaction_matching_tests;
3432

3533
mod special_transaction_tests;

0 commit comments

Comments
 (0)