feat(migrate): persist the sweep opid so a mid-sweep interruption can resume (W3-3)
Migrate-to-seed submits z_mergetoaddress -> an async opid, then only persists the resolved txid once the op completes. An app-close during Sweeping (opid submitted, txid not yet resolved) dropped the opid and resumed at the re-sweep gate, silently losing the tx. Now the opid is persisted and re-tracked on resume. If the daemon forgot it (restart — its op queue is in-memory only), the existing poller flags it stale and the callback falls back to the dismissable Sweep gate; it can never hang (a thrown RPC aborts the poll, so a stale classification only comes from a *successful* poll that omits the opid). - New seed_migration_sweep_opid setting; adopted atomically with clearing any prior txid in the SAME settings.save(), and only once the submit succeeds — so a failed "Sweep remaining" re-sweep keeps the already-mined first sweep's Confirming context, and txid/opid are never both authoritative (resume checks txid first; torn-write safe). - Resume routing extracted to a pure, unit-tested helper (data/seed_migration_resume.h::decideSeedMigrationResume): txid -> Confirming; opid AND connected -> re-track (Sweeping); else -> the dismissable Sweep gate. The connectivity gate keeps a disconnected resume out of the buttonless Sweeping spinner. - Shared makeSweepCompletionCallback(resumed): success -> Confirming; resumed-stale -> Sweep gate (re-fetch balance + "may have already completed" copy); fresh-fail -> Error. Fund safety unchanged: adopt still gated on legacy balance ~0 AND sweep tx mined; legacy wallet.dat only ever moved to a never-deleted timestamped .bak. Reviewed in two adversarial rounds (design + implementation) per the migration-code mandate; both safety facts (no fund loss, no hang) held, and the resume-UX traps they surfaced are fixed. Build-clean; ctest 1/1 (adds testSeedMigrationResume). See docs/wallet-hardening.md. *** Still requires a live mainnet interrupted-sweep run before release (human gate). *** Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
#include "ui/node_status_banner.h"
|
||||
#include "ui/staleness_badge.h"
|
||||
#include "ui/notifications.h"
|
||||
#include "data/seed_migration_resume.h"
|
||||
#include "util/address_validation.h"
|
||||
#include "util/amount_format.h"
|
||||
#include "util/payment_uri.h"
|
||||
@@ -2687,6 +2688,34 @@ void testNotificationHistory()
|
||||
EXPECT_EQ((int)(n.totalPushed() - base), 152); // clearing doesn't rewind the counter
|
||||
}
|
||||
|
||||
void testSeedMigrationResume()
|
||||
{
|
||||
using dragonx::decideSeedMigrationResume;
|
||||
using dragonx::MigrationResume;
|
||||
|
||||
// No pending migration (or missing dest) → start fresh at the intro.
|
||||
EXPECT_TRUE(decideSeedMigrationResume(false, false, "", "", true) == MigrationResume::Intro);
|
||||
EXPECT_TRUE(decideSeedMigrationResume(false, true, "tx", "op", true) == MigrationResume::Intro);
|
||||
EXPECT_TRUE(decideSeedMigrationResume(true, false, "tx", "op", true) == MigrationResume::Intro);
|
||||
|
||||
// A persisted txid outranks everything → resume at the confirm/adopt gate (txid-first invariant).
|
||||
EXPECT_TRUE(decideSeedMigrationResume(true, true, "tx", "", true) == MigrationResume::Confirming);
|
||||
EXPECT_TRUE(decideSeedMigrationResume(true, true, "tx", "op", true) == MigrationResume::Confirming);
|
||||
EXPECT_TRUE(decideSeedMigrationResume(true, true, "tx", "op", false) == MigrationResume::Confirming);
|
||||
|
||||
// Opid but no txid, AND connected → re-track the opid (recover the txid / detect stale).
|
||||
EXPECT_TRUE(decideSeedMigrationResume(true, true, "", "op", true) == MigrationResume::RetrackOpid);
|
||||
|
||||
// W3-3 connectivity gate: opid but NOT connected → the dismissable Sweep gate, NOT the buttonless
|
||||
// Sweeping spinner (whose only exit is the opid poller, which needs a connection). This is the
|
||||
// trap the review caught.
|
||||
EXPECT_TRUE(decideSeedMigrationResume(true, true, "", "op", false) == MigrationResume::SweepGate);
|
||||
|
||||
// No txid and no opid → the Sweep gate (whether or not connected).
|
||||
EXPECT_TRUE(decideSeedMigrationResume(true, true, "", "", true) == MigrationResume::SweepGate);
|
||||
EXPECT_TRUE(decideSeedMigrationResume(true, true, "", "", false) == MigrationResume::SweepGate);
|
||||
}
|
||||
|
||||
void testLoggerFileSink()
|
||||
{
|
||||
using dragonx::util::Logger;
|
||||
@@ -7077,6 +7106,7 @@ int main()
|
||||
testNodeStatusBanner();
|
||||
testStalenessBadge();
|
||||
testNotificationHistory();
|
||||
testSeedMigrationResume();
|
||||
testLoggerFileSink();
|
||||
testDaemonLifecycleExecution();
|
||||
testDaemonLifecycleAdapters();
|
||||
|
||||
Reference in New Issue
Block a user