fix(rescan): probe the node's block range off the UI thread
Finding the lowest block a node still has (Settings > Rescan, and the automatic reconcile after a bootstrap) ran a binary search of about 42 synchronous getblockhash/getblock calls on the UI thread, each waiting for the RPC connection behind any wallet scan (10-20 s on a big wallet) and each with a 30 s timeout. The window froze for that long. It now runs as one task on the wallet RPC worker, bounded to 90 s, and the answer comes back on the UI thread; meanwhile the dialog shows "Checking which blocks..." with Rescan disabled. An answer from an earlier connection counts as "could not tell", and one for an earlier opening of the dialog is dropped. The search probes block 1 first, so a full-history node answers in one probe, and it answers "could not tell" at once while a runtime rescan holds the lane. Fixed along the way: any probe error (timeout, busy node) read as "block missing", so a busy node could get a bogus "bootstrapped, rescan from the tip"; and a non-string getblockhash reply never called back, leaving the dialog on "Checking..." for good. Only the daemon's "can't read block" error now counts as missing; anything else means "could not tell". User-visible: - after a bootstrap, a probe that cannot tell is tried up to 6 times, 30 s apart and only while the node is not busy, then a toast points to Settings > Rescan, instead of falling back to the -rescan restart, which fails on a bootstrapped node; - the Settings Rescan dialog warns when the probe could not tell. Two new strings, translated into all eight languages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Pure search for the lowest block height whose data the node can still read (a node restored from a
|
||||
// bootstrap snapshot lacks everything below the snapshot base). No I/O: the caller supplies the probe.
|
||||
// Header-only so the test target needs no new source.
|
||||
#pragma once
|
||||
|
||||
#include "../rpc/rpc_failure.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace dragonx {
|
||||
namespace services {
|
||||
|
||||
enum class BlockProbe { Present, Missing, Unknown };
|
||||
|
||||
struct LowestBlockResult {
|
||||
bool ok = false; // false: could not tell (never read as "missing")
|
||||
int lowest = 0; // lowest readable height (valid when ok)
|
||||
bool fullHistory = false; // block 1 readable: not a bootstrapped / pruned node
|
||||
};
|
||||
|
||||
// What a failed getblockhash / getblock means. Only "the daemon says it cannot read the block data"
|
||||
// (RPC_INTERNAL_ERROR -32603: "Can't read block from disk" / "Block not available (pruned data)",
|
||||
// external/dragonx/src/rpc/blockchain.cpp) is Missing. Every transport problem (timeout, busy gate
|
||||
// "RPC deferred", queue full, abort, not listening), warmup (-28), out of range (-8), not found (-5) and
|
||||
// anything unrecognised is Unknown, which stops the search.
|
||||
inline BlockProbe classifyBlockProbeError(const std::exception& e)
|
||||
{
|
||||
if (const auto* r = dynamic_cast<const rpc::RpcError*>(&e)) {
|
||||
if (r->code == -32603) return BlockProbe::Missing;
|
||||
const std::string m = r->what();
|
||||
if (m.find("from disk") != std::string::npos || m.find("pruned") != std::string::npos)
|
||||
return BlockProbe::Missing;
|
||||
return BlockProbe::Unknown;
|
||||
}
|
||||
return BlockProbe::Unknown; // RpcTransportError, runtime_error("Not connected"), json errors
|
||||
}
|
||||
|
||||
// call(method, params) -> json, throwing like RPCClient::call. Present = both calls answered.
|
||||
template <typename Call>
|
||||
BlockProbe probeBlockPresence(Call&& call, int height)
|
||||
{
|
||||
try {
|
||||
nlohmann::json h = call("getblockhash", nlohmann::json::array({height}));
|
||||
if (!h.is_string()) return BlockProbe::Unknown;
|
||||
nlohmann::json b = call("getblock", nlohmann::json::array({h.get<std::string>()}));
|
||||
return b.is_null() ? BlockProbe::Unknown : BlockProbe::Present;
|
||||
} catch (const std::exception& e) {
|
||||
return classifyBlockProbeError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Assumes block data is present from some base height upward (as the old search did). tip = a height known
|
||||
// to exist (the caller's synced tip). Probe order: 1 (a normal full-history node answers in ONE probe), then
|
||||
// the tip (the search's premise), then a binary search over (1, tip]. Any Unknown ends it with ok=false:
|
||||
// nothing is guessed.
|
||||
inline LowestBlockResult findLowestAvailableBlock(int tip, const std::function<BlockProbe(int)>& probe)
|
||||
{
|
||||
LowestBlockResult out;
|
||||
if (tip <= 1) return out;
|
||||
switch (probe(1)) {
|
||||
case BlockProbe::Present: out.ok = true; out.lowest = 1; out.fullHistory = true; return out;
|
||||
case BlockProbe::Unknown: return out;
|
||||
case BlockProbe::Missing: break;
|
||||
}
|
||||
if (probe(tip) != BlockProbe::Present) return out; // an unreadable tip is as unusable as Unknown
|
||||
int lo = 1, hi = tip; // invariant: lo Missing, hi Present
|
||||
while (hi - lo > 1) {
|
||||
const int mid = lo + (hi - lo) / 2;
|
||||
switch (probe(mid)) {
|
||||
case BlockProbe::Present: hi = mid; break;
|
||||
case BlockProbe::Missing: lo = mid; break;
|
||||
case BlockProbe::Unknown: return out;
|
||||
}
|
||||
}
|
||||
out.ok = true;
|
||||
out.lowest = hi;
|
||||
out.fullHistory = false;
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace services
|
||||
} // namespace dragonx
|
||||
Reference in New Issue
Block a user