feat(fullnode): auto-reconcile wallet after bootstrap; runtime rescan for pruned nodes
A wallet bootstrapped from a snapshot keeps its wallet.dat but never rescans, so its spent-state is stale and the first send tries to spend already-spent notes and is rejected. The startup -rescan flag can't fix it either: the snapshot lacks the pre-snapshot block history -rescan needs, so it errors. The working fix is a runtime rescanblockchain RPC from a height the snapshot actually has. - Add App::runtimeRescan(startHeight): runs rescanblockchain via the worker, drives the rescanning UI state, and owns completion via the RPC callback (getrescaninfo is unavailable on this daemon). Suppresses the per-second mining/rescan pollers and the Core/balance/tx refreshes while the daemon holds cs_main for the scan. - Add App::detectLowestAvailableBlockHeight(): async binary search via getblock for the lowest height whose block data is on disk → the snapshot base, and whether the node still has full history. - Auto-reconcile after bootstrap: both completion sites (wizard + Settings download dialog) mark a pending rescan; once the daemon is back up and the tip is known, detect the base and runtimeRescan() from it (or -rescan restart on a full node). - Settings "Rescan Blockchain" now probes first: full-history nodes get the existing -rescan restart; bootstrapped/pruned nodes get a prompt pre-filled with the detected base height that runs the runtime rescan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2499,4 +2499,110 @@ void App::resendWithFeeGapWorkaround(const std::string& from, const std::string&
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
// Bootstrapped/pruned-node rescan support
|
||||
//
|
||||
// A node restored from a bootstrap snapshot only has block data from the snapshot base upward;
|
||||
// blocks below it are absent on disk. The startup -rescan flag rescans from genesis and fails on
|
||||
// such a node ("error in HDD data"), and rescanblockchain(0) hits the same missing blocks. The
|
||||
// fix is to rescan from a height the snapshot includes — found here by binary-searching for the
|
||||
// lowest height whose block data the node can actually read.
|
||||
// --------------------------------------------------------------------------------------------
|
||||
void App::detectLowestAvailableBlockHeight(std::function<void(bool, int, bool)> cb)
|
||||
{
|
||||
if (!rpc_ || !rpc_->isConnected()) {
|
||||
if (cb) cb(false, 0, false);
|
||||
return;
|
||||
}
|
||||
const int tip = state_.sync.blocks;
|
||||
if (tip <= 1) {
|
||||
// No usable tip yet (or a brand-new chain) — treat as full history, nothing to probe.
|
||||
if (cb) cb(false, 0, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Shared search window; each probe halves it. Invariant maintained: block at `hi` is readable.
|
||||
struct SearchState { int lo; int hi; std::function<void(bool, int, bool)> cb; };
|
||||
auto st = std::make_shared<SearchState>(SearchState{0, tip, std::move(cb)});
|
||||
auto step = std::make_shared<std::function<void()>>();
|
||||
*step = [this, st, step]() {
|
||||
if (st->lo >= st->hi) {
|
||||
const bool fullHistory = (st->lo <= 1); // genesis/early block present → not bootstrapped
|
||||
if (st->cb) st->cb(true, st->lo, fullHistory);
|
||||
return;
|
||||
}
|
||||
const int mid = st->lo + (st->hi - st->lo) / 2;
|
||||
rpc_->getBlock(mid, [st, step, mid](const nlohmann::json& result, const std::string& error) {
|
||||
if (error.empty() && !result.is_null()) {
|
||||
st->hi = mid; // block data present → lowest available is <= mid
|
||||
} else {
|
||||
st->lo = mid + 1; // block data missing → lowest available is > mid
|
||||
}
|
||||
(*step)();
|
||||
});
|
||||
};
|
||||
(*step)();
|
||||
}
|
||||
|
||||
void App::runtimeRescan(int startHeight)
|
||||
{
|
||||
if (!supportsFullNodeLifecycleActions()) {
|
||||
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
|
||||
return;
|
||||
}
|
||||
if (!state_.connected || !rpc_ || !rpc_->isConnected() || !worker_) {
|
||||
ui::Notifications::instance().warning("Not connected to daemon");
|
||||
return;
|
||||
}
|
||||
if (runtime_rescan_active_) return;
|
||||
if (startHeight < 0) startHeight = 0;
|
||||
|
||||
DEBUG_LOGF("[App] Starting runtime rescanblockchain from height %d\n", startHeight);
|
||||
|
||||
// The rescan runs inside the daemon (holds cs_main/cs_wallet) — it does not restart. We own
|
||||
// completion via this RPC's callback; rescan_confirmed_active_ keeps the -rescan-style pollers
|
||||
// from misreading state, and runtime_rescan_active_ suppresses the per-second pollers that
|
||||
// would otherwise pile up behind the held lock.
|
||||
runtime_rescan_active_ = true;
|
||||
state_.sync.rescanning = true;
|
||||
rescan_confirmed_active_ = true;
|
||||
state_.sync.rescan_progress = 0.0f;
|
||||
state_.sync.rescan_status = "Rescanning from block " + std::to_string(startHeight) + "...";
|
||||
transactions_dirty_ = true;
|
||||
last_tx_block_height_ = -1;
|
||||
invalidateShieldedHistoryScanProgress(true);
|
||||
|
||||
ui::Notifications::instance().info("Rescanning blockchain from block " + std::to_string(startHeight) +
|
||||
" — this can take a while.");
|
||||
|
||||
worker_->post([this, startHeight]() -> rpc::RPCWorker::MainCb {
|
||||
bool ok = false;
|
||||
std::string err;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Settings / Runtime rescan");
|
||||
rpc_->call("rescanblockchain", {startHeight}); // blocks until the scan finishes
|
||||
ok = true;
|
||||
} catch (const std::exception& e) {
|
||||
err = e.what();
|
||||
}
|
||||
return [this, ok, err]() {
|
||||
runtime_rescan_active_ = false;
|
||||
rescan_confirmed_active_ = false;
|
||||
state_.sync.rescanning = false;
|
||||
state_.sync.rescan_status.clear();
|
||||
if (ok) {
|
||||
state_.sync.rescan_progress = 1.0f;
|
||||
transactions_dirty_ = true;
|
||||
last_tx_block_height_ = -1;
|
||||
invalidateShieldedHistoryScanProgress(true);
|
||||
ui::Notifications::instance().success("Blockchain rescan complete");
|
||||
refreshData();
|
||||
} else {
|
||||
state_.sync.rescan_progress = 0.0f;
|
||||
ui::Notifications::instance().error("Rescan failed: " + err);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace dragonx
|
||||
|
||||
Reference in New Issue
Block a user