fix(wallet-switch): cluster B — daemon-switch robustness

From the wallet-switching audit:

- Revert on failure (HIGH): switchToWallet persisted active_wallet_file before
  confirming the new daemon started, so a missing/corrupt wallet wedged across
  restarts. The switch worker now confirms dragonxd survives a grace period; on
  failure it flags the main thread (processWalletSwitchRevert), which restores
  the previous wallet file + re-scopes the vault + re-arms reconnect (settings
  writes stay on the main thread).
- Cross-guard concurrent lifecycle ops (HIGH): switchToWallet now refuses during
  a rescan/repair (state_.sync.rescanning) or seed migration; rescan/repair now
  refuse while daemon_restarting_; and restartDaemonAfterEncryption now sets
  daemon_restarting_ (which also fixes a latent reconnect-to-a-stopped-daemon
  race during the encryption restart). So the switch/adopt/restart/rescan/repair/
  encryption ops are mutually exclusive.
- Quit during switch (MED): beginShutdown now joins the "Switch wallet" task (like
  the adopt task) so quitting can't orphan a freshly-started dragonxd.
- Reset the daemon crash count on switch (LOW) so a prior crash-wedge can't block
  reconnecting to the new wallet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-15 12:37:11 -05:00
parent f546d3e2b1
commit fc509a9340
4 changed files with 76 additions and 5 deletions

View File

@@ -1029,6 +1029,16 @@ void App::switchToWallet(const std::string& walletFile)
ui::Notifications::instance().warning("The node is busy restarting — try again in a moment.");
return;
}
// Don't race the other daemon-lifecycle operations. rescan/repair set state_.sync.rescanning; the
// seed-migration flow drives its own daemon stop/start; an encryption restart sets daemon_restarting_.
if (state_.sync.rescanning) {
ui::Notifications::instance().warning("A rescan or repair is in progress — try again once it finishes.");
return;
}
if (show_seed_migration_) {
ui::Notifications::instance().warning("Finish or cancel the seed migration before switching wallets.");
return;
}
if (!isUsingEmbeddedDaemon()) {
ui::Notifications::instance().warning("Switching wallets needs the embedded daemon — stop any external dragonxd first.");
return;
@@ -1043,8 +1053,13 @@ void App::switchToWallet(const std::string& walletFile)
bool needRescan = true;
if (const auto* e = wallet_index_.find(walletFile)) needRescan = !e->syncedHere;
const std::string prevWallet = settings_->getActiveWalletFile(); // for revert if the new one fails
wallet_switch_prev_file_ = prevWallet;
wallet_switch_failed_.store(false);
settings_->setActiveWalletFile(walletFile);
settings_->save();
// A prior crash-wedge shouldn't block reconnecting to the freshly-launched wallet daemon.
if (daemon_controller_) daemon_controller_->resetCrashCount();
// Re-scope the PIN vault to the new wallet (its own vault-<scope>.dat), and clear any carried-over
// lock-screen attempt/lockout state + wipe the passphrase/PIN entry buffers — the previous wallet's
@@ -1076,11 +1091,39 @@ void App::switchToWallet(const std::string& walletFile)
for (int i = 0; i < 60 && isEmbeddedDaemonRunning(); ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(500));
if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true);
if (!shutting_down_) startEmbeddedDaemon();
daemon_restarting_ = false; // re-arm reconnect once the new daemon has been launched
bool ok = shutting_down_ ? true : startEmbeddedDaemon();
// A missing/corrupt wallet makes dragonxd exit during init — confirm the process survived a
// moment; if not, the wallet is bad. (A valid launch keeps the process alive while it syncs.)
if (ok && !shutting_down_) {
std::this_thread::sleep_for(std::chrono::milliseconds(1500));
if (!isEmbeddedDaemonRunning()) ok = false;
}
if (ok) {
daemon_restarting_ = false; // re-arm reconnect once the new daemon is up
} else {
// Leave the reconnect GATED (daemon_restarting_ stays true) and hand the revert to the main
// thread — settings writes + vault re-scope must not run on this worker thread.
wallet_switch_failed_.store(true);
}
});
}
// Main-thread continuation for a failed wallet switch: restore the previous wallet file so a broken
// active_wallet_file isn't persisted, re-scope the vault back, and re-arm the reconnect so the connect
// loop brings the (good) previous wallet's daemon back up. Called each tick from update().
void App::processWalletSwitchRevert()
{
if (!wallet_switch_failed_.exchange(false)) return;
if (settings_ && !wallet_switch_prev_file_.empty() &&
settings_->getActiveWalletFile() != wallet_switch_prev_file_) {
settings_->setActiveWalletFile(wallet_switch_prev_file_);
settings_->save();
if (vault_) vault_->setWalletScope(wallet_switch_prev_file_);
}
ui::Notifications::instance().error("Couldn't open that wallet — reverted to the previous one.");
daemon_restarting_ = false; // the connect loop now restarts the previous wallet's daemon
}
void App::wipePendingTransactionHistoryCachePassphrase()
{
if (!pending_transaction_history_cache_passphrase_.empty()) {