fix(wallet): guard against opening a missing/wrong wallet file (W1-1, W1-2, W1-4)
- W1-1 (High): switchToWallet never verified the target wallet file exists before switching. dragonxd auto-creates a fresh empty wallet for a missing -wallet=<name>, so a moved/deleted wallet file silently "opened" as a brand-new empty wallet with a zero balance — looking exactly like fund loss. It now std::filesystem::exists-checks datadir/<walletFile> before switching (ahead of the daemon-stop prompt) and blocks with a "not found (moved or deleted?)" warning. Because the check runs regardless of how switchToWallet is invoked, it also closes W1-4 (the stale switcher-row TOCTOU). - W1-2 (Med): walletOutputLooksCorrupt matched the generic "Error loading wallet" string, which dragonxd also prints for DB_TOO_NEW (a newer-version wallet) — so a version mismatch was offered a -salvagewallet repair that cannot fix it. The generic match is now excluded when the output also contains "newer version". Build-clean; ctest 1/1. Remaining P1-B: W1-3 (syncedHere timing) + the startup-path existence check. See docs/wallet-hardening.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -197,10 +197,14 @@ static WarmupText translateWarmup(const std::string& raw)
|
||||
// Used to offer a -salvagewallet repair when a switch fails because the target wallet is corrupt.
|
||||
static bool walletOutputLooksCorrupt(const std::string& out)
|
||||
{
|
||||
// W1-2: the generic "Error loading wallet" fallback is ALSO printed for DB_TOO_NEW
|
||||
// ("...requires ... newer version..."), which -salvagewallet cannot fix — so don't misclassify a
|
||||
// version mismatch as salvageable corruption and offer a repair that can't help.
|
||||
const bool versionMismatch = out.find("newer version") != std::string::npos;
|
||||
return out.find("Failed to rename") != std::string::npos
|
||||
|| out.find("salvage failed") != std::string::npos
|
||||
|| out.find("wallet.dat corrupt") != std::string::npos
|
||||
|| out.find("Error loading wallet") != std::string::npos;
|
||||
|| (out.find("Error loading wallet") != std::string::npos && !versionMismatch);
|
||||
}
|
||||
|
||||
// Phrases dragonxd prints to its console while initializing, in the order translateWarmup()
|
||||
@@ -1135,6 +1139,19 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed
|
||||
ui::Notifications::instance().warning("Finish or cancel the pending send before switching wallets.");
|
||||
return;
|
||||
}
|
||||
// W1-1: verify the target wallet file actually exists before switching. dragonxd auto-CREATES a
|
||||
// fresh empty wallet for a missing -wallet=<name>, so without this a moved/deleted wallet file would
|
||||
// silently "open" as a brand-new empty wallet with a zero balance — looking exactly like fund loss.
|
||||
// (Also closes the W1-4 stale-switcher-row race: the check runs no matter how switchToWallet is called.)
|
||||
{
|
||||
std::error_code existEc;
|
||||
const std::string walletPath = util::Platform::getDragonXDataDir() + "/" + walletFile;
|
||||
if (!std::filesystem::exists(walletPath, existEc)) {
|
||||
ui::Notifications::instance().warning(
|
||||
"Wallet file not found (moved or deleted?): " + walletFile + " — it was not opened.", 15.0f);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If we're connected to a node this session did NOT spawn (no live process handle — it was left
|
||||
// running by "keep node running", started by the user, or we just direct-connected to a config-provided
|
||||
// one), confirm before stopping it: switching must stop+restart it on the new wallet, but the user may
|
||||
|
||||
Reference in New Issue
Block a user