fix(migrate): require a reorg-safe sweep depth + re-verify before adopt; make the wallet.dat swap crash-safe
Resilience audit, migrate-to-seed (the sweep RPC is byte-identical — see below): - Adopt was enabled at >= 1 confirmation and never re-read state before stopping the daemon, so a 1-block reorg after the swap could un-mine the sweep and strand the funds in the .bak. Adopt now needs a reorg-safe depth (>= 10, matching the History "confirmed" threshold), and the button runs verifyThenAdoptSeedWallet(): a read-only gettransaction(confs) + z_gettotalbalance minconf 0 (includes unconfirmed, fixing the old minconf-1 "no funds" gate) immediately before the daemon stop, refusing while syncing or if any read fails. The remainder re-sweep still shows once mined (>=1). - The wallet.dat swap was rename(legacy->bak) then non-atomic copy(new->wallet.dat): a hard kill left wallet.dat missing or half-written. New order: probe the new file is a BDB wallet; stage a size-verified copy beside wallet.dat; COPY (never overwrite) the legacy wallet to the timestamped .bak and verify it; then ONE atomic fs::rename of the staged file over wallet.dat. At every instant wallet.dat is the whole old or whole new wallet, and a verified .bak exists before the replace. On any failure only our scratch files are removed; the user's wallet is untouched. Byte-identical: z_mergetoaddress (beginSweepToSeedWallet, ["ANY_TADDR","ANY_ZADDR"], amounts, fee, recipient, memo) and beginAdoptSeedWallet's disconnect/vault-removal/stop/rescan/restart logic are unchanged; only the adopt gating, two new read-only RPCs, presentation, and the file-move safety change. Pure tests: sweepConfsAreReorgSafe + adoptReverifyPasses. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+9
-4
@@ -5043,7 +5043,7 @@ void App::renderSeedMigrationDialog()
|
||||
ImGui::Spacing();
|
||||
if (!seed_migration_nofunds_confirmed_) ImGui::BeginDisabled();
|
||||
if (ui::material::TactileButton(TR("mig_adopt_now"), ImVec2(190 * dp, 0)))
|
||||
beginAdoptSeedWallet();
|
||||
verifyThenAdoptSeedWallet(); // fresh minconf-0, sync-aware balance check before the swap
|
||||
if (!seed_migration_nofunds_confirmed_) ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton(TR("refresh"), ImVec2(110 * dp, 0)))
|
||||
@@ -5107,7 +5107,8 @@ void App::renderSeedMigrationDialog()
|
||||
|
||||
case SeedMigrationStep::Confirming: {
|
||||
const double dust = DRAGONX_DEFAULT_FEE; // treat <= one fee as "empty"
|
||||
const bool confirmed = seed_migration_sweep_confs_ >= 1;
|
||||
const bool mined = seed_migration_sweep_confs_ >= 1; // remainder re-sweep stays offered once mined
|
||||
const bool confirmed = dragonx::sweepConfsAreReorgSafe(seed_migration_sweep_confs_); // adopt needs reorg-safe depth
|
||||
const bool legacyEmpty = seed_migration_legacy_remaining_ >= 0.0 &&
|
||||
seed_migration_legacy_remaining_ <= dust;
|
||||
ImGui::TextWrapped("%s", TR("mig_confirming"));
|
||||
@@ -5127,8 +5128,8 @@ void App::renderSeedMigrationDialog()
|
||||
ImGui::Spacing();
|
||||
if (confirmed && legacyEmpty) {
|
||||
if (ui::material::TactileButton(TR("mig_adopt"), ImVec2(180 * dp, 0)))
|
||||
beginAdoptSeedWallet();
|
||||
} else if (confirmed && seed_migration_legacy_remaining_ >= 0.0 && !legacyEmpty) {
|
||||
verifyThenAdoptSeedWallet(); // re-reads confs + balance right before the daemon stop
|
||||
} else if (mined && seed_migration_legacy_remaining_ >= 0.0 && !legacyEmpty) {
|
||||
// Only claim a remainder once the legacy balance was actually measured; while a sweep is
|
||||
// mined but the balance scan is still gated (node syncing), remaining stays at the -1.0
|
||||
// sentinel, so fall through to the waiting state rather than offering a sweep on an unknown.
|
||||
@@ -5152,6 +5153,10 @@ void App::renderSeedMigrationDialog()
|
||||
if (ui::material::TactileButton(TR("cancel"), ImVec2(110 * dp, 0)))
|
||||
seed_migration_confirm_sweep_ = false;
|
||||
}
|
||||
} else if (mined && legacyEmpty) {
|
||||
// Mined but not yet reorg-safe deep: say so instead of the misleading "waiting to be mined".
|
||||
char dbuf[200]; snprintf(dbuf, sizeof(dbuf), TR("mig_waiting_depth"), dragonx::kSweepAdoptMinConfs);
|
||||
ImGui::TextWrapped("%s", dbuf);
|
||||
} else {
|
||||
ImGui::TextColored(kMedium, "%s", TR("mig_waiting_mine"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user