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

@@ -241,16 +241,19 @@ void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestar
// the daemon is restarting.
connection_status_ = TR("restarting_after_encryption");
}
// Gate the main-loop reconnect while the daemon is down (so tryConnect can't hit the stopped
// node or start a duplicate) and block a concurrent wallet switch/rescan, which check this flag.
daemon_restarting_ = true;
// Give daemon a moment to shut down, then restart
// (do this off the main thread to avoid stalling the UI)
async_tasks_.submit(taskName, [this](const util::AsyncTaskManager::Token& token) {
for (int i = 0; i < 20 && !token.cancelled() && !shutting_down_; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (token.cancelled() || shutting_down_) return;
if (token.cancelled() || shutting_down_) { daemon_restarting_ = false; return; }
stopEmbeddedDaemon();
if (token.cancelled() || shutting_down_) return;
if (token.cancelled() || shutting_down_) { daemon_restarting_ = false; return; }
startEmbeddedDaemon();
// tryConnect will be called by the update loop
daemon_restarting_ = false; // re-arm reconnect (tryConnect runs from the update loop)
});
} else {
ui::Notifications::instance().warning(