implement dust detection for sp wallets on received utxos at a higher default limit
What changed, and why it matters
This commit changes how Sparrow Wallet flags tiny, unwanted incoming bitcoin payments ('dust attack' UTXOs). For single-signature software wallets (PolicyType.SINGLE_SP), it now uses a higher default threshold (5000 satoshis instead of 1000) and no longer requires the same address to appear more than once before marking a tiny payment as suspicious. For other wallet types, the old logic remains unchanged. The change is defensive: it makes dust attacks more visible to affected users, but it is not a fix for a vulnerability that lets an attacker steal funds.
Treat as a routine defensive UX improvement, not an emergency security patch. Users relying on dust-attack warnings for SINGLE_SP wallets should verify the new threshold behaves as expected and does not produce excessive false positives. Review the accompanying drongo submodule update to confirm no unintended side effects.
Security signals we found
Behavioral change in dust-attack detection logic
Higher default threshold for single-signature software wallets
Removal of duplicate-address requirement for SINGLE_SP dust flagging
New configurable threshold field persisted in application config
Evidence from the diff
The patch adds a second configuration field, dustAttackThresholdSp, and a constant DUST_ATTACK_THRESHOLD_SP_SATS set to 5000. In WalletUtxosEntry.calculateDust(), when the wallet policy type is SINGLE_SP, it marks a UTXO as a dust attack if its value is <= the new threshold and not all transaction inputs come from the same wallet. For non-SINGLE_SP wallets it keeps the original 1000-sat threshold and the duplicate-node check. The drongo submodule is also updated, presumably to expose PolicyType.SINGLE_SP.
Changed components
WalletUtxosEntry.calculateDust()Config (dustAttackThresholdSp persistence)drongo submodule / PolicyTypeInspect captured patch +25 / −10
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java
index 0055831..1dbb24c 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java
@@ -18,12 +18,12 @@ import org.slf4j.LoggerFactory;
import java.io.*;
import java.lang.reflect.Type;
import java.util.*;
-import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppServices.ENUMERATE_HW_PERIOD_SECS;
import static com.sparrowwallet.sparrow.net.PagedBatchRequestBuilder.DEFAULT_PAGE_SIZE;
import static com.sparrowwallet.sparrow.net.TcpTransport.DEFAULT_MAX_TIMEOUT;
import static com.sparrowwallet.sparrow.wallet.WalletUtxosEntry.DUST_ATTACK_THRESHOLD_SATS;
+import static com.sparrowwallet.sparrow.wallet.WalletUtxosEntry.DUST_ATTACK_THRESHOLD_SP_SATS;
public class Config {
private static final Logger log = LoggerFactory.getLogger(Config.class);
@@ -64,6 +64,7 @@ public class Config {
private List<File> recentWalletFiles;
private Integer keyDerivationPeriod;
private long dustAttackThreshold = DUST_ATTACK_THRESHOLD_SATS;
+ private long dustAttackThresholdSp = DUST_ATTACK_THRESHOLD_SP_SATS;
private int enumerateHwPeriod = ENUMERATE_HW_PERIOD_SECS;
private QRDensity qrDensity;
private QREncoding qrEncoding;
@@ -448,6 +449,10 @@ public class Config {
return dustAttackThreshold;
}
+ public long getDustAttackThresholdSp() {
+ return dustAttackThresholdSp;
+ }
+
public int getEnumerateHwPeriod() {
return enumerateHwPeriod;
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java
index 10d1166..9ca2610 100644
--- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java
+++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.wallet;
+import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.io.Config;
@@ -9,6 +10,7 @@ import java.util.stream.Collectors;
public class WalletUtxosEntry extends Entry {
public static final int DUST_ATTACK_THRESHOLD_SATS = 1000;
+ public static final int DUST_ATTACK_THRESHOLD_SP_SATS = 5000;
public WalletUtxosEntry(Wallet wallet) {
super(wallet, wallet.getName(), wallet.getWalletUtxos().entrySet().stream().map(entry -> new UtxoEntry(entry.getValue().getWallet(), entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList()));
@@ -50,14 +52,22 @@ public class WalletUtxosEntry extends Entry {
}
protected void calculateDust() {
- long dustAttackThreshold = Config.get().getDustAttackThreshold();
- Set<WalletNode> duplicateNodes = getWallet().getWalletTxos().values().stream()
- .collect(Collectors.groupingBy(e -> e, Collectors.counting()))
- .entrySet().stream().filter(e -> e.getValue() > 1).map(Map.Entry::getKey).collect(Collectors.toSet());
-
- for(Entry entry : getChildren()) {
- UtxoEntry utxoEntry = (UtxoEntry) entry;
- utxoEntry.setDustAttack(utxoEntry.getValue() <= dustAttackThreshold && duplicateNodes.contains(utxoEntry.getNode()) && !utxoEntry.getWallet().allInputsFromWallet(utxoEntry.getHashIndex().getHash()));
+ if(getWallet().getPolicyType() == PolicyType.SINGLE_SP) {
+ long dustAttackThreshold = Config.get().getDustAttackThresholdSp();
+ for(Entry entry : getChildren()) {
+ UtxoEntry utxoEntry = (UtxoEntry) entry;
+ utxoEntry.setDustAttack(utxoEntry.getValue() <= dustAttackThreshold && !utxoEntry.getWallet().allInputsFromWallet(utxoEntry.getHashIndex().getHash()));
+ }
+ } else {
+ long dustAttackThreshold = Config.get().getDustAttackThreshold();
+ Set<WalletNode> duplicateNodes = getWallet().getWalletTxos().values().stream()
+ .collect(Collectors.groupingBy(e -> e, Collectors.counting()))
+ .entrySet().stream().filter(e -> e.getValue() > 1).map(Map.Entry::getKey).collect(Collectors.toSet());
+
+ for(Entry entry : getChildren()) {
+ UtxoEntry utxoEntry = (UtxoEntry) entry;
+ utxoEntry.setDustAttack(utxoEntry.getValue() <= dustAttackThreshold && duplicateNodes.contains(utxoEntry.getNode()) && !utxoEntry.getWallet().allInputsFromWallet(utxoEntry.getHashIndex().getHash()));
+ }
}
}
Why this scored 30/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.