do not advance the stored block height of a silent payments wallet until a scan has covered it
What changed, and why it matters
This commit fixes a bookkeeping bug in Sparrow Wallet for silent-payments wallets. Previously, the wallet could record that it had scanned up to a certain block height before the scan actually finished. If the app then restarted, it might skip scanning that block range and miss payments sent to it. The change makes the wallet wait until a scan has actually covered a block before advancing its stored starting point for the next scan.
Users relying on silent-payments wallets should upgrade to a release containing this commit. After upgrading, affected wallets may need a rescan from the birth height to ensure no silent-payment outputs were missed. Developers should review whether other wallet types or scan paths have similar cursor-advance race conditions.
Security signals we found
silent-payments scan cursor race condition
stored block height advanced before scan coverage
potential missed incoming payments after restart
wallet state inconsistency between persisted height and actual scanned range
Evidence from the diff
In WalletForm.updateWallet(), the stored block height was unconditionally set to the supplied blockHeight whenever non-null. For silent-payments (SINGLE_SP) wallets, storedBlockHeight is used as the starting height for the next scan. The patch introduces a ‘scanned’ guard: for silent-payments wallets, the height is only advanced when spSubscriptionHeld is true and no scan is in progress. This prevents the persisted scan cursor from racing ahead of actual scanning, which could cause missed silent-payment outputs after a restart or resync.
Changed components
src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.javasilent-payments wallet scanning logicstoredBlockHeight persistenceInspect captured patch +7 / −3
### src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java
@@ -317,11 +317,15 @@ private List<WalletNode> updateWallet(Integer blockHeight, Wallet currentWallet,
currentWallet.setBirthHeight(min.getAsInt());
}
- if(blockHeight != null) {
- currentWallet.setStoredBlockHeight(blockHeight);
+ //The stored block height is where a silent payments wallet begins its next scan, so it can only follow the chain once a scan has covered it
+ boolean scanned = wallet.getPolicyType() != PolicyType.SINGLE_SP || (spSubscriptionHeld && !spScanInProgress);
+ Integer scannedBlockHeight = scanned ? blockHeight : null;
+
+ if(scannedBlockHeight != null) {
+ currentWallet.setStoredBlockHeight(scannedBlockHeight);
}
- return notifyIfChanged(blockHeight, currentWallet, previousWallet, nestedHistoryChangedNodes);
+ return notifyIfChanged(scannedBlockHeight, currentWallet, previousWallet, nestedHistoryChangedNodes);
}
private List<WalletNode> notifyIfChanged(Integer blockHeight, Wallet currentWallet, Wallet previousWallet, List<WalletNode> nestedHistoryChangedNodes) {Why this scored 41/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.