feat(fullnode): show Sapling note witness-rebuild progress

The daemon's post-rescan witness rebuild ("Building Witnesses for block ...")
is a distinct, often-long phase that previously showed only as an indeterminate
"Rescanning..." with no progress. Parse the daemon's witness-build log lines and
surface a dedicated progress indicator.

- Parse "Building Witnesses for block <h> <frac> complete, <n> remaining" (and the
  earlier "Setting Initial Sapling Witness for tx ..., <i> of <m>") from daemon
  output, extracting a 0..1 fraction and remaining-block count.
- New SyncInfo fields building_witnesses / witness_progress / witness_remaining,
  cleared at every rescan-completion site (warmup-end, getrescaninfo poll, runtime
  rescan callback, daemon-log "finished").
- Status bar shows "Rebuilding witnesses NN%" (priority over the generic rescan
  text); the loading overlay (shown during -rescan warmup) gets a labelled witness
  progress bar with the remaining-block count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-21 12:23:52 -05:00
parent a0532275dd
commit b32fe07cb1
4 changed files with 143 additions and 4 deletions

View File

@@ -649,6 +649,9 @@ void App::update()
rescan_confirmed_active_ = 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();
state_.sync.building_witnesses = false;
state_.sync.witness_progress = 0.0f;
state_.sync.witness_remaining = 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.
}; };
@@ -722,6 +725,13 @@ void App::update()
float rescanPct = 0.0f; float rescanPct = 0.0f;
std::string lastStatus; std::string lastStatus;
// Sapling witness rebuild progress, logged as
// "Building Witnesses for block <h> <frac> complete, <n> remaining"
// (and an earlier "Setting Initial Sapling Witness for tx <hash>, <i> of <m>").
bool foundWitness = false;
float witnessPct = -1.0f; // -1 = no fraction parsed this batch
int witnessRemaining = -1;
size_t pos = 0; size_t pos = 0;
while (pos < output.size()) { while (pos < output.size()) {
size_t eol = output.find('\n', pos); size_t eol = output.find('\n', pos);
@@ -788,10 +798,57 @@ void App::update()
finished = true; finished = true;
} }
} }
// Sapling note witness rebuild: "Building Witnesses for block <h> <frac>
// complete, <n> remaining". The float before "complete" is a 0..1 fraction.
auto witIdx = line.find("Building Witnesses for block");
if (witIdx != std::string::npos) {
foundWitness = true;
auto compIdx = line.find(" complete", witIdx);
if (compIdx != std::string::npos) {
size_t numEnd = compIdx, numStart = numEnd;
while (numStart > 0 && (std::isdigit((unsigned char)line[numStart - 1]) ||
line[numStart - 1] == '.')) numStart--;
if (numStart < numEnd) {
try { witnessPct = std::stof(line.substr(numStart, numEnd - numStart)) * 100.0f; } catch (...) {}
}
}
auto remIdx = line.find(" remaining", witIdx);
if (remIdx != std::string::npos) {
size_t numEnd = remIdx, numStart = numEnd;
while (numStart > 0 && std::isdigit((unsigned char)line[numStart - 1])) numStart--;
if (numStart < numEnd) {
try { witnessRemaining = std::stoi(line.substr(numStart, numEnd - numStart)); } catch (...) {}
}
}
lastStatus = line;
}
// Earlier per-tx phase: "Setting Initial Sapling Witness for tx <hash>, <i> of <m>".
auto setIdx = line.find("Setting Initial Sapling Witness");
if (setIdx != std::string::npos) {
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;
}
} }
// Return callback to apply results on main thread // Return callback to apply results on main thread
return [this, foundRescan, finished, rescanPct, status = std::move(lastStatus)]() { return [this, foundRescan, finished, rescanPct, foundWitness, witnessPct,
witnessRemaining, status = std::move(lastStatus)]() {
if (finished) { if (finished) {
if (state_.sync.rescanning) { if (state_.sync.rescanning) {
ui::Notifications::instance().success("Blockchain rescan complete"); ui::Notifications::instance().success("Blockchain rescan complete");
@@ -800,6 +857,25 @@ void App::update()
rescan_confirmed_active_ = 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();
// Witness rebuild finishes with the rescan it's part of.
state_.sync.building_witnesses = false;
state_.sync.witness_progress = 0.0f;
state_.sync.witness_remaining = 0;
} else if (foundWitness) {
// Witness rebuild is the tail phase of a rescan — keep rescanning set so
// the broader gating holds, but surface the witness-specific progress.
state_.sync.rescanning = true;
rescan_confirmed_active_ = true;
state_.sync.building_witnesses = true;
if (witnessPct >= 0.0f) {
state_.sync.witness_progress = witnessPct / 100.0f;
}
if (witnessRemaining >= 0) {
state_.sync.witness_remaining = witnessRemaining;
}
if (!status.empty()) {
state_.sync.rescan_status = status;
}
} else if (foundRescan) { } else if (foundRescan) {
state_.sync.rescanning = true; state_.sync.rescanning = true;
// Reading "Still rescanning" straight from the daemon log is hard proof the // Reading "Still rescanning" straight from the daemon log is hard proof the
@@ -1839,7 +1915,18 @@ void App::renderStatusBar()
ImGui::SameLine(0, sbSectionGap); ImGui::SameLine(0, sbSectionGap);
ImGui::TextDisabled("|"); ImGui::TextDisabled("|");
ImGui::SameLine(0, sbSeparatorGap); ImGui::SameLine(0, sbSeparatorGap);
if (state_.sync.rescanning) { if (state_.sync.building_witnesses) {
// Witness rebuild is the tail phase of a rescan — show its own progress with priority.
const ImVec4 witCol(0.6f, 0.8f, 1.0f, 1.0f);
if (state_.sync.witness_progress > 0.01f) {
ImGui::TextColored(witCol, TR("sb_building_witnesses_pct"),
state_.sync.witness_progress * 100.0f);
} else {
int dots = (int)(ImGui::GetTime() * 2.0f) % 4;
const char* dotStr = (dots == 0) ? "." : (dots == 1) ? ".." : (dots == 2) ? "..." : "";
ImGui::TextColored(witCol, "%s%s", TR("sb_building_witnesses"), dotStr);
}
} else if (state_.sync.rescanning) {
// Show rescan progress (takes priority over sync) // Show rescan progress (takes priority over sync)
// Use animated dots if progress is unknown (0%) // Use animated dots if progress is unknown (0%)
if (state_.sync.rescan_progress > 0.01f) { if (state_.sync.rescan_progress > 0.01f) {
@@ -3609,6 +3696,43 @@ void App::renderLoadingOverlay(float contentH)
curY += ts.y + gap; curY += ts.y + gap;
} }
// -------------------------------------------------------------------
// 2c. Sapling note witness rebuild progress (post-rescan phase, runs inside warmup)
// -------------------------------------------------------------------
if (state_.sync.building_witnesses) {
float progress = state_.sync.witness_progress;
if (progress < 0.0f) progress = 0.0f;
if (progress > 1.0f) progress = 1.0f;
float barRadius = loadElem("progress-bar", 3.0f);
float barX = wp.x + cx - barW * 0.5f;
ImVec2 barMin(barX, curY);
ImVec2 barMax(barX + barW, curY + barH);
dl->AddRectFilled(barMin, barMax,
ui::schema::UI().resolveColor("var(--progress-track)", IM_COL32(255, 255, 255, 30)), barRadius);
ImVec2 fillMax(barMin.x + barW * progress, barMax.y);
if (fillMax.x > barMin.x + 1.0f) {
dl->AddRectFilled(barMin, fillMax,
ui::schema::UI().resolveColor("var(--primary)", IM_COL32(255, 218, 0, 200)), barRadius);
}
curY += barH + gap;
char wbuf[128];
if (progress > 0.01f && state_.sync.witness_remaining > 0)
snprintf(wbuf, sizeof(wbuf), "Rebuilding Sapling witnesses %.0f%% — %d blocks left",
progress * 100.0f, state_.sync.witness_remaining);
else if (progress > 0.01f)
snprintf(wbuf, sizeof(wbuf), "Rebuilding Sapling witnesses %.0f%%", progress * 100.0f);
else
snprintf(wbuf, sizeof(wbuf), "Rebuilding Sapling note witnesses…");
ImFont* capFont = Type().caption();
if (!capFont) capFont = ImGui::GetFont();
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, wbuf);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(wp.x + cx - ts.x * 0.5f, curY),
IM_COL32(150, 150, 150, 255), wbuf);
curY += ts.y + gap;
}
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// 3. Sync progress bar (if connected and syncing) // 3. Sync progress bar (if connected and syncing)
// ------------------------------------------------------------------- // -------------------------------------------------------------------

View File

@@ -1159,6 +1159,9 @@ void App::refreshCoreData()
rescan_confirmed_active_ = 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();
state_.sync.building_witnesses = false;
state_.sync.witness_progress = 0.0f;
state_.sync.witness_remaining = 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;
@@ -2590,6 +2593,9 @@ void App::runtimeRescan(int startHeight)
rescan_confirmed_active_ = false; rescan_confirmed_active_ = false;
state_.sync.rescanning = false; state_.sync.rescanning = false;
state_.sync.rescan_status.clear(); state_.sync.rescan_status.clear();
state_.sync.building_witnesses = false;
state_.sync.witness_progress = 0.0f;
state_.sync.witness_remaining = 0;
if (ok) { if (ok) {
state_.sync.rescan_progress = 1.0f; state_.sync.rescan_progress = 1.0f;
transactions_dirty_ = true; transactions_dirty_ = true;

View File

@@ -127,6 +127,13 @@ struct SyncInfo {
float rescan_progress = 0.0f; // 0.0 - 1.0 float rescan_progress = 0.0f; // 0.0 - 1.0
std::string rescan_status; // e.g. "Rescanning... 25%" std::string rescan_status; // e.g. "Rescanning... 25%"
// Sapling note witness rebuild (the phase after a rescan/zap where the daemon rebuilds the
// wallet's note witnesses, logged as "Building Witnesses for block ..."). Tracked separately
// from rescan_progress because it's a distinct, often-long phase with its own progress.
bool building_witnesses = false;
float witness_progress = 0.0f; // 0.0 - 1.0
int witness_remaining = 0; // blocks left to build witnesses for (0 if unknown)
bool isSynced() const { return !syncing && blocks > 0 && blocks >= headers - 2; } bool isSynced() const { return !syncing && blocks > 0 && blocks >= headers - 2; }
}; };

View File

@@ -817,6 +817,8 @@ void I18n::loadBuiltinEnglish()
strings_["sb_syncing_basic"] = "Syncing %.1f%% (%d left)"; strings_["sb_syncing_basic"] = "Syncing %.1f%% (%d left)";
strings_["sb_rescanning_pct"] = "Rescanning %.0f%%"; strings_["sb_rescanning_pct"] = "Rescanning %.0f%%";
strings_["sb_rescanning"] = "Rescanning"; strings_["sb_rescanning"] = "Rescanning";
strings_["sb_building_witnesses_pct"] = "Rebuilding witnesses %.0f%%";
strings_["sb_building_witnesses"] = "Rebuilding witnesses";
strings_["sb_importing_keys"] = "Importing keys"; strings_["sb_importing_keys"] = "Importing keys";
strings_["sb_daemon_not_found"] = "Daemon not found"; strings_["sb_daemon_not_found"] = "Daemon not found";
strings_["sb_loading_config"] = "Loading configuration..."; strings_["sb_loading_config"] = "Loading configuration...";