From 3216debc7d2094ff441f4049787f6a3d24d65a39 Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 10 Aug 2026 12:04:07 -0500 Subject: [PATCH] fix(node): detect a wallet salvage at startup, not only on connect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported: loading a BDB-inconsistent wallet silently renamed it and created a new one — no recovery dialog. Two causes, both fixed: 1) Detection ran only in onConnected(). The salvage happens at STARTUP, and the node may never connect (block-index abort, long sync, crash) — or a long sync trims the salvage line out of the rolling output buffer before connect. Extract detectWalletAutoRecovery() and run it every tryConnect() tick (every ~5s during startup), so the salvage is caught the instant it appears, regardless of whether the node connects. Also hold the crash-restart loop while a salvage is pending, so the wallet can't be re-salvaged/shrunk while the Rebuild/Restore dialog is up. 2) walletAutoRecovered() only matched the SUCCESSFUL-salvage strings. A BDB-inconsistent file makes aggressive salvage FAIL ("found no records"), which prints different lines. Broaden the detector to the signals that fire in every case: "CDBEnv::Salvage", the "Renamed to wallet..bak" rename, and "found no records in wallet" — while still not matching normal startup or a block-DB abort. Adds the exact failed-salvage sequence to the detector test. Build clean, suite green (1/1). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app.h | 1 + src/app_network.cpp | 40 ++++++++++++++++++--------- src/daemon/daemon_startup_diagnosis.h | 15 ++++++++-- src/util/i18n.cpp | 1 + tests/test_phase4.cpp | 6 ++++ 5 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/app.h b/src/app.h index 9f66b7b..a9fde2d 100644 --- a/src/app.h +++ b/src/app.h @@ -1344,6 +1344,7 @@ private: void renderBlockDbReindexDialog(); // offer to rebuild an unreadable block database (-reindex) void reindexBlockDatabase(); // restart the daemon with -reindex to rebuild the block DB void renderWalletRecoveredDialog(); // warn that the node auto-recovered/salvaged wallet.dat + void detectWalletAutoRecovery(); // scan daemon output for a salvage; fire the warning once/session void restoreOriginalWallet(); // swap the wallet..bak back over the salvaged copy + restart void pumpWalletRestore(); // main-thread: surface the restore/rebuild op's result void rebuildWalletDatabase(); // rebuild a BDB-inconsistent wallet into a loadable one (helper) diff --git a/src/app_network.cpp b/src/app_network.cpp index e7bd35a..f24e1b2 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -229,6 +229,24 @@ static constexpr int kDaemonWaitWarnAttempts = 4; // Connection Management // ============================================================================ +// dragonxd moves wallet.dat to wallet..bak and loads a salvaged copy whenever BDB verify fails — +// no flag, and often a false positive (stale/cross-platform env) or an inconsistent-but-readable file. +// The salvage prints to the node's captured output at STARTUP, but the node may then fail to connect +// (block-index abort, long sync, crash) so we must NOT wait for onConnected — scan the output on every +// tryConnect tick, early enough that the line hasn't been trimmed from the rolling buffer. Fires once +// per session; the dialog offers Rebuild (fix the DB) / Restore (swap the .bak back). +void App::detectWalletAutoRecovery() +{ + if (wallet_auto_recovered_warned_) return; + if (!isUsingEmbeddedDaemon() || !daemon_controller_ || !daemon_controller_->daemon()) return; + if (!daemon::walletAutoRecovered(daemon_controller_->daemon()->getOutput())) return; + wallet_auto_recovered_ = true; + wallet_auto_recovered_warned_ = true; + show_wallet_recovered_dialog_ = true; + ui::Notifications::instance().error(TR("wallet_recovered_notify"), 30.0f); + VERBOSE_LOGF("[recovery] Daemon auto-recovered/salvaged wallet.dat — surfacing the recovery dialog\n"); +} + void App::tryConnect() { // Lite builds have no full node / RPC daemon, so never run the RPC connection state machine @@ -236,6 +254,10 @@ void App::tryConnect() // derived from it each frame in App::update(), which also gates the wallet UI (isConnected()). if (isLiteBuild()) return; + // Catch a startup wallet salvage as soon as it appears in the node's output — independent of whether + // the node ever finishes starting or connects (skip only while an orchestrated swap is mid-flight). + if (!daemon_restarting_) detectWalletAutoRecovery(); + if (connection_in_progress_) return; // Don't fight an in-progress restart/adopt orchestration: while it stops the daemon, swaps @@ -519,6 +541,10 @@ void App::tryConnect() // Prevent infinite crash-restart loop if (block_db_reindex_available_) { connection_status_ = TR("sb_block_db_unreadable"); // hold; awaiting the rebuild choice + } else if (wallet_auto_recovered_) { + // A salvage is happening — DON'T restart into another one (each round can shrink + // the wallet further). Hold while the recovery dialog (Rebuild/Restore) is up. + connection_status_ = TR("sb_wallet_needs_recovery"); } else 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 @@ -575,19 +601,7 @@ void App::onConnected() daemon_last_seen_crashes_ = 0; // (onConnected resets the daemon's crash count too) connection_status_ = TR("connected"); - // Detect a silent wallet AUTO-RECOVERY: dragonxd moves wallet.dat to wallet..bak and loads a - // salvaged copy whenever BDB verify fails (no flag, often a false positive from stale/cross-platform - // env state). The node comes up fine — so we only see it here, on connect — but the loaded wallet can - // be empty/incomplete, which reads as fund loss. Surface it loudly, once per session, so the user can - // restore the untouched original from the .bak. (Full-node only; lite has no embedded dragonxd.) - if (!wallet_auto_recovered_warned_ && isUsingEmbeddedDaemon() && daemon_controller_ && daemon_controller_->daemon() && - daemon::walletAutoRecovered(daemon_controller_->daemon()->getOutput())) { - wallet_auto_recovered_ = true; - wallet_auto_recovered_warned_ = true; - show_wallet_recovered_dialog_ = true; - ui::Notifications::instance().error(TR("wallet_recovered_notify"), 30.0f); - VERBOSE_LOGF("[connect] Daemon auto-recovered wallet.dat (salvage) — warning the user\n"); - } + detectWalletAutoRecovery(); // also runs every tryConnect tick — catches a salvage even if we never connect // Stamp the active wallet as opened in the index (last-opened + size + synced-here). Balance + // address count fill in on the first address refresh (addresses aren't loaded yet here). diff --git a/src/daemon/daemon_startup_diagnosis.h b/src/daemon/daemon_startup_diagnosis.h index 5307865..72ce57e 100644 --- a/src/daemon/daemon_startup_diagnosis.h +++ b/src/daemon/daemon_startup_diagnosis.h @@ -35,9 +35,18 @@ inline bool blockDbOutputLooksBroken(const std::string& out) // node silently comes up on a possibly-empty wallet — which reads as fund loss unless we surface it. inline bool walletAutoRecovered(const std::string& out) { - return out.find("wallet.dat corrupt, data salvaged") != std::string::npos // RECOVER_OK warning - || out.find("Original wallet.dat saved as wallet.") != std::string::npos // the rename-aside notice - || out.find("wallet.dat corrupt, salvage failed") != std::string::npos; // RECOVER_FAIL + // Cover BOTH salvage outcomes. A successful salvage prints the "data salvaged"/"saved as wallet..bak" + // warning; a FAILED one (e.g. an inconsistent-but-readable file where aggressive salvage finds no + // records) prints "salvage failed"/"found no records". In every case CWalletDB::Recover first logs + // "Renamed to wallet..bak" and CDBEnv::Salvage logs its own banner — those two fire the + // instant a salvage begins, before the daemon may abort, so they're the earliest reliable signal. + return out.find("CDBEnv::Salvage") != std::string::npos // salvage is running + || out.find("wallet.dat corrupt, data salvaged") != std::string::npos // RECOVER_OK + || out.find("Original wallet.dat saved as wallet.") != std::string::npos + || out.find("wallet.dat corrupt, salvage failed") != std::string::npos // RECOVER_FAIL + || out.find("found no records in wallet") != std::string::npos // aggressive salvage empty + || (out.find("Renamed ") != std::string::npos && out.find(" to wallet.") != std::string::npos + && out.find(".bak") != std::string::npos); // Recover moved wallet.dat aside } // If `name` is a daemon salvage backup "wallet..bak", return its timestamp; else -1. diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 87b5de3..d0fcecd 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1345,6 +1345,7 @@ void I18n::loadBuiltinEnglish() strings_["sb_daemon_crashed"] = "Daemon crashed %d times"; strings_["sb_daemon_start_failed"] = "Couldn't start dragonxd"; strings_["sb_block_db_unreadable"] = "Block database unreadable — rebuild required"; + strings_["sb_wallet_needs_recovery"] = "Wallet needs recovery — see the prompt"; // Persistent node-status banner (App::renderNodeStatusBanner). strings_["node_banner_offline_title"] = "Not connected to the DragonX node"; strings_["node_banner_crashed_title"] = "The node stopped unexpectedly"; diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 321c783..c420a7d 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -6157,6 +6157,12 @@ void testBlockDbOutputDiagnosis() EXPECT_TRUE(walletAutoRecovered( "Warning: wallet.dat corrupt, data salvaged! Original wallet.dat saved as wallet.1786300000.bak in ...")); EXPECT_TRUE(walletAutoRecovered("wallet.dat corrupt, salvage failed")); + // The FAILED-salvage sequence a BDB-inconsistent wallet actually produces (must also be detected — + // this is the case that previously slipped through and silently emptied the wallet). + EXPECT_TRUE(walletAutoRecovered( + "Renamed wallet.dat to wallet.1786375505.bak\n" + "CDBEnv::Salvage: Database salvage found errors, all data may not be recoverable.\n" + "Salvage(aggressive) found no records in wallet.1786375505.bak.\n")); EXPECT_FALSE(walletAutoRecovered("Loading wallet...\nWallet completed loading\n")); // normal load EXPECT_FALSE(walletAutoRecovered(": Error loading block database.")); // block-DB abort != salvage EXPECT_FALSE(walletAutoRecovered(""));