fix(fullnode): stable overall progress for Sapling witness rebuild

The witness-rebuild bar reset repeatedly because the daemon's "Building Witnesses
for block <h> <frac> complete" line reports per-call progress: BuildWitnessCache is
re-invoked for each connected block and each call walks from its own start height to
the tip, so the fraction restarts every time. The earlier "Setting Initial Sapling
Witness for tx <i> of <m>" counter resets per call too, so neither is a usable
overall metric.

Derive a stable, monotonic percentage from the "<n> remaining" count instead: track
the largest "remaining" seen during the phase as the full span and show how far
remaining has fallen below it. The longest pass defines 0→100%; the short per-block
follow-up passes only nudge the bar near the end rather than resetting it. The
"Setting Initial" line now only marks the phase active. Per-phase tracking resets at
phase start and every rescan-completion site.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-21 12:44:01 -05:00
parent b32fe07cb1
commit e2bc3623b6
3 changed files with 39 additions and 19 deletions

View File

@@ -652,6 +652,7 @@ void App::update()
state_.sync.building_witnesses = false; state_.sync.building_witnesses = false;
state_.sync.witness_progress = 0.0f; state_.sync.witness_progress = 0.0f;
state_.sync.witness_remaining = 0; state_.sync.witness_remaining = 0;
witness_rebuild_total_blocks_ = 0;
} }
// else: rescanning=false but not yet confirmed → pre-restart daemon; keep waiting. // else: rescanning=false but not yet confirmed → pre-restart daemon; keep waiting.
}; };
@@ -825,23 +826,11 @@ void App::update()
} }
// Earlier per-tx phase: "Setting Initial Sapling Witness for tx <hash>, <i> of <m>". // Earlier per-tx phase: "Setting Initial Sapling Witness for tx <hash>, <i> of <m>".
auto setIdx = line.find("Setting Initial Sapling Witness"); // Its <i> resets to 0 on every BuildWitnessCache call (re-invoked per block), so
if (setIdx != std::string::npos) { // it's not a reliable overall counter — use it only to mark that the witness
// phase is underway; the block-walk "remaining" below drives the percentage.
if (line.find("Setting Initial Sapling Witness") != std::string::npos) {
foundWitness = true; foundWitness = true;
auto ofIdx = line.find(" of ", setIdx);
if (ofIdx != std::string::npos) {
size_t iEnd = ofIdx, iStart = iEnd;
while (iStart > 0 && std::isdigit((unsigned char)line[iStart - 1])) iStart--;
size_t mStart = ofIdx + 4, mEnd = mStart;
while (mEnd < line.size() && std::isdigit((unsigned char)line[mEnd])) mEnd++;
if (iStart < iEnd && mStart < mEnd) {
try {
float i = std::stof(line.substr(iStart, iEnd - iStart));
float m = std::stof(line.substr(mStart, mEnd - mStart));
if (m > 0.0f) witnessPct = (i / m) * 100.0f;
} catch (...) {}
}
}
lastStatus = line; lastStatus = line;
} }
} }
@@ -861,17 +850,41 @@ void App::update()
state_.sync.building_witnesses = false; state_.sync.building_witnesses = false;
state_.sync.witness_progress = 0.0f; state_.sync.witness_progress = 0.0f;
state_.sync.witness_remaining = 0; state_.sync.witness_remaining = 0;
witness_rebuild_total_blocks_ = 0;
} else if (foundWitness) { } else if (foundWitness) {
// Witness rebuild is the tail phase of a rescan — keep rescanning set so // Witness rebuild is the tail phase of a rescan — keep rescanning set so
// the broader gating holds, but surface the witness-specific progress. // the broader gating holds, but surface the witness-specific progress.
state_.sync.rescanning = true; state_.sync.rescanning = true;
rescan_confirmed_active_ = true; rescan_confirmed_active_ = true;
state_.sync.building_witnesses = true; // First witness line of a phase → reset the per-phase tracking so a new
if (witnessPct >= 0.0f) { // rebuild starts from 0 rather than inheriting the previous phase's peak.
state_.sync.witness_progress = witnessPct / 100.0f; if (!state_.sync.building_witnesses) {
state_.sync.building_witnesses = true;
state_.sync.witness_progress = 0.0f;
state_.sync.witness_remaining = 0;
witness_rebuild_total_blocks_ = 0;
} }
if (witnessRemaining >= 0) { if (witnessRemaining >= 0) {
state_.sync.witness_remaining = witnessRemaining; state_.sync.witness_remaining = witnessRemaining;
// The peak "remaining" is the full span of the longest pass; derive a
// stable overall % from it. Per-call resets only shrink "remaining"
// relative to this peak, so the bar advances and never jumps back.
if (witnessRemaining > witness_rebuild_total_blocks_)
witness_rebuild_total_blocks_ = witnessRemaining;
if (witness_rebuild_total_blocks_ > 0) {
float p = 1.0f - static_cast<float>(witnessRemaining) /
static_cast<float>(witness_rebuild_total_blocks_);
if (p < 0.0f) p = 0.0f;
if (p > 1.0f) p = 1.0f;
if (p > state_.sync.witness_progress) // monotonic within the phase
state_.sync.witness_progress = p;
}
} else if (witnessPct >= 0.0f) {
// No "remaining" token (e.g. the initial per-tx pass) — fall back to the
// raw fraction, still clamped monotonic so it can't regress.
float p = witnessPct / 100.0f;
if (p > state_.sync.witness_progress)
state_.sync.witness_progress = p;
} }
if (!status.empty()) { if (!status.empty()) {
state_.sync.rescan_status = status; state_.sync.rescan_status = status;

View File

@@ -635,6 +635,11 @@ private:
// Set when a bootstrap completes; consumed once the daemon is connected to auto-run a rescan // Set when a bootstrap completes; consumed once the daemon is connected to auto-run a rescan
// that reconciles the preserved wallet.dat against the freshly-imported chain. // that reconciles the preserved wallet.dat against the freshly-imported chain.
bool post_bootstrap_rescan_pending_ = false; bool post_bootstrap_rescan_pending_ = false;
// Largest "blocks remaining" seen during the current witness-rebuild phase. The daemon's
// "Building Witnesses for block" fraction resets every call (it's re-invoked per connected
// block, each walking from its own start height to the tip), so we derive a stable, monotonic
// overall percentage from how far "remaining" has fallen below this peak. Reset per phase.
int witness_rebuild_total_blocks_ = 0;
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.

View File

@@ -1162,6 +1162,7 @@ void App::refreshCoreData()
state_.sync.building_witnesses = false; state_.sync.building_witnesses = false;
state_.sync.witness_progress = 0.0f; state_.sync.witness_progress = 0.0f;
state_.sync.witness_remaining = 0; state_.sync.witness_remaining = 0;
witness_rebuild_total_blocks_ = 0;
// Notes/witnesses were rebuilt — force a fresh history + balance pull. // Notes/witnesses were rebuilt — force a fresh history + balance pull.
transactions_dirty_ = true; transactions_dirty_ = true;
last_tx_block_height_ = -1; last_tx_block_height_ = -1;
@@ -2596,6 +2597,7 @@ void App::runtimeRescan(int startHeight)
state_.sync.building_witnesses = false; state_.sync.building_witnesses = false;
state_.sync.witness_progress = 0.0f; state_.sync.witness_progress = 0.0f;
state_.sync.witness_remaining = 0; state_.sync.witness_remaining = 0;
witness_rebuild_total_blocks_ = 0;
if (ok) { if (ok) {
state_.sync.rescan_progress = 1.0f; state_.sync.rescan_progress = 1.0f;
transactions_dirty_ = true; transactions_dirty_ = true;