fix(shutdown): don't warn "node is rebuilding" for routine witness activity / on v1.3.0

The shutdown guard fired its "Node is rebuilding its witness cache" prompt almost always on an
active wallet: the daemon does a per-tx VerifyAndSetInitialWitness as each newly received wallet
tx lands during normal sync, and those markers (sparse — minutes apart — with no matching
"rebuilt N note witness cache(s) … in Xms") kept the heuristic latched "active".

- daemonWitnessRebuildActive() now requires the last progress marker to be part of the CURRENT
  log activity (within ~15s of the newest log line, via a same-log timestamp delta), so routine
  minutes-old per-tx witness sets no longer count as an ongoing rebuild.
- shouldConfirmDaemonStop() suppresses the prompt entirely on v1.3.0+ daemons (version >= 1030000):
  they checkpoint witness-rescan progress, so stopping mid-rebuild resumes on the next start rather
  than redoing it — the warning's "restarts it (several minutes)" premise no longer holds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 02:21:49 -05:00
parent 398fb274fa
commit 56d93b6128

View File

@@ -5808,26 +5808,47 @@ std::vector<std::string> App::tailDaemonDebugLog(int maxLines) const
return lines;
}
// Parse a "YYYY-MM-DD HH:MM:SS ..." debug.log line prefix to time_t. Interpreted as local time, but it
// is only ever used for DELTAS between two lines of the SAME log, so the timezone cancels. Returns 0 if
// the line has no such timestamp prefix.
static std::time_t parseDaemonLogTimestamp(const std::string& line)
{
int y = 0, mo = 0, d = 0, h = 0, mi = 0, s = 0;
if (std::sscanf(line.c_str(), "%d-%d-%d %d:%d:%d", &y, &mo, &d, &h, &mi, &s) != 6) return 0;
std::tm tm{};
tm.tm_year = y - 1900; tm.tm_mon = mo - 1; tm.tm_mday = d;
tm.tm_hour = h; tm.tm_min = mi; tm.tm_sec = s; tm.tm_isdst = -1;
return std::mktime(&tm);
}
bool App::daemonWitnessRebuildActive() const
{
// Scan the debug.log tail for the daemon's witness-rebuild markers (wallet.cpp): "Cleared witness
// data from" (start), "Setting Initial Sapling Witness" / "Reading blocks for witness rebuild"
// (progress), vs. "rebuilt N note witness cache(s)" / "aborting…" (finished). Active iff the most
// recent relevant line is a start/progress line, not a completion.
const auto lines = tailDaemonDebugLog(80);
int state = 0; // 0 none, 1 active, 2 finished/aborted
// data from" (start), "Reading blocks for witness rebuild" / "Setting Initial Sapling Witness"
// (progress), vs. "rebuilt N note witness cache(s)" / "aborting…" (finished).
//
// A genuine ongoing rebuild logs progress CONTINUOUSLY. The routine per-tx witness set the daemon
// does as each new wallet tx lands during normal sync is SPARSE (minutes apart) and must NOT trip
// this — that was firing the "node is rebuilding" prompt on wallets that just receive frequently.
// So require the last progress marker to be (a) later than any completion AND (b) part of the CURRENT
// activity — within a few seconds of the newest log line (same-log timestamp delta → timezone-free).
const auto lines = tailDaemonDebugLog(120);
std::time_t newest = 0, lastProgress = 0, lastDone = 0;
for (const auto& l : lines) {
const std::time_t ts = parseDaemonLogTimestamp(l);
if (ts > newest) newest = ts;
if (l.find("note witness cache(s) to height") != std::string::npos ||
l.find("aborting witness rebuild") != std::string::npos ||
l.find("aborted during witness rebuild") != std::string::npos) {
state = 2;
if (ts > lastDone) lastDone = ts;
} else if (l.find("Reading blocks for witness rebuild") != std::string::npos ||
l.find("Setting Initial Sapling Witness") != std::string::npos ||
l.find("Cleared witness data from") != std::string::npos) {
state = 1;
if (ts > lastProgress) lastProgress = ts;
}
}
return state == 1;
if (lastProgress == 0 || lastDone >= lastProgress || newest == 0) return false;
return (newest - lastProgress) <= 15; // progress is part of the current activity → ongoing rebuild
}
bool App::shouldConfirmDaemonStop() const
@@ -5839,6 +5860,10 @@ bool App::shouldConfirmDaemonStop() const
settings_ && settings_->getKeepDaemonRunning(),
settings_ && settings_->getStopExternalDaemon());
if (decision.action != daemon::DaemonController::ShutdownAction::StopDaemon) return false;
// v1.3.0+ checkpoints witness-rescan progress, so stopping mid-rebuild resumes on the next start
// instead of redoing it from scratch — the warning's premise no longer holds, so don't prompt.
// (daemon_version encodes major*1e6 + minor*1e4 + rev*100 + build; v1.3.0 == 1030000.)
if (state_.daemon_version >= 1030000) return false;
return daemonWitnessRebuildActive();
}