fix(fullnode): make witness/rescan progress work on the real daemon

Verified by running the app against a live node and watching a real rescan. Three
issues that only surfaced at runtime:

- Wrong RPC name: this daemon (hush/komodo) exposes the runtime rescan as
  "rescan <height>", not bitcoin's "rescanblockchain". runtimeRescan() and
  RPCClient::rescanBlockchain() used the bitcoin name and failed with "Method not
  found" on every node. Corrected to "rescan".

- Witness/rescan progress never surfaced during a rescan: the daemon-output parser
  that drives it was gated behind rpcConnected, but a heavy rescan holds cs_main so
  getinfo times out and the RPC reads disconnected — silencing the parser exactly
  when it's needed. The parser reads the daemon's stdout pipe (no RPC), so it now
  runs whenever the daemon process is alive. It also now parses INLINE on the main
  thread instead of via fast_worker_, so it can't be starved when the worker is
  blocked on a getrescaninfo call (which waits on cs_main during a witness rebuild).

- Witness rebuild has TWO sub-phases with different scales — the initial-witness
  pass ("Setting Initial Sapling Witness for tx <hash>, <i> of <N>") and the cache
  walk ("Building Witnesses for block <h> <frac> complete, <n> remaining"). Tracking
  them with one monotonic value pinned the bar at the initial pass's ~100% through
  the whole cache walk. They're now tracked as distinct phases (witness_phase) with
  their own monotonic progress and labels ("Setting witnesses" vs "Rebuilding
  witnesses"), so neither resets/bounces and the long phase shows real movement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 13:00:36 -05:00
parent e2bc3623b6
commit 25ee1496b4
6 changed files with 134 additions and 49 deletions

View File

@@ -603,12 +603,21 @@ void App::update()
// Fast refresh (mining stats + daemon memory) every second // Fast refresh (mining stats + daemon memory) every second
// Skip when wallet is locked — no need to poll, and queued tasks // Skip when wallet is locked — no need to poll, and queued tasks
// would delay the PIN unlock worker task. // 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 (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 // 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. // 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. // 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(); refreshMiningInfo();
} }
@@ -650,9 +659,12 @@ void App::update()
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.building_witnesses = false;
state_.sync.witness_phase = 0;
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; 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. // 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) // Check daemon output for rescan progress (offloaded to worker)
if (daemon_controller_ && daemon_controller_->isRunning()) { if (daemon_controller_ && daemon_controller_->isRunning()) {
std::string newOutput = daemon_controller_->outputSince(daemon_output_offset_); std::string newOutput = daemon_controller_->outputSince(daemon_output_offset_);
if (!newOutput.empty() && fast_worker_) { if (!newOutput.empty()) {
fast_worker_->post([this, output = std::move(newOutput)]() -> rpc::RPCWorker::MainCb { // Parse inline on the main thread. It's cheap string scanning of a small incremental
// Parse on worker thread — pure string work, no shared state access // 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 foundRescan = false;
bool finished = false; bool finished = false;
float rescanPct = 0.0f; float rescanPct = 0.0f;
std::string lastStatus; std::string lastStatus;
// Sapling witness rebuild progress, logged as // Sapling witness rebuild progress. Two daemon signals:
// "Building Witnesses for block <h> <frac> complete, <n> remaining" // "Setting Initial Sapling Witness for tx <hash>, <i> of <N>" (the common one —
// (and an earlier "Setting Initial Sapling Witness for tx <hash>, <i> of <m>"). // one per wallet tx; <hash> is the stable per-tx key, <N> the total tx count)
// "Building Witnesses for block <h> <frac> complete, <n> remaining" (rarer)
bool foundWitness = false; bool foundWitness = false;
float witnessPct = -1.0f; // -1 = no fraction parsed this batch float witnessPct = -1.0f; // -1 = no fraction parsed this batch
int witnessRemaining = -1; int witnessRemaining = -1;
std::vector<std::string> witnessTxids; // distinct-tx keys seen this batch
int witnessTotalTxs = -1; // max N seen this batch
size_t pos = 0; size_t pos = 0;
while (pos < output.size()) { while (pos < output.size()) {
@@ -825,19 +844,37 @@ void App::update()
lastStatus = line; lastStatus = line;
} }
// Earlier per-tx phase: "Setting Initial Sapling Witness for tx <hash>, <i> of <m>". // Primary signal: "Setting Initial Sapling Witness for tx <hash>, <i> of <N>".
// Its <i> resets to 0 on every BuildWitnessCache call (re-invoked per block), so // <i> is the tx's slot in an unordered map (bounces — useless as progress), so
// it's not a reliable overall counter — use it only to mark that the witness // we key on <hash> (one per tx) and count distinct txs against <N>. Extract the
// phase is underway; the block-walk "remaining" below drives the percentage. // hash between "for tx " and the following comma, and N after " of ".
if (line.find("Setting Initial Sapling Witness") != std::string::npos) { {
foundWitness = true; auto setIdx = line.find("Setting Initial Sapling Witness for tx ");
lastStatus = line; 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 // Apply results directly — we are already on the main thread.
return [this, foundRescan, finished, rescanPct, foundWitness, witnessPct, const std::string& status = lastStatus;
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");
@@ -848,27 +885,43 @@ void App::update()
state_.sync.rescan_status.clear(); state_.sync.rescan_status.clear();
// Witness rebuild finishes with the rescan it's part of. // Witness rebuild finishes with the rescan it's part of.
state_.sync.building_witnesses = false; state_.sync.building_witnesses = false;
state_.sync.witness_phase = 0;
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; witness_rebuild_total_blocks_ = 0;
witness_seen_txids_.clear();
witness_total_txs_ = 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;
// First witness line of a phase → reset the per-phase tracking so a new state_.sync.building_witnesses = true;
// rebuild starts from 0 rather than inheriting the previous phase's peak.
if (!state_.sync.building_witnesses) { // Which sub-phase is this batch? A "<n> remaining" token means the
state_.sync.building_witnesses = true; // 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_progress = 0.0f;
state_.sync.witness_remaining = 0; state_.sync.witness_remaining = 0;
witness_rebuild_total_blocks_ = 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; 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_) if (witnessRemaining > witness_rebuild_total_blocks_)
witness_rebuild_total_blocks_ = witnessRemaining; witness_rebuild_total_blocks_ = witnessRemaining;
if (witness_rebuild_total_blocks_ > 0) { if (witness_rebuild_total_blocks_ > 0) {
@@ -879,12 +932,19 @@ void App::update()
if (p > state_.sync.witness_progress) // monotonic within the phase if (p > state_.sync.witness_progress) // monotonic within the phase
state_.sync.witness_progress = p; state_.sync.witness_progress = p;
} }
} else if (witnessPct >= 0.0f) { } else if (phase == 1) {
// No "remaining" token (e.g. the initial per-tx pass) — fall back to the // Initial pass: count DISTINCT witnessed txs / total. The set only
// raw fraction, still clamped monotonic so it can't regress. // grows (dedups the daemon's double-prints), so it's monotonic; the
float p = witnessPct / 100.0f; // raw "<i>" can't be used — it's an unordered-map slot that bounces.
if (p > state_.sync.witness_progress) if (witnessTotalTxs > witness_total_txs_) witness_total_txs_ = witnessTotalTxs;
state_.sync.witness_progress = p; for (const auto& txid : witnessTxids) witness_seen_txids_.insert(txid);
if (witness_total_txs_ > 0 && !witness_seen_txids_.empty()) {
float p = static_cast<float>(witness_seen_txids_.size()) /
static_cast<float>(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()) { if (!status.empty()) {
state_.sync.rescan_status = status; state_.sync.rescan_status = status;
@@ -902,8 +962,6 @@ void App::update()
state_.sync.rescan_status = status; state_.sync.rescan_status = status;
} }
} }
};
});
} }
} else if (!daemon_controller_ || !daemon_controller_->isRunning()) { } else if (!daemon_controller_ || !daemon_controller_->isRunning()) {
// Clear rescan state if daemon is not running (but preserve during restart) // Clear rescan state if daemon is not running (but preserve during restart)
@@ -1930,14 +1988,17 @@ void App::renderStatusBar()
ImGui::SameLine(0, sbSeparatorGap); ImGui::SameLine(0, sbSeparatorGap);
if (state_.sync.building_witnesses) { if (state_.sync.building_witnesses) {
// Witness rebuild is the tail phase of a rescan — show its own progress with priority. // 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 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) { if (state_.sync.witness_progress > 0.01f) {
ImGui::TextColored(witCol, TR("sb_building_witnesses_pct"), ImGui::TextColored(witCol, "%s %.0f%%", witLabel, state_.sync.witness_progress * 100.0f);
state_.sync.witness_progress * 100.0f);
} else { } else {
int dots = (int)(ImGui::GetTime() * 2.0f) % 4; int dots = (int)(ImGui::GetTime() * 2.0f) % 4;
const char* dotStr = (dots == 0) ? "." : (dots == 1) ? ".." : (dots == 2) ? "..." : ""; 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) { } else if (state_.sync.rescanning) {
// Show rescan progress (takes priority over sync) // Show rescan progress (takes priority over sync)
@@ -3730,11 +3791,13 @@ void App::renderLoadingOverlay(float contentH)
curY += barH + gap; curY += barH + gap;
char wbuf[128]; char wbuf[128];
if (progress > 0.01f && state_.sync.witness_remaining > 0) if (state_.sync.witness_phase == 2 && progress > 0.01f && state_.sync.witness_remaining > 0)
snprintf(wbuf, sizeof(wbuf), "Rebuilding Sapling witnesses %.0f%% — %d blocks left", snprintf(wbuf, sizeof(wbuf), "Rebuilding witness cache %.0f%% — %d blocks left",
progress * 100.0f, state_.sync.witness_remaining); 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) 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 else
snprintf(wbuf, sizeof(wbuf), "Rebuilding Sapling note witnesses…"); snprintf(wbuf, sizeof(wbuf), "Rebuilding Sapling note witnesses…");
ImFont* capFont = Type().caption(); ImFont* capFont = Type().caption();

View File

@@ -640,6 +640,13 @@ private:
// block, each walking from its own start height to the tip), so we derive a stable, monotonic // 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. // overall percentage from how far "remaining" has fallen below this peak. Reset per phase.
int witness_rebuild_total_blocks_ = 0; int witness_rebuild_total_blocks_ = 0;
// The daemon's primary witness signal is "Setting Initial Sapling Witness for tx <hash>, <i>
// of <N>", logged once per wallet tx as its initial witness is set. The <i> 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<std::string> witness_seen_txids_;
int witness_total_txs_ = 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

@@ -1160,9 +1160,12 @@ void App::refreshCoreData()
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.building_witnesses = false;
state_.sync.witness_phase = 0;
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; witness_rebuild_total_blocks_ = 0;
witness_seen_txids_.clear();
witness_total_txs_ = 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;
@@ -2584,7 +2587,7 @@ void App::runtimeRescan(int startHeight)
std::string err; std::string err;
try { try {
rpc::RPCClient::TraceScope trace("Settings / Runtime rescan"); rpc::RPCClient::TraceScope trace("Settings / Runtime rescan");
rpc_->call("rescanblockchain", {startHeight}); // blocks until the scan finishes rpc_->call("rescan", {startHeight}); // hush "rescan <height>"; blocks until the scan finishes
ok = true; ok = true;
} catch (const std::exception& e) { } catch (const std::exception& e) {
err = e.what(); err = e.what();
@@ -2595,9 +2598,12 @@ void App::runtimeRescan(int startHeight)
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.building_witnesses = false;
state_.sync.witness_phase = 0;
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; witness_rebuild_total_blocks_ = 0;
witness_seen_txids_.clear();
witness_total_txs_ = 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,12 +127,18 @@ 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 // Sapling note witness rebuild — a distinct, often-long phase after a rescan/zap. The daemon
// wallet's note witnesses, logged as "Building Witnesses for block ..."). Tracked separately // reports it in TWO sub-phases with different signals, so we track which is active:
// from rescan_progress because it's a distinct, often-long phase with its own progress. // 1 = initial pass ("Setting Initial Sapling Witness for tx <hash>, <i> of <N>") — progress
// is distinct-txs-witnessed / N (the <i> bounces, so it can't be used directly).
// 2 = witness-cache walk ("Building Witnesses for block <h> <frac> complete, <n> 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; bool building_witnesses = false;
float witness_progress = 0.0f; // 0.0 - 1.0 int witness_phase = 0; // 0 none, 1 initial-witness pass, 2 witness-cache walk
int witness_remaining = 0; // blocks left to build witnesses for (0 if unknown) 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; } bool isSynced() const { return !syncing && blocks > 0 && blocks >= headers - 2; }
}; };

View File

@@ -668,7 +668,8 @@ void RPCClient::stop(Callback cb, ErrorCallback err)
void RPCClient::rescanBlockchain(int startHeight, 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 <height>", not bitcoin's "rescanblockchain".
doRPC("rescan", {startHeight}, cb, err);
} }
void RPCClient::z_validateAddress(const std::string& address, Callback cb, ErrorCallback 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) void RPCClient::rescanBlockchain(int startHeight, UnifiedCallback cb)
{ {
doRPC("rescanblockchain", {startHeight}, // hush/komodo daemons expose this as "rescan <height>", not bitcoin's "rescanblockchain".
doRPC("rescan", {startHeight},
[cb](const json& result) { [cb](const json& result) {
if (cb) cb(result, ""); if (cb) cb(result, "");
}, },

View File

@@ -818,7 +818,8 @@ void I18n::loadBuiltinEnglish()
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_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_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...";