fix(node): detect a wallet salvage at startup, not only on connect
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>
This commit is contained in:
@@ -229,6 +229,24 @@ static constexpr int kDaemonWaitWarnAttempts = 4;
|
||||
// Connection Management
|
||||
// ============================================================================
|
||||
|
||||
// dragonxd moves wallet.dat to wallet.<ts>.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.<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");
|
||||
}
|
||||
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).
|
||||
|
||||
Reference in New Issue
Block a user