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 <wallet> to wallet.<ts>.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) <noreply@anthropic.com>
98 lines
5.2 KiB
C++
98 lines
5.2 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
//
|
|
// daemon_startup_diagnosis.h — pure classifiers over a crashed daemon's captured console output,
|
|
// so the app can offer a targeted one-click fix instead of a bare "daemon crashed" / silent no-funds.
|
|
|
|
#pragma once
|
|
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace dragonx {
|
|
namespace daemon {
|
|
|
|
// True when dragonxd aborted because its BLOCK DATABASE could not be loaded — either a
|
|
// daemon-vs-chaindata serialization-format mismatch after a daemon update (the deterministic
|
|
// "non-canonical optional discriminant" → "Error loading block database" → "Aborted block database
|
|
// rebuild. Exiting." sequence) or a genuinely corrupt/incomplete block index. In BOTH cases the fix
|
|
// is the same: `-reindex` rebuilds the index + chainstate from the intact raw blocks (blk*.dat).
|
|
// This is what otherwise silently presents as a wallet with zero balance — the node never starts.
|
|
inline bool blockDbOutputLooksBroken(const std::string& out)
|
|
{
|
|
return out.find("Error loading block database") != std::string::npos
|
|
|| out.find("non-canonical optional discriminant") != std::string::npos
|
|
|| out.find("Aborted block database rebuild") != std::string::npos
|
|
|| 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)
|
|
{
|
|
// Cover BOTH salvage outcomes. A successful salvage prints the "data salvaged"/"saved as wallet.<ts>.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 <wallet> to wallet.<ts>.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.<unixtime>.bak", return its timestamp; else -1.
|
|
inline long long parseWalletSalvageBakTs(const std::string& name)
|
|
{
|
|
if (name.rfind("wallet.", 0) != 0) return -1; // must start "wallet."
|
|
if (name.size() < 12 || name.compare(name.size() - 4, 4, ".bak") != 0) return -1; // ...and end ".bak"
|
|
const std::string mid = name.substr(7, name.size() - 7 - 4); // digits between the dots
|
|
if (mid.empty() || mid.size() > 18) return -1;
|
|
for (char c : mid) if (c < '0' || c > '9') return -1;
|
|
long long ts = 0;
|
|
for (char c : mid) ts = ts * 10 + (c - '0');
|
|
return ts;
|
|
}
|
|
|
|
// Most RECENT salvage backup (highest timestamp). Pure, testable.
|
|
inline std::string newestWalletSalvageBak(const std::vector<std::string>& filenames)
|
|
{
|
|
long long best = -1;
|
|
std::string bestName;
|
|
for (const auto& f : filenames) {
|
|
const long long ts = parseWalletSalvageBakTs(f);
|
|
if (ts > best) { best = ts; bestName = f; }
|
|
}
|
|
return bestName;
|
|
}
|
|
|
|
// LARGEST salvage backup, from (filename, fileSize) pairs — the least-salvaged one, i.e. the original.
|
|
// This is what "Restore original wallet" should use: a salvage CASCADE shrinks the wallet each round, so
|
|
// the newest .bak is the WORST and the largest is the pristine pre-salvage original (an emptied salvage
|
|
// is tiny; a real wallet is large). Ties break toward the newest timestamp. Returns "" if none present.
|
|
inline std::string largestWalletSalvageBak(const std::vector<std::pair<std::string, unsigned long long>>& files)
|
|
{
|
|
std::string bestName;
|
|
unsigned long long bestSize = 0;
|
|
long long bestTs = -1;
|
|
for (const auto& fp : files) {
|
|
const long long ts = parseWalletSalvageBakTs(fp.first);
|
|
if (ts < 0) continue;
|
|
if (fp.second > bestSize || (fp.second == bestSize && ts > bestTs)) {
|
|
bestSize = fp.second; bestTs = ts; bestName = fp.first;
|
|
}
|
|
}
|
|
return bestName;
|
|
}
|
|
|
|
} // namespace daemon
|
|
} // namespace dragonx
|