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:
16
src/app.cpp
16
src/app.cpp
@@ -649,6 +649,9 @@ void App::update()
|
||||
// capture_mode_ is only set for the offline full sweep, so the live tab-only sweep is unaffected.
|
||||
if (capture_mode_) return;
|
||||
|
||||
// If a wallet switch's daemon failed to start, revert to the previous wallet (main-thread-safe).
|
||||
processWalletSwitchRevert();
|
||||
|
||||
// Track user interaction for auto-lock
|
||||
if (io.MouseDelta.x != 0 || io.MouseDelta.y != 0 ||
|
||||
io.MouseClicked[0] || io.MouseClicked[1] ||
|
||||
@@ -4150,6 +4153,11 @@ void App::rescanBlockchain()
|
||||
ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress.");
|
||||
return;
|
||||
}
|
||||
// Don't race a wallet switch / seed-adopt / encryption restart, which drive their own daemon stop/start.
|
||||
if (daemon_restarting_) {
|
||||
ui::Notifications::instance().warning("The node is busy restarting — try again in a moment.");
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[App] Starting blockchain rescan - stopping daemon first\n");
|
||||
ui::Notifications::instance().info("Restarting daemon with -rescan flag...");
|
||||
@@ -4197,6 +4205,10 @@ void App::repairWallet()
|
||||
ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress.");
|
||||
return;
|
||||
}
|
||||
if (daemon_restarting_) {
|
||||
ui::Notifications::instance().warning("The node is busy restarting — try again in a moment.");
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[App] Starting wallet repair (-zapwallettxes=2) - stopping daemon first\n");
|
||||
ui::Notifications::instance().info("Restarting daemon with -zapwallettxes=2 (wallet repair)...");
|
||||
@@ -4370,6 +4382,10 @@ void App::beginShutdown()
|
||||
// the adopt task will NOT restart the daemon (it checks shutting_down_ before startEmbeddedDaemon).
|
||||
if (async_tasks_.isRunning("Adopt seed wallet"))
|
||||
async_tasks_.join("Adopt seed wallet");
|
||||
// The wallet-switch task also drives daemon stop/start — let it finish before shutdown touches the
|
||||
// daemon so it can't orphan a freshly-started dragonxd (it checks shutting_down_ before starting).
|
||||
if (async_tasks_.isRunning("Switch wallet"))
|
||||
async_tasks_.join("Switch wallet");
|
||||
|
||||
// Signal the RPC worker to stop accepting new tasks (non-blocking), and abort any call
|
||||
// already in flight so the later join() doesn't wait out a request timeout.
|
||||
|
||||
@@ -475,6 +475,8 @@ public:
|
||||
// (rescan only if it was never synced in this datadir). Per-wallet data follows automatically
|
||||
// via the identity-scoped caches (P1). No-op if that wallet is already active.
|
||||
void switchToWallet(const std::string& walletFile);
|
||||
// Main-thread continuation: if a wallet switch's daemon failed to start, revert active_wallet_file.
|
||||
void processWalletSwitchRevert();
|
||||
|
||||
// Wallet encryption helpers
|
||||
void encryptWalletWithPassphrase(const std::string& passphrase);
|
||||
@@ -729,6 +731,13 @@ private:
|
||||
// Daemon restart (e.g. after changing debug log categories)
|
||||
std::atomic<bool> daemon_restarting_{false};
|
||||
|
||||
// Wallet-switch failure recovery: the switch worker sets wallet_switch_failed_ when the new
|
||||
// wallet's daemon won't start/stay up; the main loop then reverts active_wallet_file to
|
||||
// wallet_switch_prev_file_ (settings writes stay on the main thread) so a broken wallet isn't
|
||||
// persisted across restarts. daemon_restarting_ stays set until the revert re-arms the reconnect.
|
||||
std::atomic<bool> wallet_switch_failed_{false};
|
||||
std::string wallet_switch_prev_file_;
|
||||
|
||||
// Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast
|
||||
// and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it.
|
||||
std::atomic<int> pending_delete_result_{-1};
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user