feat(node): warn when the daemon auto-recovers (salvages) wallet.dat
dragonxd auto-recovers a wallet.dat that fails BDB verification on startup — no flag needed (CWallet::Verify → CDBEnv::Verify(walletFile, CWalletDB::Recover)): it moves the original to wallet.<timestamp>.bak, salvages readable keys into a fresh wallet.dat, and keeps running. The salvage can be incomplete (or the whole thing a FALSE POSITIVE from stale/cross-platform BDB env state — __db.* / the database/ dir carried between machines), so the node silently comes up on a possibly-empty wallet. To the user that reads as fund loss, with no warning. Detect it and warn loudly instead: - daemon/daemon_startup_diagnosis.h: pure walletAutoRecovered() (the salvage / "Original wallet.dat saved as wallet.<ts>.bak" markers) + newestWalletSalvageBak() (picks the wallet.<unixtime>.bak the recovery just made). - onConnected() scans the node's captured output once per session; on a match it shows a warning dialog + notification: the ORIGINAL is safe in wallet.<ts>.bak, the shown balance may be incomplete, and here are the exact steps to restore it (rename the .bak back + delete the stale database/ + __db.* env). One-click "Open data folder" jumps straight there. Full-node only; lite-safe. Deliberately does NOT auto-swap the wallet files (untested per-platform file manipulation on a real wallet is not worth the risk) — it informs + guides. Adds walletAutoRecovered / newestWalletSalvageBak coverage to testBlockDbOutputDiagnosis. Suite green (1/1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
31
src/app.cpp
31
src/app.cpp
@@ -2144,6 +2144,7 @@ void App::render()
|
||||
renderPinDialogs();
|
||||
renderSwitchStopDaemonDialog();
|
||||
renderBlockDbReindexDialog();
|
||||
renderWalletRecoveredDialog();
|
||||
|
||||
// Render notifications (toast messages)
|
||||
ui::Notifications::instance().render();
|
||||
@@ -4281,6 +4282,36 @@ void App::renderAntivirusHelpDialog()
|
||||
#endif
|
||||
}
|
||||
|
||||
// Auto-shown when the node auto-recovered (salvaged) wallet.dat: the real wallet is safe in a
|
||||
// wallet.<timestamp>.bak, but a possibly-incomplete salvaged copy is now loaded — warn loudly and point
|
||||
// the user at the datadir so they can restore the original instead of mistaking it for fund loss.
|
||||
void App::renderWalletRecoveredDialog()
|
||||
{
|
||||
if (!show_wallet_recovered_dialog_) return;
|
||||
|
||||
ui::material::OverlayDialogSpec ov;
|
||||
ov.title = TR("wallet_recovered_title");
|
||||
ov.p_open = &show_wallet_recovered_dialog_;
|
||||
ov.style = ui::material::OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 560.0f;
|
||||
ov.idSuffix = "walletrecovered";
|
||||
if (!ui::material::BeginOverlayDialog(ov)) return;
|
||||
const float dp = ui::Layout::dpiScale();
|
||||
|
||||
ui::material::DialogWarningHeader(TR("wallet_recovered_warn"));
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
||||
ImGui::TextWrapped("%s", TR("wallet_recovered_body"));
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd()));
|
||||
if (ui::material::TactileButton(TR("wallet_recovered_open_folder"), ImVec2(240.0f * dp, 0))) {
|
||||
util::Platform::openFolder(util::Platform::getDragonXDataDir());
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton(TR("wallet_recovered_dismiss"), ImVec2(150.0f * dp, 0))) {
|
||||
show_wallet_recovered_dialog_ = false; // acknowledged; keeps the salvaged wallet loaded
|
||||
}
|
||||
ui::material::EndOverlayDialog();
|
||||
}
|
||||
|
||||
// Auto-shown when the embedded node aborts on an unreadable block database — offers the one-click
|
||||
// -reindex rebuild instead of leaving the wallet stuck on a silent zero balance.
|
||||
void App::renderBlockDbReindexDialog()
|
||||
|
||||
@@ -900,6 +900,12 @@ private:
|
||||
// connect loop STOPS crash-restarting into the same abort and offers a one-click reindex instead.
|
||||
bool block_db_reindex_available_ = false; // node needs its block DB rebuilt (gates restart loop)
|
||||
bool show_block_db_reindex_confirm_ = false; // auto-shown offer dialog
|
||||
// Wallet auto-recovery: the daemon moved wallet.dat to wallet.<ts>.bak and loaded a salvaged copy
|
||||
// (BDB-verify failure — often a false positive from stale/cross-platform env state). We warn loudly
|
||||
// so a possibly-incomplete salvaged wallet isn't mistaken for fund loss. Warned once per session.
|
||||
bool wallet_auto_recovered_ = false; // a salvage happened this session
|
||||
bool wallet_auto_recovered_warned_ = false; // guard: only surface it once per session
|
||||
bool show_wallet_recovered_dialog_ = false; // auto-shown warning dialog
|
||||
// Live progress for the switch modal: it stays open from confirm through stop → wait-for-exit → start →
|
||||
// reconnect and auto-closes when the new node connects (onConnected). Phase is worker-updated;
|
||||
// dialog_open_ gates rendering — both atomic since onConnected/the worker may run off the main thread.
|
||||
@@ -1331,6 +1337,7 @@ private:
|
||||
void renderSwitchStopDaemonDialog(); // confirm before stopping an adopted node to switch wallets
|
||||
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 processDeferredEncryption();
|
||||
|
||||
// Private methods - connection
|
||||
|
||||
@@ -572,6 +572,20 @@ 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.<ts>.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");
|
||||
}
|
||||
|
||||
// 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).
|
||||
updateWalletIndexForActiveWallet(/*markOpened=*/true);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace daemon {
|
||||
@@ -26,5 +27,38 @@ inline bool blockDbOutputLooksBroken(const std::string& out)
|
||||
|| out.find("LoadBlockIndex()") != std::string::npos; // "... : failed to read value"
|
||||
}
|
||||
|
||||
// True when dragonxd AUTO-RECOVERED the wallet on startup: on any BDB-verify failure it moves the
|
||||
// original wallet.dat to "wallet.{timestamp}.bak", salvages readable keys into a fresh wallet.dat, and
|
||||
// keeps running — no flag required (CWallet::Verify → CDBEnv::Verify(walletFile, CWalletDB::Recover)).
|
||||
// The salvage can be incomplete (or a false positive from stale/cross-platform BDB env state), so the
|
||||
// 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
|
||||
}
|
||||
|
||||
// From a list of datadir filenames, pick the most recent daemon salvage backup — the "wallet.<unixtime>.bak"
|
||||
// the auto-recovery just created (highest timestamp). Returns "" if none present. Pure, so it's testable.
|
||||
inline std::string newestWalletSalvageBak(const std::vector<std::string>& filenames)
|
||||
{
|
||||
long long best = -1;
|
||||
std::string bestName;
|
||||
for (const auto& f : filenames) {
|
||||
if (f.rfind("wallet.", 0) != 0) continue; // must start "wallet."
|
||||
if (f.size() < 12 || f.compare(f.size() - 4, 4, ".bak") != 0) continue; // ...and end ".bak"
|
||||
const std::string mid = f.substr(7, f.size() - 7 - 4); // digits between the dots
|
||||
if (mid.empty() || mid.size() > 18) continue;
|
||||
bool allDigits = true;
|
||||
for (char c : mid) if (c < '0' || c > '9') { allDigits = false; break; }
|
||||
if (!allDigits) continue;
|
||||
long long ts = 0;
|
||||
for (char c : mid) ts = ts * 10 + (c - '0');
|
||||
if (ts > best) { best = ts; bestName = f; }
|
||||
}
|
||||
return bestName;
|
||||
}
|
||||
|
||||
} // namespace daemon
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -1193,6 +1193,14 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["block_db_reindex_notify"] = "The node can't read its block database (often after a daemon update). Rebuild it to restore your balance — see the prompt, or Settings › Node.";
|
||||
strings_["block_db_reindex_started"] = "Rebuilding the block database from your blocks — this can take a while.";
|
||||
|
||||
// Wallet auto-recovery warning (the node moved wallet.dat aside and loaded a salvaged copy).
|
||||
strings_["wallet_recovered_title"] = "Your wallet was auto-recovered";
|
||||
strings_["wallet_recovered_warn"] = "The node moved your wallet aside and loaded a salvaged copy.";
|
||||
strings_["wallet_recovered_body"] = "On startup the node decided your wallet.dat looked damaged and recovered it automatically. Your ORIGINAL wallet was NOT deleted — it was renamed to \"wallet.<numbers>.bak\" in your data folder, and a salvaged copy is loaded now.\n\nThe salvaged copy may be incomplete, so the balance shown here could be wrong — don't treat it as final.\n\nThis is often a false alarm caused by leftover database files (e.g. after moving the wallet between machines). To restore your original: quit the wallet, then in the data folder rename the current wallet.dat aside, rename \"wallet.<numbers>.bak\" back to \"wallet.dat\", delete the \"database\" folder and any \"__db.*\" files, and reopen.";
|
||||
strings_["wallet_recovered_open_folder"] = "Open data folder";
|
||||
strings_["wallet_recovered_dismiss"] = "Keep salvaged copy";
|
||||
strings_["wallet_recovered_notify"] = "The node recovered your wallet and moved the original to a .bak — your shown balance may be incomplete. See the prompt to restore it.";
|
||||
|
||||
// Receive Tab
|
||||
strings_["receiving_addresses"] = "Your Receiving Addresses";
|
||||
strings_["new_z_shielded"] = "New z-Address (Shielded)";
|
||||
|
||||
@@ -6139,6 +6139,23 @@ void testBlockDbOutputDiagnosis()
|
||||
EXPECT_FALSE(blockDbOutputLooksBroken("Error loading wallet")); // wallet corruption → salvage, not reindex
|
||||
EXPECT_FALSE(blockDbOutputLooksBroken("Error: Could not find any asmap file!"));
|
||||
EXPECT_FALSE(blockDbOutputLooksBroken(""));
|
||||
|
||||
// Wallet auto-recovery detection: the node salvaged wallet.dat (moved the original to a .bak).
|
||||
using dragonx::daemon::walletAutoRecovered;
|
||||
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"));
|
||||
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(""));
|
||||
|
||||
// Newest salvage backup picker (the "wallet.<unixtime>.bak" the recovery just made).
|
||||
using dragonx::daemon::newestWalletSalvageBak;
|
||||
EXPECT_EQ(newestWalletSalvageBak({"wallet.dat", "wallet.1786200000.bak", "wallet.1786300000.bak", "peers.dat"}),
|
||||
std::string("wallet.1786300000.bak")); // highest timestamp wins
|
||||
EXPECT_EQ(newestWalletSalvageBak({"wallet.dat", "wallet.dat.encrypted.bak", "notes.txt"}),
|
||||
std::string("")); // no wallet.<digits>.bak present
|
||||
EXPECT_EQ(newestWalletSalvageBak({}), std::string(""));
|
||||
}
|
||||
|
||||
// Live probe of a real lite server (env-gated). Validates CONNECT_ONLY latency + IP capture.
|
||||
|
||||
Reference in New Issue
Block a user