From 928335dd6bee169dc5b94c4426aeaf789493b25a Mon Sep 17 00:00:00 2001 From: DanS Date: Wed, 15 Jul 2026 15:41:03 -0500 Subject: [PATCH] fix(wallet-switch): late-fail revert, shutdown-freeze, per-wallet PIN gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two deferred audit MEDs + the per-wallet PIN follow-up: - Late-init failure revert (MED): the 1.5s start grace only catches a wallet that fails IMMEDIATELY. A wallet that fails LATE in dragonxd init (past the grace) used to persist as a broken active_wallet_file. Now a switch stays "pending confirm" until the daemon actually connects (onConnected clears it); if the connect loop instead hits the crash-wedge (crashCount >= 3) while a switch is pending, it flags the main-thread revert (which also resets the crash count so the restored wallet can start). - Shutdown freeze (MED): the switch worker's 30s daemon-stop wait now breaks promptly when shutdown starts, so beginShutdown's join of the switch task can't freeze the UI for the full 30s (shutdown stops the daemon itself). The seed- adopt task is intentionally left to finish (fund-safety), as before. - Per-wallet PIN (follow-up): App::hasPinVault() (and the lock-screen path) now gate on the per-wallet vault presence alone, not the GLOBAL getPinEnabled flag — so disabling PIN on one wallet no longer suppresses another wallet's PIN quick-unlock after a switch. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app.cpp | 5 ++++- src/app.h | 4 ++++ src/app_network.cpp | 17 ++++++++++++++++- src/app_security.cpp | 4 ++-- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index ff1625b..108f346 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -5126,7 +5126,10 @@ bool App::isFirstRun() const { } bool App::hasPinVault() const { - return vault_ && vault_->hasVault() && settings_ && settings_->getPinEnabled(); + // Gate purely on the (now per-wallet) vault presence — a vault exists iff a PIN was set for THIS + // wallet. The old extra `getPinEnabled()` gate was a GLOBAL flag, so disabling PIN on one wallet + // wrongly suppressed another wallet's PIN quick-unlock after a switch. + return vault_ && vault_->hasVault(); } bool App::debugGateRequiresAuth() const { diff --git a/src/app.h b/src/app.h index f6fbd2d..57734d4 100644 --- a/src/app.h +++ b/src/app.h @@ -740,6 +740,10 @@ private: // 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 wallet_switch_failed_{false}; + // True from a switch until the new wallet's daemon actually connects (onConnected). If the daemon + // instead crash-wedges (a wallet that fails LATE in init, past the fast start grace), the connect + // loop flags a revert so a broken wallet still can't stick. + std::atomic wallet_switch_pending_confirm_{false}; std::string wallet_switch_prev_file_; // Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast diff --git a/src/app_network.cpp b/src/app_network.cpp index 2d10097..32340f7 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -463,6 +463,14 @@ void App::tryConnect() if (isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) { // Prevent infinite crash-restart loop if (daemon_controller_ && daemon_controller_->crashCount() >= 3) { + if (wallet_switch_pending_confirm_.load()) { + // The just-switched-to wallet's daemon keeps crashing (e.g. a wallet that + // fails LATE in init, past the fast start grace) — revert to the previous + // wallet rather than wedging on a broken one. The main thread does the + // settings revert (processWalletSwitchRevert) + resets the crash count. + wallet_switch_pending_confirm_.store(false); + wallet_switch_failed_.store(true); + } { char buf[128]; snprintf(buf, sizeof(buf), TR("sb_daemon_crashed"), daemon_controller_->crashCount()); connection_status_ = buf; } VERBOSE_LOGF("[connect #%d] Daemon crashed %d times — not restarting (use Settings > Restart Daemon to retry)\n", @@ -496,6 +504,7 @@ void App::tryConnect() void App::onConnected() { state_.connected = true; + wallet_switch_pending_confirm_.store(false); // a successful connect confirms the switch state_.daemon_initializing = false; // RPC is answering now; clear the "initializing" overlay daemon_wait_attempts_ = 0; // re-arm the port-busy / start-failure notifications daemon_start_error_shown_ = false; @@ -1056,6 +1065,7 @@ void App::switchToWallet(const std::string& walletFile) const std::string prevWallet = settings_->getActiveWalletFile(); // for revert if the new one fails wallet_switch_prev_file_ = prevWallet; wallet_switch_failed_.store(false); + wallet_switch_pending_confirm_.store(true); // cleared on a successful connect (onConnected) settings_->setActiveWalletFile(walletFile); settings_->save(); // A prior crash-wedge shouldn't block reconnecting to the freshly-launched wallet daemon. @@ -1091,7 +1101,9 @@ void App::switchToWallet(const std::string& walletFile) try { // Stop the node fully so it releases the datadir .lock + RPC port before we relaunch. stopEmbeddedDaemon(); - for (int i = 0; i < 60 && isEmbeddedDaemonRunning(); ++i) + // Break the wait promptly if shutdown starts, so beginShutdown's join() doesn't freeze the + // UI for the full 30s (shutdown then stops the daemon itself; we won't start a new one). + for (int i = 0; i < 60 && isEmbeddedDaemonRunning() && !shutting_down_; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(500)); if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true); ok = shutting_down_ ? true : startEmbeddedDaemon(); @@ -1121,12 +1133,15 @@ void App::switchToWallet(const std::string& walletFile) void App::processWalletSwitchRevert() { if (!wallet_switch_failed_.exchange(false)) return; + wallet_switch_pending_confirm_.store(false); 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_); } + // Clear the crash-wedge so the (good) previous wallet's daemon is allowed to start again. + if (daemon_controller_) daemon_controller_->resetCrashCount(); 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 } diff --git a/src/app_security.cpp b/src/app_security.cpp index e493162..f734926 100644 --- a/src/app_security.cpp +++ b/src/app_security.cpp @@ -899,8 +899,8 @@ void App::renderLockScreen() { cy += captionFont->LegacySize + 12.0f * dp; } - // Check if PIN vault is available - bool hasPinVault = vault_ && vault_->hasVault() && settings_ && settings_->getPinEnabled(); + // Check if PIN vault is available (per-wallet vault presence; not the global getPinEnabled flag). + bool hasPinVault = vault_ && vault_->hasVault(); // Mode toggle (PIN / Passphrase) — only show if PIN vault exists if (hasPinVault) {