fix sp self-spend output discovery by always scanning all batch txs
What changed, and why it matters
This commit fixes a bug in Sparrow Wallet's silent payments feature. Previously, when scanning for incoming silent payments, the wallet only looked at transactions it had not already seen. This meant it could miss a special kind of transaction: one where the user sends bitcoin to themselves (a 'self-spend'). Because the wallet already knew about the transaction, it skipped re-scanning it for silent payment outputs, potentially causing the wallet to not display or account for those funds. The fix makes the wallet scan all relevant transactions, whether already known or not.
Users relying on silent payments should upgrade to a version containing this commit and consider rescanning their wallet to ensure any missed self-spend silent payment outputs are discovered. Developers should review whether other scanning paths have similar 'skip already-known transactions' optimizations that could omit outputs.
Security signals we found
Functional bug in cryptocurrency wallet output discovery
Silent payment self-spend outputs could remain undiscovered
Potential wallet balance/UTXO accounting discrepancy
No explicit security framing in commit message
Evidence from the diff
In ElectrumServer.java, the silent payment scanning logic previously built a references map only of transactions not already present in the wallet (wallet.getWalletTransaction(txid) == null). If all referenced transactions were already known, it returned early with an empty set, never scanning them for silent payment outputs. The patch splits this into transactionMap (already-known txs) and referencesToFetch (unknown txs), fetches headers and full transactions only for the unknown set, and then scans the combined set. This ensures self-spend outputs using silent payments are discovered even when the spending transaction is already in the wallet.
Changed components
src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.javaSilent Payments scanning logicWallet transaction discoveryInspect captured patch +11 / −8
diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java
index f1af4ef..bbf698e 100644
--- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java
+++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java
@@ -1501,21 +1501,24 @@ public class ElectrumServer {
return Collections.emptySet();
}
- Map<BlockTransactionHash, Transaction> references = new TreeMap<>();
+ Map<BlockTransactionHash, Transaction> referencesToFetch = new TreeMap<>();
+ Map<Sha256Hash, BlockTransaction> transactionMap = new HashMap<>();
Map<Sha256Hash, byte[]> tweakMap = new HashMap<>();
for(SilentPaymentsTx entry : entries) {
Sha256Hash txid = Sha256Hash.wrap(entry.tx_hash);
tweakMap.putIfAbsent(txid, Utils.hexToBytes(entry.tweak_key));
- if(wallet.getWalletTransaction(txid) == null) {
- references.put(new BlockTransaction(txid, entry.height, null, null, null), null);
+ BlockTransaction existing = wallet.getWalletTransaction(txid);
+ if(existing != null) {
+ transactionMap.put(txid, existing);
+ } else {
+ referencesToFetch.put(new BlockTransaction(txid, entry.height, null, null, null), null);
}
}
- if(references.isEmpty()) {
- return Collections.emptySet();
- }
- Map<Integer, BlockHeader> blockHeaderMap = getBlockHeaders(wallet, references.keySet());
- Map<Sha256Hash, BlockTransaction> transactionMap = getTransactions(wallet, references, blockHeaderMap);
+ if(!referencesToFetch.isEmpty()) {
+ Map<Integer, BlockHeader> blockHeaderMap = getBlockHeaders(wallet, referencesToFetch.keySet());
+ transactionMap.putAll(getTransactions(wallet, referencesToFetch, blockHeaderMap));
+ }
ECKey scanPriv = wallet.getSilentPaymentScanAddress().getScanKey();
ECKey spendPub = wallet.getSilentPaymentScanAddress().getSpendKey();
Why this scored 51/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.