fetch all nodes on a silent payments refresh when a reorg has invalidated any of them
What changed, and why it matters
This commit fixes a bug in Sparrow Wallet's handling of silent payments after a blockchain reorganization (reorg). When the blockchain briefly rewinds and then re-adds a transaction at the same height, the wallet might not refresh all of its payment addresses, potentially leaving some balances or transaction statuses outdated. The fix forces a full refresh of all wallet nodes whenever any address was previously invalidated by a reorg, ensuring no stale data is missed.
Users relying on silent payments should upgrade to a release containing this commit, especially if operating in environments where blockchain reorganizations are possible. Wallet developers should review whether similar subset-refresh optimizations elsewhere could miss reorg-invalidated state.
Security signals we found
silent payments refresh may skip reorg-invalidated nodes
blockchain reorg can leave wallet state stale
transaction re-included at same height not reported as affected
reorg exemption only cleared by full node fetch
fix widens refresh scope to all nodes after reorg
Evidence from the diff
The patch adds a helper method hasReorgInvalidatedScriptHashes(Wallet) that checks whether any wallet node (including nested child wallets) still exists in the reorgInvalidatedScriptHashes set. In fetchAndCalculateHistory, the silent-payments refresh logic now widens nodesToFetch to null (meaning all nodes) when either the wallet is newly acquired or any of its nodes were invalidated by a reorg. Previously, only newly acquired wallets triggered a full fetch; live refreshes used only affectedNodes. After a reorg, a transaction re-included at the same height is already known to the wallet, so the batch does not report it as affected, and the only way to clear the reorg exemption and revisit those nodes is to fetch every node.
Changed components
src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.javasilent payments refresh logicwallet history fetchingreorg invalidation trackingInspect captured patch +23 / −1
### src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java
@@ -435,6 +435,26 @@ private static boolean invalidateWalletScriptHashesForReorg(Wallet wallet, int f
return invalidated;
}
+ /**
+ * Returns whether any node of the given wallet, or of a nested child wallet, is still holding the exemption a reorg invalidation gave it. Only a
+ * fetch of every node revisits those nodes and clears the exemption, so a refresh that would otherwise fetch a subset must be widened to one.
+ */
+ private static boolean hasReorgInvalidatedScriptHashes(Wallet wallet) {
+ if(reorgInvalidatedScriptHashes.isEmpty()) {
+ return false;
+ }
+
+ List<Wallet> wallets = new ArrayList<>();
+ wallets.add(wallet);
+ for(Wallet childWallet : new ArrayList<>(wallet.getChildWallets())) {
+ if(childWallet.isNested()) {
+ wallets.add(childWallet);
+ }
+ }
+
+ return wallets.stream().flatMap(w -> w.getWalletNodes().keySet().stream()).map(ElectrumServer::getScriptHash).anyMatch(reorgInvalidatedScriptHashes::contains);
+ }
+
public boolean fetchAndCalculateHistory(Wallet mainWallet, List<Wallet> filterToWallets, Set<WalletNode> filterToNodes) throws ServerException {
boolean historyFetched = fetchAndCalculateWalletHistory(mainWallet, filterToWallets, filterToNodes);
for(Wallet childWallet : new ArrayList<>(mainWallet.getChildWallets())) {
@@ -3529,7 +3549,9 @@ protected Boolean call() throws ServerException {
//First refresh (acquired): fetch all nodes to re-subscribe scripthashes the server forgot.
//Live delta: only the affected ones (newly-discovered SP nodes + nodes spent by the batch).
- Set<WalletNode> nodesToFetch = acquired ? null : affectedNodes;
+ //A reorg needs all nodes as well: a transaction re-included at the same height is already in the wallet, so the batch reports
+ //nothing affected, while the nodes proving it against the discarded block are revisited only by a fetch of every node.
+ Set<WalletNode> nodesToFetch = acquired || hasReorgInvalidatedScriptHashes(wallet) ? null : affectedNodes;
if(nodesToFetch != null && nodesToFetch.isEmpty()) {
return true;
}Why this scored 59/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.