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:
33
src/app.cpp
33
src/app.cpp
@@ -608,14 +608,16 @@ void App::update()
|
||||
// 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) {
|
||||
if (!state_.warming_up && !runtime_rescan_active_) {
|
||||
refreshMiningInfo();
|
||||
}
|
||||
|
||||
// Poll getrescaninfo for rescan progress (if rescan flag is set)
|
||||
// Use fast_rpc_ when available to avoid blocking on rpc_'s
|
||||
// curl_mutex (which may be held by a long-running import).
|
||||
if (state_.sync.rescanning && fast_worker_ && !rescan_status_poll_in_progress_) {
|
||||
// Suppressed during a runtime rescanblockchain: the daemon holds cs_main for the whole
|
||||
// scan, so every poll would block, and completion is signalled by the rescan RPC callback.
|
||||
if (state_.sync.rescanning && !runtime_rescan_active_ && fast_worker_ && !rescan_status_poll_in_progress_) {
|
||||
auto* rescanRpc = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
|
||||
rescan_status_poll_in_progress_ = true;
|
||||
fast_worker_->post([this, rescanRpc]() -> rpc::RPCWorker::MainCb {
|
||||
@@ -915,15 +917,36 @@ void App::update()
|
||||
});
|
||||
}
|
||||
|
||||
// After a bootstrap the preserved wallet.dat has stale spent-state and must be reconciled
|
||||
// against the freshly-imported chain, or the first send tries to spend already-spent notes and
|
||||
// is rejected. The -rescan restart can't do it (the snapshot lacks pre-snapshot blocks), so we
|
||||
// detect the lowest available block height and run a runtime rescanblockchain from there. Wait
|
||||
// until the daemon is connected and out of warmup so the probe and rescan can actually run.
|
||||
if (post_bootstrap_rescan_pending_ && rpcConnected && state_.connected &&
|
||||
!state_.warming_up && !state_.isLocked() && !state_.sync.rescanning &&
|
||||
!runtime_rescan_active_ && !bootstrap_downloading_ &&
|
||||
state_.sync.blocks > 1) { // wait until the tip is known so the probe has a real range
|
||||
post_bootstrap_rescan_pending_ = false;
|
||||
ui::Notifications::instance().info("Bootstrap complete — reconciling your wallet with the new chain data.");
|
||||
detectLowestAvailableBlockHeight([this](bool ok, int lowest, bool fullHistory) {
|
||||
if (ok && !fullHistory) {
|
||||
runtimeRescan(lowest); // bootstrapped/pruned: rescan from the snapshot base
|
||||
} else {
|
||||
rescanBlockchain(); // full-history node (or probe failed): -rescan restart works
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Per-category refresh with tab-aware intervals
|
||||
// Skip when wallet is locked — same reason as above.
|
||||
if (rpcConnected && !state_.isLocked()) {
|
||||
const bool walletDataPage = currentPageNeedsWalletDataRefresh();
|
||||
if (network_refresh_.consumeDue(RefreshTimer::Core)) {
|
||||
if (!runtime_rescan_active_ && network_refresh_.consumeDue(RefreshTimer::Core)) {
|
||||
refreshCoreData();
|
||||
}
|
||||
// Skip balance/tx/address refresh during warmup — RPC calls fail with -28
|
||||
if (!state_.warming_up) {
|
||||
// Skip balance/tx/address refresh during warmup — RPC calls fail with -28 — and during a
|
||||
// runtime rescan, when the daemon holds cs_main and these calls would block.
|
||||
if (!state_.warming_up && !runtime_rescan_active_) {
|
||||
if (network_refresh_.consumeDue(RefreshTimer::Transactions)) {
|
||||
if (shouldRunWalletTransactionRefresh() && shouldRefreshTransactions()) {
|
||||
refreshTransactionData();
|
||||
|
||||
Reference in New Issue
Block a user