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:
52
src/app.cpp
52
src/app.cpp
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user