feat(node): detect an unreadable block DB on startup and offer a one-click reindex

When a daemon update changes the block-index on-disk format (or the index is
corrupt), dragonxd aborts at startup — "non-canonical optional discriminant" →
"Error loading block database. Aborted." — and the wallet silently shows a zero
balance. Previously the connect loop just crash-restarted into the same abort up
to 3x and then reported a bare "Daemon crashed N times", with no path forward.

Now:
- daemon/daemon_startup_diagnosis.h: pure blockDbOutputLooksBroken() classifies
  the crashed node's captured console output (the fatal block-DB markers).
- The connect loop detects it on the FIRST abort, STOPS crash-restarting into the
  same failure (each retry reloads the whole index — wasteful), and offers a fix.
- A one-shot -reindex flag (EmbeddedDaemon::setReindexOnNextStart → DaemonController
  forwarder → args) rebuilds the block index + chainstate from the intact raw
  blocks; App::reindexBlockDatabase() arms it and un-gates the loop to restart.
- An auto-shown dialog (renderBlockDbReindexDialog) + a notification explain the
  situation ("your coins are safe; the node just can't load the chain") and offer
  a one-click "Rebuild block database". Full-node only (gated), lite-safe.

This is the exact trap behind a real "big wallet shows no funds" report: a
post-format-change daemon over pre-change chaindata. Reindex also fixes a plain
corrupt index.

Adds testBlockDbOutputDiagnosis (the abort sequence + individual markers trip it;
normal startup / wallet-corruption / asmap errors do not). Suite green (1/1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-09 23:49:46 -05:00
parent f88304fed2
commit d136916e80
10 changed files with 163 additions and 1 deletions

View File

@@ -2143,6 +2143,7 @@ void App::render()
renderDecryptWalletDialog();
renderPinDialogs();
renderSwitchStopDaemonDialog();
renderBlockDbReindexDialog();
// Render notifications (toast messages)
ui::Notifications::instance().render();
@@ -4280,6 +4281,37 @@ void App::renderAntivirusHelpDialog()
#endif
}
// Auto-shown when the embedded node aborts on an unreadable block database — offers the one-click
// -reindex rebuild instead of leaving the wallet stuck on a silent zero balance.
void App::renderBlockDbReindexDialog()
{
if (!show_block_db_reindex_confirm_) return;
ui::material::OverlayDialogSpec ov;
ov.title = TR("block_db_reindex_title");
ov.p_open = &show_block_db_reindex_confirm_; // X / backdrop dismisses (offer remains; loop stays held)
ov.style = ui::material::OverlayStyle::BlurFloat;
ov.cardWidth = 540.0f;
ov.idSuffix = "blockdbreindex";
if (!ui::material::BeginOverlayDialog(ov)) return;
const float dp = ui::Layout::dpiScale();
ui::material::DialogWarningHeader(TR("block_db_reindex_warn"));
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::TextWrapped("%s", TR("block_db_reindex_body"));
ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd()));
if (ui::material::TactileButton(TR("block_db_reindex_confirm"), ImVec2(260.0f * dp, 0))) {
reindexBlockDatabase(); // clears show_block_db_reindex_confirm_ + block_db_reindex_available_
}
ImGui::SameLine();
if (ui::material::TactileButton(TR("cancel"), ImVec2(110.0f * dp, 0))) {
show_block_db_reindex_confirm_ = false;
// Leave block_db_reindex_available_ set: the connect loop keeps HOLDING (no crash-restart storm)
// rather than looping into the same abort; the user can rebuild later from Settings.
}
ui::material::EndOverlayDialog();
}
void App::renderSwitchStopDaemonDialog()
{
const bool confirm = show_switch_stop_daemon_confirm_;
@@ -6054,4 +6086,24 @@ void App::restartDaemon()
});
}
// One-click recovery for an unreadable block database: arm the one-shot -reindex flag, then un-gate the
// connect loop (which detected the abort and stopped restarting). Its next attempt calls
// startEmbeddedDaemon(), which consumes the flag → the node rebuilds its block index + chainstate from
// the raw blocks. The daemon is not running here (it aborted), so no explicit stop/restart is needed.
void App::reindexBlockDatabase()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
return;
}
if (!daemon_controller_) return;
daemon_controller_->setReindexOnNextStart(true);
daemon_controller_->resetCrashCount(); // the abort no longer counts against the restart budget
show_block_db_reindex_confirm_ = false;
block_db_reindex_available_ = false; // un-gate → the connect loop restarts the node with -reindex
connection_status_ = TR("sb_starting_daemon");
ui::Notifications::instance().info(TR("block_db_reindex_started"), 12.0f);
DEBUG_LOGF("[App] Block-database reindex requested — restarting node with -reindex\n");
}
} // namespace dragonx