report failed bitcoin core descriptor imports instead of only logging them
What changed, and why it matters
This commit changes Sparrow Wallet so that when it fails to import Bitcoin Core wallet descriptors, the user is shown a clear error dialog or terminal message instead of the failure being hidden in logs. Previously, descriptor import failures were only logged, which could leave users unaware that their transaction history or balances might be incomplete. The change improves visibility and reliability but does not introduce or fix a traditional security vulnerability.
No urgent security action required. Treat as a normal reliability/UX improvement. Users running prior versions should be aware that failed Bitcoin Core descriptor imports may have been silently logged, so verifying wallet history completeness against Bitcoin Core is prudent if unexpected balances are seen.
Security signals we found
Failure to surface descriptor import errors could previously leave users with incomplete wallet history without realizing it
New event-driven error reporting reduces silent failure conditions
No input sanitization changes, cryptographic changes, or network changes observed
Evidence from the diff
The patch adds a new event type, CormorantImportStatusEvent, carrying the affected wallets and an aggregated error message. BitcoindClient now detects two failure modes during descriptor import: a mismatch between the number of import requests and Bitcoin Core results, and per-descriptor import failures returned in the results. It deduplicates repeated warnings per descriptor and posts the event on the JavaFX event bus (GUI) or terminal status line. AppController subscribes to the event and displays an error dialog; SparrowTextGui subscribes for terminal mode. The change is defensive UX hardening rather than a code-execution or privilege-escalation fix.
Changed components
Sparrow Wallet GUI (AppController)Sparrow Wallet terminal UI (SparrowTextGui)Cormorant Bitcoin Core client (BitcoindClient)Event system (CormorantImportStatusEvent)Inspect captured patch +86 / −2
### src/main/java/com/sparrowwallet/sparrow/AppController.java
@@ -3156,6 +3156,14 @@ public void cormorantPruneStatus(CormorantPruneStatusEvent event) {
}
}
+ @Subscribe
+ public void cormorantImportStatus(CormorantImportStatusEvent event) {
+ String walletNames = event.getWallets().stream().map(Wallet::getFullDisplayName).collect(Collectors.joining(", "));
+ AppServices.showErrorDialog("Error importing Bitcoin Core descriptors",
+ "Bitcoin Core did not import " + (walletNames.isEmpty() ? "one or more descriptors" : "the descriptors for " + walletNames) + ":\n\n" + event.getErrorMessage() + "\n\n" +
+ "Transactions and balances may be incomplete until the import succeeds.");
+ }
+
@Subscribe
public void bwtBootStatus(BwtBootStatusEvent event) {
serverToggle.setDisable(true);
### src/main/java/com/sparrowwallet/sparrow/event/CormorantImportStatusEvent.java
@@ -0,0 +1,29 @@
+package com.sparrowwallet.sparrow.event;
+
+import com.sparrowwallet.drongo.wallet.Wallet;
+
+import java.util.Set;
+
+public class CormorantImportStatusEvent extends CormorantStatusEvent {
+ private final Set<Wallet> wallets;
+ private final String errorMessage;
+
+ public CormorantImportStatusEvent(String status, Set<Wallet> wallets, String errorMessage) {
+ super(status);
+ this.wallets = wallets;
+ this.errorMessage = errorMessage;
+ }
+
+ @Override
+ public boolean isFor(Wallet wallet) {
+ return wallets.contains(wallet);
+ }
+
+ public Set<Wallet> getWallets() {
+ return wallets;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+}
### src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClient.java
@@ -12,6 +12,7 @@
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
+import com.sparrowwallet.sparrow.event.CormorantImportStatusEvent;
import com.sparrowwallet.sparrow.event.CormorantPruneStatusEvent;
import com.sparrowwallet.sparrow.event.CormorantScanStatusEvent;
import com.sparrowwallet.sparrow.event.CormorantSyncStatusEvent;
@@ -92,6 +93,7 @@ public class BitcoindClient {
private boolean initialImportStarted;
private final List<String> pruneWarnedDescriptors = new ArrayList<>();
+ private final Set<String> importFailedDescriptors = Collections.synchronizedSet(new HashSet<>());
private final Map<Sha256Hash, VsizeFeerate> mempoolEntries = new ConcurrentHashMap<>();
private MempoolEntriesState mempoolEntriesState = MempoolEntriesState.UNINITIALIZED;
@@ -329,7 +331,7 @@ private int getDefaultRange(Wallet wallet, KeyPurpose keyPurpose) {
return wallet.getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && keyPurpose == KeyPurpose.RECEIVE ? POSTMIX_GAP_LIMIT : DEFAULT_GAP_LIMIT;
}
- private void importDescriptors(Map<String, ScanDate> descriptors) throws ScanDateBeforePruneException {
+ private void importDescriptors(Map<String, ScanDate> descriptors) throws ScanDateBeforePruneException, ImportFailedException {
//Sort descriptors in alphanumeric order to avoid deadlocks, particularly with BIP47 wallets
Set<String> sortedDescriptors = new TreeSet<>(descriptors.keySet());
for(String descriptor : sortedDescriptors) {
@@ -354,7 +356,7 @@ private void importDescriptors(Map<String, ScanDate> descriptors) throws ScanDat
}
}
- private Set<String> addDescriptors(Map<String, ScanDate> descriptors) throws ScanDateBeforePruneException {
+ private Set<String> addDescriptors(Map<String, ScanDate> descriptors) throws ScanDateBeforePruneException, ImportFailedException {
boolean forceRescan = descriptors.values().stream().anyMatch(scanDate -> scanDate.forceRescan);
if(!initialized || forceRescan) {
ListDescriptorsResult listDescriptorsResult = getBitcoindService().listDescriptors(false);
@@ -426,15 +428,28 @@ private Set<String> addDescriptors(Map<String, ScanDate> descriptors) throws Sca
scanningDescriptors.clear();
}
+ if(results.size() != importDescriptors.size()) {
+ String error = "Bitcoin Core returned " + results.size() + " results for " + importDescriptors.size() + " imported descriptors";
+ log.error(error);
+ postImportFailure(importingDescriptors.keySet().stream().collect(Collectors.toMap(descriptor -> descriptor, descriptor -> error, (a, b) -> a, LinkedHashMap::new)));
+ throw new ImportFailedException(error);
+ }
+
+ Map<String, String> failedDescriptors = new LinkedHashMap<>();
for(int i = 0; i < importDescriptors.size(); i++) {
ImportDescriptor importDescriptor = importDescriptors.get(i);
ImportDescriptorResult importDescriptorResult = results.get(i);
if(importDescriptorResult.success()) {
importedDescriptors.put(importDescriptor.getDesc(), importingDescriptors.get(importDescriptor.getDesc()));
+ importFailedDescriptors.remove(importDescriptor.getDesc());
} else {
log.error("Error importing descriptor " + importDescriptor.getDesc() + ": " + importDescriptorResult);
+ String error = importDescriptorResult.error() == null ? null : importDescriptorResult.error().getMessage();
+ failedDescriptors.put(importDescriptor.getDesc(), error == null ? "Unknown error" : error);
}
}
+
+ postImportFailure(failedDescriptors);
}
initialized = true;
@@ -444,6 +459,7 @@ private Set<String> addDescriptors(Map<String, ScanDate> descriptors) throws Sca
public void stop() {
timer.cancel();
pruneWarnedDescriptors.clear();
+ importFailedDescriptors.clear();
stopped = true;
}
@@ -768,6 +784,32 @@ private Set<Wallet> getScanningWallets() {
return scanningWallets;
}
+ private void postImportFailure(Map<String, String> failedDescriptors) {
+ //Only warn once for each descriptor, until it is successfully imported again
+ failedDescriptors.keySet().removeIf(descriptor -> !importFailedDescriptors.add(descriptor));
+ if(!failedDescriptors.isEmpty()) {
+ Set<Wallet> failedWallets = getDescriptorWallets(failedDescriptors.keySet());
+ String errorMessage = failedDescriptors.values().stream().distinct().collect(Collectors.joining("\n"));
+ Platform.runLater(() -> EventManager.get().post(new CormorantImportStatusEvent("Error importing descriptors", failedWallets, errorMessage)));
+ }
+ }
+
+ private Set<Wallet> getDescriptorWallets(Collection<String> descriptors) {
+ Set<Wallet> descriptorWallets = new LinkedHashSet<>();
+ for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
+ if(openWallet.isValid()) {
+ for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
+ if(descriptors.contains(OutputDescriptor.normalize(OutputDescriptor.getOutputDescriptor(openWallet, keyPurpose).toString(false, false)))) {
+ descriptorWallets.add(openWallet);
+ break;
+ }
+ }
+ }
+ }
+
+ return descriptorWallets;
+ }
+
private boolean isEmptyBlockchain(BlockchainInfo blockchainInfo) {
return blockchainInfo.blocks() == 0 && blockchainInfo.getProgressPercent() == 100;
}
### src/main/java/com/sparrowwallet/sparrow/terminal/SparrowTextGui.java
@@ -219,4 +219,9 @@ public void cormorantScanStatusEvent(CormorantScanStatusEvent event) {
public void cormorantPruneStatus(CormorantPruneStatusEvent event) {
statusUpdated(new StatusEvent("Error importing wallet, pruned date after wallet birthday"));
}
+
+ @Subscribe
+ public void cormorantImportStatus(CormorantImportStatusEvent event) {
+ statusUpdated(new StatusEvent("Error importing descriptors, wallet history may be incomplete"));
+ }
}Why this scored 20/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.