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:
@@ -148,6 +148,12 @@ struct SettingsPageState {
|
||||
bool confirm_clear_ztx = false;
|
||||
bool confirm_delete_blockchain = false;
|
||||
bool confirm_rescan = false;
|
||||
// Rescan dialog: probe the node's available block range so a bootstrapped/pruned node gets a
|
||||
// runtime rescan from a snapshot-available height instead of the (failing) -rescan-from-genesis.
|
||||
bool rescan_height_detecting = false;
|
||||
bool rescan_height_detected = false;
|
||||
bool rescan_full_history = true; // genesis present → traditional -rescan restart
|
||||
int rescan_start_height = 0; // editable pre-fill for the runtime rescan
|
||||
bool confirm_repair_wallet = false;
|
||||
bool confirm_reinstall_daemon = false;
|
||||
// Cached daemon-binary status for the "daemon binary" panel (loaded once / on Refresh,
|
||||
@@ -2241,6 +2247,9 @@ void RenderSettingsPage(App* app) {
|
||||
ImGui::SameLine(0, Layout::spacingMd());
|
||||
if (TactileButton(TR("rescan"), ImVec2(0, 0), dbBtnFont)) {
|
||||
s_settingsState.confirm_rescan = true;
|
||||
// Re-probe the available block range each time the dialog is opened.
|
||||
s_settingsState.rescan_height_detecting = false;
|
||||
s_settingsState.rescan_height_detected = false;
|
||||
}
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_rescan"));
|
||||
ImGui::EndDisabled();
|
||||
@@ -2702,8 +2711,22 @@ void RenderSettingsPage(App* app) {
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm: rescan blockchain (restarts the daemon, re-scans the whole chain — long but safe)
|
||||
// Confirm: rescan blockchain. On a normal (full-history) node this restarts the daemon with
|
||||
// -rescan; on a bootstrapped/pruned node that would fail (pre-snapshot blocks are absent), so we
|
||||
// probe the lowest available block height and run a runtime rescanblockchain from a confirmed,
|
||||
// editable height instead.
|
||||
if (s_settingsState.confirm_rescan) {
|
||||
// Kick off the one-shot block-range probe the first frame the dialog is open.
|
||||
if (!s_settingsState.rescan_height_detecting && !s_settingsState.rescan_height_detected) {
|
||||
s_settingsState.rescan_height_detecting = true;
|
||||
app->detectLowestAvailableBlockHeight([](bool ok, int lowest, bool fullHistory) {
|
||||
s_settingsState.rescan_height_detecting = false;
|
||||
s_settingsState.rescan_height_detected = true;
|
||||
s_settingsState.rescan_full_history = (!ok) || fullHistory;
|
||||
s_settingsState.rescan_start_height = (ok && !fullHistory) ? lowest : 0;
|
||||
});
|
||||
}
|
||||
|
||||
if (BeginOverlayDialog(TR("confirm_rescan_title"), &s_settingsState.confirm_rescan, 500.0f, 0.94f)) {
|
||||
ImGui::PushFont(Type().iconLarge());
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING);
|
||||
@@ -2712,9 +2735,25 @@ void RenderSettingsPage(App* app) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", TR("confirm_rescan_msg"));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_rescan_safe"));
|
||||
|
||||
const bool detecting = s_settingsState.rescan_height_detecting;
|
||||
const bool bootstrapped = s_settingsState.rescan_height_detected && !s_settingsState.rescan_full_history;
|
||||
|
||||
if (detecting) {
|
||||
ImGui::TextWrapped("%s", TR("rescan_detecting"));
|
||||
} else if (bootstrapped) {
|
||||
ImGui::TextWrapped("%s", TR("rescan_bootstrapped_msg"));
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("%s", TR("rescan_from_height"));
|
||||
ImGui::SetNextItemWidth(160.0f);
|
||||
ImGui::InputInt("##rescanHeight", &s_settingsState.rescan_start_height);
|
||||
if (s_settingsState.rescan_start_height < 0) s_settingsState.rescan_start_height = 0;
|
||||
} else {
|
||||
ImGui::TextWrapped("%s", TR("confirm_rescan_msg"));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_rescan_safe"));
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
@@ -2724,10 +2763,16 @@ void RenderSettingsPage(App* app) {
|
||||
s_settingsState.confirm_rescan = false;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(detecting);
|
||||
if (ImGui::Button(TrId("rescan", "rescan_confirm").c_str(), ImVec2(btnW, 40))) {
|
||||
app->rescanBlockchain();
|
||||
if (bootstrapped) {
|
||||
app->runtimeRescan(s_settingsState.rescan_start_height);
|
||||
} else {
|
||||
app->rescanBlockchain();
|
||||
}
|
||||
s_settingsState.confirm_rescan = false;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
EndOverlayDialog();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user