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:
@@ -3846,6 +3846,24 @@ void testSeedMigrationResume()
|
||||
// ambiguous (the tx may be unconfirmed in the mempool), so direct adopt must be blocked.
|
||||
EXPECT_TRUE(dragonx::sweepGateMayAdoptEmptyWallet(/*sweepSubmitted=*/false)); // empty wallet -> ok
|
||||
EXPECT_FALSE(dragonx::sweepGateMayAdoptEmptyWallet(/*sweepSubmitted=*/true)); // in-flight -> blocked
|
||||
|
||||
// FIX E: reorg-safe adopt depth + the final pre-stop re-verification verdict.
|
||||
EXPECT_EQ(dragonx::kSweepAdoptMinConfs, 10);
|
||||
EXPECT_FALSE(dragonx::sweepConfsAreReorgSafe(-1));
|
||||
EXPECT_FALSE(dragonx::sweepConfsAreReorgSafe(0));
|
||||
EXPECT_FALSE(dragonx::sweepConfsAreReorgSafe(1));
|
||||
EXPECT_FALSE(dragonx::sweepConfsAreReorgSafe(9));
|
||||
EXPECT_TRUE(dragonx::sweepConfsAreReorgSafe(10));
|
||||
const double kDust = 0.0001;
|
||||
EXPECT_TRUE(dragonx::adoptReverifyPasses(true, 10, 0.0, false, kDust)); // deep + empty + synced
|
||||
EXPECT_FALSE(dragonx::adoptReverifyPasses(true, 9, 0.0, false, kDust)); // 1-block-reorg window
|
||||
EXPECT_FALSE(dragonx::adoptReverifyPasses(true, -1, 0.0, false, kDust)); // conflicted
|
||||
EXPECT_FALSE(dragonx::adoptReverifyPasses(true, 50, 0.0, true, kDust)); // syncing -> balance untrusted
|
||||
EXPECT_FALSE(dragonx::adoptReverifyPasses(true, 50, 5.0, false, kDust)); // funds remain / unconfirmed
|
||||
EXPECT_FALSE(dragonx::adoptReverifyPasses(true, 50, -1.0, false, kDust)); // balance unknown
|
||||
EXPECT_TRUE(dragonx::adoptReverifyPasses(false, 0, 0.0, false, kDust)); // genuinely empty, no sweep
|
||||
EXPECT_FALSE(dragonx::adoptReverifyPasses(false, 0, 2.0, false, kDust)); // no-funds path but unconfirmed funds
|
||||
EXPECT_FALSE(dragonx::adoptReverifyPasses(false, 0, 0.0, true, kDust)); // no-funds path while syncing
|
||||
}
|
||||
|
||||
void testLoggerFileSink()
|
||||
|
||||
Reference in New Issue
Block a user