From b32fe07cb1d75ebe1185fc9628c3a6e965cee483 Mon Sep 17 00:00:00 2001 From: DanS Date: Sun, 21 Jun 2026 12:23:52 -0500 Subject: [PATCH] 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 complete, remaining" (and the earlier "Setting Initial Sapling Witness for tx ..., of ") 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) --- src/app.cpp | 130 +++++++++++++++++++++++++++++++++++++++- src/app_network.cpp | 6 ++ src/data/wallet_state.h | 9 ++- src/util/i18n.cpp | 2 + 4 files changed, 143 insertions(+), 4 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index 5dc7a89..190e2cc 100644 --- a/src/app.cpp +++ b/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 complete, remaining" + // (and an earlier "Setting Initial Sapling Witness for tx , of "). + 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 + // complete, 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 , of ". + 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) // ------------------------------------------------------------------- diff --git a/src/app_network.cpp b/src/app_network.cpp index c0de2f1..607f643 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -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; diff --git a/src/data/wallet_state.h b/src/data/wallet_state.h index 08c8745..d1bb6c7 100644 --- a/src/data/wallet_state.h +++ b/src/data/wallet_state.h @@ -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; } }; diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 1fc56cf..f88a7a1 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -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...";