From f9ddab059efd645e48ad2f535ad61ccdd8cf85ff Mon Sep 17 00:00:00 2001 From: DanS Date: Sun, 2 Aug 2026 15:18:09 -0500 Subject: [PATCH] fix(wallet): stamp syncedHere only after identity verified + guard the startup wallet file (W1-3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - W1-3 (Med): updateWalletIndexForActiveWallet stamped syncedHere in the markOpened block at bare connect (idHash still empty), letting a freshly-restored wallet skip its needed rescan. syncedHere is now stamped only once the wallet's identity is verified (idHash non-empty), so it takes effect at the post-address-refresh index update; lastOpenedEpoch still records at open. - Startup guard (the W1-1 launch counterpart): App::init now exists()-checks the recorded active wallet before the daemon is configured. A non-default active wallet moved/deleted between sessions falls back to the default wallet.dat with a warning, instead of the daemon silently auto-creating an empty wallet under the missing name. Runs before the PIN vault init so the vault is scoped to the wallet actually opened. Completes P1-B. Remaining P1: W3-3 (sweep opid persistence) deferred for careful adversarially-reviewed work — re-tracking a stale opid could hang the migration if the op poller doesn't time out; the existing balance/mined gates already prevent fund loss. See docs/wallet-hardening.md. Build-clean; ctest 1/1. Co-Authored-By: Claude Opus 4.8 --- docs/wallet-hardening.md | 9 +++++++-- src/app.cpp | 22 ++++++++++++++++++++++ src/app_network.cpp | 9 ++++++++- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/docs/wallet-hardening.md b/docs/wallet-hardening.md index 9715808..4b71107 100644 --- a/docs/wallet-hardening.md +++ b/docs/wallet-hardening.md @@ -18,8 +18,8 @@ Status legend: ☐ not started · ◐ in progress · ☑ landed & verified |-------|----------|-------|--------| | **P0-A** | W7-1, W2-1, W4-1, W4-3, W2-3, W4-5, W5-3 ✓ | Secret hardening (console redaction + delete-export + memzero + lite encrypt-at-create) | ☑ 7/7 | | **P0-B** | W2-2/W4-2, W2-4 | Encryption integrity (never silently unencrypted) | ☑ | -| **P1-A** | W3-1, W3-2, W3-4 ✓ · W3-3 ☐ | Migrate-to-seed correctness (fund-adjacent) | ◐ 3/4 | -| **P1-B** | W1-1, W1-2, W1-4 ✓ · W1-3 ☐ | Missing/wrong wallet-file safety | ◐ 3/4 | +| **P1-A** | W3-1, W3-2, W3-4 ✓ · W3-3 ⚑ | Migrate-to-seed correctness (fund-adjacent) | ◐ 3/4 | +| **P1-B** | W1-1, W1-2, W1-3, W1-4 ✓ + startup guard | Missing/wrong wallet-file safety | ☑ | | **P2** | W6-2, W5-1, W5-2, W6-1, W6-3 | Stale state & lite save-failure surfacing | ☐ | | **F** | W7-2, W7-3, W7-4, QoL | Diagnostics foundation + QoL bundle | ☐ | @@ -112,6 +112,11 @@ Land W7-2 first — it unblocks the rest. ## Progress log +- **P1-B / W1-3 + startup wallet-existence guard** — ☑ landed: + - **W1-3 (Med):** `syncedHere` was stamped in the `markOpened` block at bare connect (idHash still empty), letting a freshly-restored wallet skip its needed rescan. It's now stamped only once the identity is verified (idHash non-empty), so it takes effect at the post-address-refresh index update (`updateWalletIndexForActiveWallet` after addresses load), while `lastOpenedEpoch` still records at open. + - **Startup guard (the W1-1 launch counterpart):** `App::init` now `exists()`-checks the recorded active wallet before the daemon is configured; a **non-default** active wallet that was moved/deleted between sessions falls back to the default `wallet.dat` with a warning, instead of the daemon silently auto-creating an empty wallet under the missing name. Runs before the PIN-vault init so the vault is scoped to the wallet actually opened. + Build-clean; `ctest` 1/1. +- **P1-A / W3-3 (sweep opid persistence)** — ⚑ **deferred for careful, adversarially-reviewed work** (not rushed). `trackOperation` only enqueues the opid for a background poller; a resume that re-tracks a persisted opid is only safe if the poller times out a *stale* opid (daemon restarted → op gone) rather than polling forever — otherwise a resume would hang the migration permanently, worse than today's re-sweep. Verifying that (and the double-sweep interactions) is exactly the "two rounds of adversarial review + a live mainnet run" the migration code mandates. The existing safety gates (adopt requires the legacy balance ~0 AND the sweep tx mined) already prevent fund *loss* on a mid-sweep interruption; W3-3 is a stuck-state robustness improvement, so it can wait for a dedicated pass. - **P1-B / W1-1 (+ W1-4) · W1-2 (wallet-file safety)** — ☑ landed: - **W1-1 (High):** `switchToWallet` never checked the target wallet file exists, so a moved/deleted file "opened" as a fresh empty wallet (dragonxd auto-creates for a missing `-wallet=`), looking exactly like fund loss. It now `std::filesystem::exists`-checks `datadir + "/" + walletFile` before switching and blocks with a "not found (moved or deleted?)" warning. Placed before the daemon-stop prompt, and — since the check runs no matter how `switchToWallet` is invoked — it also **closes W1-4** (the stale-switcher-row TOCTOU). - **W1-2 (Med):** `walletOutputLooksCorrupt` matched the generic "Error loading wallet" string, so a `DB_TOO_NEW` (newer-version) wallet was offered a `-salvagewallet` repair that can't fix it. Now the generic match is excluded when the output also contains "newer version". diff --git a/src/app.cpp b/src/app.cpp index 2ce31f1..666fdf0 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -341,6 +341,28 @@ bool App::init() // Ensure ObsidianDragon config directory and template files exist util::Platform::ensureObsidianDragonSetup(); + // W1-1 (startup): if the recorded active wallet file was moved/deleted between sessions, don't hand a + // missing -wallet= to the daemon — it would auto-create a fresh empty wallet under that name, + // silently "opening" as a zero-balance wallet at launch. Fall back to the always-present default and + // warn. (The default "wallet.dat" is legitimately absent on first run, so it is skipped.) Runs before + // the vault init below so the vault is scoped to the wallet actually opened. + if (settings_) { + const std::string active = settings_->getActiveWalletFile(); + if (!active.empty() && active != "wallet.dat") { + std::error_code walEc; + const std::string walPath = util::Platform::getDragonXDataDir() + "/" + active; + if (!std::filesystem::exists(walPath, walEc)) { + DEBUG_LOGF("[App] active wallet '%s' not found at startup — falling back to wallet.dat\n", + active.c_str()); + settings_->setActiveWalletFile("wallet.dat"); + settings_->save(); + ui::Notifications::instance().warning( + "Your last-used wallet file (" + active + ") was not found — opened the default wallet " + "instead. If you moved it, restore it and switch back from the wallet list.", 20.0f); + } + } + } + // Initialize PIN vault, scoped to the active wallet so one wallet's stored passphrase is never // offered for another (the default wallet keeps the legacy vault.dat). vault_ = std::make_unique(settings_ ? settings_->getActiveWalletFile() : ""); diff --git a/src/app_network.cpp b/src/app_network.cpp index dd89e7a..d8ccfa4 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -1096,9 +1096,16 @@ void App::updateWalletIndexForActiveWallet(bool markOpened) if (!ec) e.sizeBytesAtLastOpen = static_cast(sz); } + // W1-3: record "synced here" only once the wallet's identity is actually verified (its addresses are + // known -> idHash non-empty). Stamping it on the bare connect (before any address readback) would let + // a freshly-restored wallet skip its needed rescan. It's idempotent, so the post-refresh update + // (updateWalletIndexForActiveWallet after addresses load) sets it once; lastOpenedEpoch is still + // recorded at open time here. + if (!idHash.empty()) { + e.syncedHere = true; // loaded + identity-verified in this datadir -> catch-up (no full rescan) + } if (markOpened) { e.lastOpenedEpoch = static_cast(std::time(nullptr)); - e.syncedHere = true; // we've loaded it in this datadir -> catch-up (no full rescan) on switch } if (wallet_index_.upsert(e)) wallet_index_.save();