fix(rescan): stop the instant false "rescan complete"; show live status

Clicking Settings → Rescan restarted the daemon with -rescan correctly, but the
progress poll fired "Blockchain rescan complete" the instant it was clicked,
then showed nothing for the entire (multi-hour) rescan — so it looked broken.

Cause: the very first getrescaninfo poll runs before the daemon has restarted
and hits the still-running pre-restart daemon, which answers rescanning=false.
The completion branch took that as "done", cleared the rescanning flag, and the
real rescan then ran invisibly. (Confirmed from a Windows debug-log capture: an
instant OK{"rescanning":false}, then ~6400 warmup errors over ~5h, all swallowed.)

Fixes:
- Gate completion on a new rescan_confirmed_active_ flag that's only set once we
  actually observe the rescan running, so a pre-restart rescanning=false can't be
  misread as completion.
- While the daemon is in -rescan RPC warmup it rejects every call with the live
  phase as the message ("Loading block index..." -> "Rescanning..."). Treat that
  as proof-of-progress: surface it as rescan_status and mark confirmed-active,
  instead of silently swallowing it. The status bar keeps its animated
  "Rescanning..." for the whole run, then reports complete when warmup ends.
- Read rescan_progress whether the daemon returns it as a string or a number
  (the get<std::string>() would have thrown on a numeric field).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 17:34:44 -05:00
parent bc788d008e
commit 25fef8ff4d
2 changed files with 34 additions and 8 deletions

View File

@@ -600,27 +600,46 @@ void App::update()
auto info = rescanRpc->call("getrescaninfo"); auto info = rescanRpc->call("getrescaninfo");
bool rescanning = info.value("rescanning", false); bool rescanning = info.value("rescanning", false);
float progress = 0.0f; float progress = 0.0f;
if (info.contains("rescan_progress")) { if (info.contains("rescan_progress") && info["rescan_progress"].is_string()) {
std::string progStr = info["rescan_progress"].get<std::string>(); try { progress = std::stof(info["rescan_progress"].get<std::string>()) * 100.0f; } catch (...) {}
try { progress = std::stof(progStr) * 100.0f; } catch (...) {} } else if (info.contains("rescan_progress") && info["rescan_progress"].is_number()) {
progress = info["rescan_progress"].get<float>() * 100.0f;
} }
return [this, rescanning, progress]() { return [this, rescanning, progress]() {
rescan_status_poll_in_progress_ = false; rescan_status_poll_in_progress_ = false;
if (rescanning) { if (rescanning) {
state_.sync.rescanning = true; state_.sync.rescanning = true;
rescan_confirmed_active_ = true;
if (progress > 0.0f) { if (progress > 0.0f) {
state_.sync.rescan_progress = progress / 100.0f; state_.sync.rescan_progress = progress / 100.0f;
} }
} else if (state_.sync.rescanning) { } else if (state_.sync.rescanning && rescan_confirmed_active_) {
// Rescan just finished // Genuine completion: getrescaninfo answers cleanly AND we previously
// saw the rescan running. Without the confirmed-active gate, the first
// poll (which hits the still-running pre-restart daemon, rescanning=false)
// would fire a false "complete" the instant rescan was clicked.
ui::Notifications::instance().success("Blockchain rescan complete"); ui::Notifications::instance().success("Blockchain rescan complete");
state_.sync.rescanning = false; state_.sync.rescanning = false;
rescan_confirmed_active_ = false;
state_.sync.rescan_progress = 1.0f; state_.sync.rescan_progress = 1.0f;
state_.sync.rescan_status.clear(); state_.sync.rescan_status.clear();
} }
// else: rescanning=false but not yet confirmed → pre-restart daemon; keep waiting.
};
} catch (const std::exception& e) {
// During -rescan the daemon is in RPC warmup and rejects every call with the
// current phase as the message ("Loading block index..." → "Rescanning...").
// That's not a failure — it's proof the rescan is running. Surface it and mark
// the rescan confirmed-active so completion is recognised when warmup ends.
std::string phase = e.what();
return [this, phase]() {
rescan_status_poll_in_progress_ = false;
if (state_.sync.rescanning) {
rescan_confirmed_active_ = true;
state_.sync.rescan_status = phase;
}
}; };
} catch (...) { } catch (...) {
// RPC not available yet or failed
return [this](){ rescan_status_poll_in_progress_ = false; }; return [this](){ rescan_status_poll_in_progress_ = false; };
} }
}); });
@@ -2797,12 +2816,15 @@ void App::rescanBlockchain()
ui::Notifications::instance().warning(decision.warning); ui::Notifications::instance().warning(decision.warning);
return; return;
} }
DEBUG_LOGF("[App] Starting blockchain rescan - stopping daemon first\n"); DEBUG_LOGF("[App] Starting blockchain rescan - stopping daemon first\n");
ui::Notifications::instance().info("Restarting daemon with -rescan flag..."); ui::Notifications::instance().info("Restarting daemon with -rescan flag...");
// Initialize rescan state for status bar display // Initialize rescan state for status bar display. rescan_confirmed_active_ stays false until we
// actually observe the restarted daemon rescanning — so the first poll (which may still reach the
// pre-restart daemon and see rescanning=false) can't be misread as instant completion.
state_.sync.rescanning = true; state_.sync.rescanning = true;
rescan_confirmed_active_ = false;
state_.sync.rescan_progress = 0.0f; state_.sync.rescan_progress = 0.0f;
state_.sync.rescan_status = decision.status; state_.sync.rescan_status = decision.status;
transactions_dirty_ = true; transactions_dirty_ = true;

View File

@@ -595,6 +595,10 @@ private:
bool transactions_dirty_ = false; // true → force tx refresh regardless of block height bool transactions_dirty_ = false; // true → force tx refresh regardless of block height
bool encryption_state_prefetched_ = false; // suppress duplicate getwalletinfo on connect bool encryption_state_prefetched_ = false; // suppress duplicate getwalletinfo on connect
bool rescan_status_poll_in_progress_ = false; bool rescan_status_poll_in_progress_ = false;
// True once we've actually observed the rescan running (daemon restarted into -rescan warmup).
// Gates the "rescan complete" detection so a getrescaninfo poll that hits the still-running
// pre-restart daemon (which reports rescanning=false) can't fire a false "complete" instantly.
bool rescan_confirmed_active_ = false;
bool opid_poll_in_progress_ = false; bool opid_poll_in_progress_ = false;
// Consecutive Core-refresh cycles where BOTH core RPCs failed → likely a dead // Consecutive Core-refresh cycles where BOTH core RPCs failed → likely a dead
// connection. After kCoreFailuresBeforeDisconnect, tear down and reconnect. // connection. After kCoreFailuresBeforeDisconnect, tear down and reconnect.