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:
130
src/app.cpp
130
src/app.cpp
@@ -649,6 +649,9 @@ void App::update()
|
||||
rescan_confirmed_active_ = false;
|
||||
state_.sync.rescan_progress = 1.0f;
|
||||
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.
|
||||
};
|
||||
@@ -722,6 +725,13 @@ void App::update()
|
||||
float rescanPct = 0.0f;
|
||||
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;
|
||||
while (pos < output.size()) {
|
||||
size_t eol = output.find('\n', pos);
|
||||
@@ -788,10 +798,57 @@ void App::update()
|
||||
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 [this, foundRescan, finished, rescanPct, status = std::move(lastStatus)]() {
|
||||
return [this, foundRescan, finished, rescanPct, foundWitness, witnessPct,
|
||||
witnessRemaining, status = std::move(lastStatus)]() {
|
||||
if (finished) {
|
||||
if (state_.sync.rescanning) {
|
||||
ui::Notifications::instance().success("Blockchain rescan complete");
|
||||
@@ -800,6 +857,25 @@ void App::update()
|
||||
rescan_confirmed_active_ = false;
|
||||
state_.sync.rescan_progress = 1.0f;
|
||||
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) {
|
||||
state_.sync.rescanning = true;
|
||||
// Reading "Still rescanning" straight from the daemon log is hard proof the
|
||||
@@ -1839,11 +1915,22 @@ void App::renderStatusBar()
|
||||
ImGui::SameLine(0, sbSectionGap);
|
||||
ImGui::TextDisabled("|");
|
||||
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)
|
||||
// Use animated dots if progress is unknown (0%)
|
||||
if (state_.sync.rescan_progress > 0.01f) {
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), TR("sb_rescanning_pct"),
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), TR("sb_rescanning_pct"),
|
||||
state_.sync.rescan_progress * 100.0f);
|
||||
} else {
|
||||
// Animated "Rescanning..." with pulsing dots
|
||||
@@ -3609,6 +3696,43 @@ void App::renderLoadingOverlay(float contentH)
|
||||
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)
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -1159,6 +1159,9 @@ void App::refreshCoreData()
|
||||
rescan_confirmed_active_ = false;
|
||||
state_.sync.rescan_progress = 1.0f;
|
||||
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.
|
||||
transactions_dirty_ = true;
|
||||
last_tx_block_height_ = -1;
|
||||
@@ -2590,6 +2593,9 @@ void App::runtimeRescan(int startHeight)
|
||||
rescan_confirmed_active_ = false;
|
||||
state_.sync.rescanning = false;
|
||||
state_.sync.rescan_status.clear();
|
||||
state_.sync.building_witnesses = false;
|
||||
state_.sync.witness_progress = 0.0f;
|
||||
state_.sync.witness_remaining = 0;
|
||||
if (ok) {
|
||||
state_.sync.rescan_progress = 1.0f;
|
||||
transactions_dirty_ = true;
|
||||
|
||||
@@ -126,7 +126,14 @@ struct SyncInfo {
|
||||
bool rescanning = false;
|
||||
float rescan_progress = 0.0f; // 0.0 - 1.0
|
||||
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; }
|
||||
};
|
||||
|
||||
|
||||
@@ -817,6 +817,8 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["sb_syncing_basic"] = "Syncing %.1f%% (%d left)";
|
||||
strings_["sb_rescanning_pct"] = "Rescanning %.0f%%";
|
||||
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_daemon_not_found"] = "Daemon not found";
|
||||
strings_["sb_loading_config"] = "Loading configuration...";
|
||||
|
||||
Reference in New Issue
Block a user