diff --git a/src/app.cpp b/src/app.cpp index 2783031..dc1aefa 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -603,12 +603,21 @@ void App::update() // Fast refresh (mining stats + daemon memory) every second // Skip when wallet is locked — no need to poll, and queued tasks // would delay the PIN unlock worker task. + // + // Also enter this block when the embedded daemon is running even if RPC currently reads + // disconnected: during a long rescan the daemon holds cs_main, so getinfo times out and the + // socket is marked down — but the rescan/witness PROGRESS we parse comes from the daemon's + // stdout pipe (no RPC needed), and getrescaninfo still answers (it takes no lock). Without this, + // the daemon-output parser stopped exactly when a rescan started, so witness progress never + // surfaced. The RPC-dependent pollers inside stay individually gated (mining skips during + // warmup/rescan; the getrescaninfo poll only runs once rescanning is flagged). if (network_refresh_.consumeDue(RefreshTimer::Fast)) { - if (rpcConnected && !state_.isLocked()) { + const bool daemonAlive = daemon_controller_ && daemon_controller_->isRunning(); + if ((rpcConnected || daemonAlive) && !state_.isLocked()) { // Skip the mining poll while the daemon is in warmup (e.g. during -rescan). Otherwise // getmininginfo is rejected with -28 ("Rescanning...") every second and floods the log. // The rescan progress poll below still runs — that's how we track the warmup/rescan. - if (!state_.warming_up && !runtime_rescan_active_) { + if (rpcConnected && !state_.warming_up && !runtime_rescan_active_) { refreshMiningInfo(); } @@ -650,9 +659,12 @@ void App::update() state_.sync.rescan_progress = 1.0f; state_.sync.rescan_status.clear(); state_.sync.building_witnesses = false; + state_.sync.witness_phase = 0; state_.sync.witness_progress = 0.0f; state_.sync.witness_remaining = 0; witness_rebuild_total_blocks_ = 0; + witness_seen_txids_.clear(); + witness_total_txs_ = 0; } // else: rescanning=false but not yet confirmed → pre-restart daemon; keep waiting. }; @@ -718,20 +730,27 @@ void App::update() // Check daemon output for rescan progress (offloaded to worker) if (daemon_controller_ && daemon_controller_->isRunning()) { std::string newOutput = daemon_controller_->outputSince(daemon_output_offset_); - if (!newOutput.empty() && fast_worker_) { - fast_worker_->post([this, output = std::move(newOutput)]() -> rpc::RPCWorker::MainCb { - // Parse on worker thread — pure string work, no shared state access + if (!newOutput.empty()) { + // Parse inline on the main thread. It's cheap string scanning of a small incremental + // buffer, and keeping it OFF the worker is essential: during a rescan the worker can + // be blocked for minutes on a getrescaninfo call (it waits on cs_main while the daemon + // holds it for a witness rebuild). On the worker, the parse queued behind that and the + // rescan/witness progress froze. Inline, progress updates every frame regardless. + const std::string output = std::move(newOutput); bool foundRescan = false; bool finished = false; 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 "). + // Sapling witness rebuild progress. Two daemon signals: + // "Setting Initial Sapling Witness for tx , of " (the common one — + // one per wallet tx; is the stable per-tx key, the total tx count) + // "Building Witnesses for block complete, remaining" (rarer) bool foundWitness = false; float witnessPct = -1.0f; // -1 = no fraction parsed this batch int witnessRemaining = -1; + std::vector witnessTxids; // distinct-tx keys seen this batch + int witnessTotalTxs = -1; // max N seen this batch size_t pos = 0; while (pos < output.size()) { @@ -825,19 +844,37 @@ void App::update() lastStatus = line; } - // Earlier per-tx phase: "Setting Initial Sapling Witness for tx , of ". - // Its resets to 0 on every BuildWitnessCache call (re-invoked per block), so - // 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; - lastStatus = line; + // Primary signal: "Setting Initial Sapling Witness for tx , of ". + // is the tx's slot in an unordered map (bounces — useless as progress), so + // we key on (one per tx) and count distinct txs against . Extract the + // hash between "for tx " and the following comma, and N after " of ". + { + auto setIdx = line.find("Setting Initial Sapling Witness for tx "); + if (setIdx != std::string::npos) { + foundWitness = true; + size_t hStart = setIdx + std::string("Setting Initial Sapling Witness for tx ").size(); + size_t comma = line.find(',', hStart); + if (comma != std::string::npos && comma > hStart) { + witnessTxids.push_back(line.substr(hStart, comma - hStart)); + auto ofIdx = line.find(" of ", comma); + if (ofIdx != std::string::npos) { + size_t nStart = ofIdx + 4, nEnd = nStart; + while (nEnd < line.size() && std::isdigit((unsigned char)line[nEnd])) nEnd++; + if (nEnd > nStart) { + try { + int n = std::stoi(line.substr(nStart, nEnd - nStart)); + if (n > witnessTotalTxs) witnessTotalTxs = n; + } catch (...) {} + } + } + } + lastStatus = line; + } } } - // Return callback to apply results on main thread - return [this, foundRescan, finished, rescanPct, foundWitness, witnessPct, - witnessRemaining, status = std::move(lastStatus)]() { + // Apply results directly — we are already on the main thread. + const std::string& status = lastStatus; if (finished) { if (state_.sync.rescanning) { ui::Notifications::instance().success("Blockchain rescan complete"); @@ -848,27 +885,43 @@ void App::update() state_.sync.rescan_status.clear(); // Witness rebuild finishes with the rescan it's part of. state_.sync.building_witnesses = false; + state_.sync.witness_phase = 0; state_.sync.witness_progress = 0.0f; state_.sync.witness_remaining = 0; witness_rebuild_total_blocks_ = 0; + witness_seen_txids_.clear(); + witness_total_txs_ = 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; - // First witness line of a phase → reset the per-phase tracking so a new - // rebuild starts from 0 rather than inheriting the previous phase's peak. - if (!state_.sync.building_witnesses) { - state_.sync.building_witnesses = true; + state_.sync.building_witnesses = true; + + // Which sub-phase is this batch? A " remaining" token means the + // cache walk (phase 2); otherwise per-tx "Setting Initial" lines are the + // initial pass (phase 1). When both appear in one batch the walk wins — + // it always runs after the initial pass within a BuildWitnessCache call. + const int phase = (witnessRemaining >= 0) ? 2 + : (!witnessTxids.empty() ? 1 : state_.sync.witness_phase); + + // Entering a new sub-phase → reset its progress + accumulators so the bar + // restarts from 0 instead of inheriting the previous phase's value. + if (phase != state_.sync.witness_phase) { + state_.sync.witness_phase = phase; state_.sync.witness_progress = 0.0f; state_.sync.witness_remaining = 0; witness_rebuild_total_blocks_ = 0; + witness_seen_txids_.clear(); + witness_total_txs_ = 0; } - if (witnessRemaining >= 0) { + + if (phase == 2) { + // Cache walk: derive a stable % from how far "remaining" has fallen + // from its peak (the first/largest value = the full walk span). The + // daemon re-invokes the walk per block, so "remaining" sawtooths; the + // peak anchor + monotonic clamp keep the bar advancing, never resetting. 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) { @@ -879,12 +932,19 @@ void App::update() 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; + } else if (phase == 1) { + // Initial pass: count DISTINCT witnessed txs / total. The set only + // grows (dedups the daemon's double-prints), so it's monotonic; the + // raw "" can't be used — it's an unordered-map slot that bounces. + if (witnessTotalTxs > witness_total_txs_) witness_total_txs_ = witnessTotalTxs; + for (const auto& txid : witnessTxids) witness_seen_txids_.insert(txid); + if (witness_total_txs_ > 0 && !witness_seen_txids_.empty()) { + float p = static_cast(witness_seen_txids_.size()) / + static_cast(witness_total_txs_); + if (p > 1.0f) p = 1.0f; + if (p > state_.sync.witness_progress) // monotonic within the phase + state_.sync.witness_progress = p; + } } if (!status.empty()) { state_.sync.rescan_status = status; @@ -902,8 +962,6 @@ void App::update() state_.sync.rescan_status = status; } } - }; - }); } } else if (!daemon_controller_ || !daemon_controller_->isRunning()) { // Clear rescan state if daemon is not running (but preserve during restart) @@ -1930,14 +1988,17 @@ void App::renderStatusBar() ImGui::SameLine(0, sbSeparatorGap); if (state_.sync.building_witnesses) { // Witness rebuild is the tail phase of a rescan — show its own progress with priority. + // Phase 1 (initial pass) and phase 2 (cache walk) have distinct labels so the progress + // restart at the phase boundary reads as a new step rather than a glitch. const ImVec4 witCol(0.6f, 0.8f, 1.0f, 1.0f); + const char* witLabel = (state_.sync.witness_phase == 2) + ? TR("sb_witness_cache") : TR("sb_building_witnesses"); if (state_.sync.witness_progress > 0.01f) { - ImGui::TextColored(witCol, TR("sb_building_witnesses_pct"), - state_.sync.witness_progress * 100.0f); + ImGui::TextColored(witCol, "%s %.0f%%", witLabel, 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); + ImGui::TextColored(witCol, "%s%s", witLabel, dotStr); } } else if (state_.sync.rescanning) { // Show rescan progress (takes priority over sync) @@ -3730,11 +3791,13 @@ void App::renderLoadingOverlay(float contentH) 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", + if (state_.sync.witness_phase == 2 && progress > 0.01f && state_.sync.witness_remaining > 0) + snprintf(wbuf, sizeof(wbuf), "Rebuilding witness cache %.0f%% — %d blocks left", progress * 100.0f, state_.sync.witness_remaining); + else if (state_.sync.witness_phase == 2 && progress > 0.01f) + snprintf(wbuf, sizeof(wbuf), "Rebuilding witness cache %.0f%%", progress * 100.0f); else if (progress > 0.01f) - snprintf(wbuf, sizeof(wbuf), "Rebuilding Sapling witnesses %.0f%%", progress * 100.0f); + snprintf(wbuf, sizeof(wbuf), "Setting initial Sapling witnesses %.0f%%", progress * 100.0f); else snprintf(wbuf, sizeof(wbuf), "Rebuilding Sapling note witnesses…"); ImFont* capFont = Type().caption(); diff --git a/src/app.h b/src/app.h index 002e3ba..3f6c7bc 100644 --- a/src/app.h +++ b/src/app.h @@ -640,6 +640,13 @@ private: // 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; + // The daemon's primary witness signal is "Setting Initial Sapling Witness for tx , + // of ", logged once per wallet tx as its initial witness is set. The is the tx's slot in + // an UNORDERED map, so it bounces wildly (was the cause of the resetting progress). The honest + // monotonic metric is how many DISTINCT txs have been witnessed (the set only grows; it also + // dedups the daemon's occasional double-prints) over the reported total N. + std::unordered_set witness_seen_txids_; + int witness_total_txs_ = 0; bool opid_poll_in_progress_ = false; // Consecutive Core-refresh cycles where BOTH core RPCs failed → likely a dead // connection. After kCoreFailuresBeforeDisconnect, tear down and reconnect. diff --git a/src/app_network.cpp b/src/app_network.cpp index 26d71d9..7b74cfd 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -1160,9 +1160,12 @@ void App::refreshCoreData() state_.sync.rescan_progress = 1.0f; state_.sync.rescan_status.clear(); state_.sync.building_witnesses = false; + state_.sync.witness_phase = 0; state_.sync.witness_progress = 0.0f; state_.sync.witness_remaining = 0; witness_rebuild_total_blocks_ = 0; + witness_seen_txids_.clear(); + witness_total_txs_ = 0; // Notes/witnesses were rebuilt — force a fresh history + balance pull. transactions_dirty_ = true; last_tx_block_height_ = -1; @@ -2584,7 +2587,7 @@ void App::runtimeRescan(int startHeight) std::string err; try { rpc::RPCClient::TraceScope trace("Settings / Runtime rescan"); - rpc_->call("rescanblockchain", {startHeight}); // blocks until the scan finishes + rpc_->call("rescan", {startHeight}); // hush "rescan "; blocks until the scan finishes ok = true; } catch (const std::exception& e) { err = e.what(); @@ -2595,9 +2598,12 @@ void App::runtimeRescan(int startHeight) state_.sync.rescanning = false; state_.sync.rescan_status.clear(); state_.sync.building_witnesses = false; + state_.sync.witness_phase = 0; state_.sync.witness_progress = 0.0f; state_.sync.witness_remaining = 0; witness_rebuild_total_blocks_ = 0; + witness_seen_txids_.clear(); + witness_total_txs_ = 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 d1bb6c7..b6192f9 100644 --- a/src/data/wallet_state.h +++ b/src/data/wallet_state.h @@ -127,12 +127,18 @@ struct SyncInfo { 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. + // Sapling note witness rebuild — a distinct, often-long phase after a rescan/zap. The daemon + // reports it in TWO sub-phases with different signals, so we track which is active: + // 1 = initial pass ("Setting Initial Sapling Witness for tx , of ") — progress + // is distinct-txs-witnessed / N (the bounces, so it can't be used directly). + // 2 = witness-cache walk ("Building Witnesses for block complete, remaining") + // — progress derived from how far "remaining" has fallen from its per-phase peak. + // The two are sequential with different scales, so progress is NOT carried across the boundary + // (that would pin the bar at the initial pass's ~100% through the whole cache walk). 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) + int witness_phase = 0; // 0 none, 1 initial-witness pass, 2 witness-cache walk + float witness_progress = 0.0f; // 0.0 - 1.0, within the current sub-phase + int witness_remaining = 0; // blocks left in the cache walk (0 if unknown / phase 1) bool isSynced() const { return !syncing && blocks > 0 && blocks >= headers - 2; } }; diff --git a/src/rpc/rpc_client.cpp b/src/rpc/rpc_client.cpp index 2cb5a62..c2fc683 100644 --- a/src/rpc/rpc_client.cpp +++ b/src/rpc/rpc_client.cpp @@ -668,7 +668,8 @@ void RPCClient::stop(Callback cb, ErrorCallback err) void RPCClient::rescanBlockchain(int startHeight, Callback cb, ErrorCallback err) { - doRPC("rescanblockchain", {startHeight}, cb, err); + // hush/komodo daemons expose this as "rescan ", not bitcoin's "rescanblockchain". + doRPC("rescan", {startHeight}, cb, err); } void RPCClient::z_validateAddress(const std::string& address, Callback cb, ErrorCallback err) @@ -776,7 +777,8 @@ void RPCClient::getInfo(UnifiedCallback cb) void RPCClient::rescanBlockchain(int startHeight, UnifiedCallback cb) { - doRPC("rescanblockchain", {startHeight}, + // hush/komodo daemons expose this as "rescan ", not bitcoin's "rescanblockchain". + doRPC("rescan", {startHeight}, [cb](const json& result) { if (cb) cb(result, ""); }, diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index f88a7a1..a877b3b 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -818,7 +818,8 @@ void I18n::loadBuiltinEnglish() 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_building_witnesses"] = "Setting witnesses"; + strings_["sb_witness_cache"] = "Rebuilding witnesses"; strings_["sb_importing_keys"] = "Importing keys"; strings_["sb_daemon_not_found"] = "Daemon not found"; strings_["sb_loading_config"] = "Loading configuration...";