Security audit remediation (15 confirmed findings from the codebase audit):
- H-02: scrub+delete the decrypt-flow plaintext key export on ALL exit paths
(RAII guard) and purge stale obsidiandecryptexport* files at startup.
- M-01/L-03/L-04/L-05/L-07: sodium_memzero the Set-PIN and encrypt-PIN worker
passphrase/PIN copies, the RPC Basic-auth string (auth_), the exported/imported
key buffers (App::wipeSecrets, called from ~App and before main's _Exit), and
the first-run wizard "Skip" buffers.
- M-03/M-04/M-05/L-06: return locked COPIES from XmrigManager/EmbeddedDaemon
getters (dedicated error_mutex_; DaemonController::lastError now by value),
route xmrig last_error_ writes through a locked setter, and wrap
shutdown_status_/wizard_stop_status_ in a locking GuardedStatus
(wizard_stopping_external_ -> std::atomic).
- M-02: persist after a console send/shield/import in the lite backend.
- L-01: require the confirm click for z_shieldcoinbase/z_mergetoaddress.
- L-02: quote/escape each Windows daemon argv per the MSDN CommandLineToArgvW rules.
- L-08: pin json/tomlplusplus/libwebp FetchContent to immutable commit SHAs.
- I-01: extract updater archives from the already-verified in-memory buffer
(no disk re-read TOCTOU).
Feature: warn once (full-node) when the active wallet loads empty while a sibling
wallet file in the datadir holds keys. A funded salvage wallet.<ts>.bak routes to
the recovery/Restore flow; a funded sibling .dat routes to the wallet manager.
Per-wallet-file dismissal; gated on synced + address-list-loaded to avoid false
positives on warm reconnect / spent-down wallets.
UX fixes:
- send: show the TOTAL balance (with a spendable "available" note) in the source
dropdown and keep pending-change addresses visible.
- chat: insert emoji at the cursor position; restrict new-chat recipients to
shielded (z) addresses.
- console: optional auto-focus of the command input on tab open (off by default).
- shutdown: when "stop external daemon" is on, keep the shutdown screen up until
the external node actually exits, showing live status.
Adversarially reviewed; verified across full-node, lite, and Windows builds; tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
When a switch fails, the failure modal now distinguishes a CORRUPT target wallet
from other failures and offers a one-click repair:
- The switch worker watermarks the node's captured console output before the
start and, if the node dies in init, scans this start's output for a corruption
signature ("Failed to rename … .bak" / "salvage failed" / "wallet.dat corrupt" /
"Error loading wallet") → sets switch_wallet_corrupt_.
- The Failed modal then shows an accurate "this wallet appears corrupt" message
(instead of the generic "Couldn't open that wallet") plus a "Try to repair
(salvage)" button that retries the switch with the target node started under
-salvagewallet (recovers readable keypairs; implies -rescan).
- EmbeddedDaemon::setSalvageOnNextStart (one-shot, precedence salvage > zap >
rescan) + controller forwarder; switchToWallet gains a salvage arg.
Salvage operates only on the corrupt target (never the good wallet, no fund
movement). Adversarially verified: output isolation, one-shot lifecycle, state
handling, re-entrancy, no success-path regression.
i18n (EN + 8 languages) + 2 new CJK glyphs baked into the subset font.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Switching to a named wallet failed every time when the app had connected to a
pre-existing dragonxd at startup: the daemon is flagged externalDaemonDetected,
so stopEmbeddedDaemon()'s policy is DisconnectOnly ("not ours to stop") and the
switch skips the stop entirely. The old daemon keeps the RPC port, and the
relaunch fast-fails on EmbeddedDaemon::start()'s isPortInUse check — misread as a
bad wallet, reverting after a ~40s hang on the process-handle-only run-wait
(which reads false immediately for an adopted daemon).
A switch legitimately needs to restart the node, so:
- App::stopDaemonForWalletSwitch(): for an adopted daemon, send a graceful RPC
"stop" using the creds we actually connected with (saved_config_) — only our
own daemon obeys it, so a foreign dragonxd is a safe no-op — then wait (bounded
~40s) for the RPC port to actually free (isRpcPortInUse, the same gate start()
uses). Owned daemons take the normal stopEmbeddedDaemon() path. No PID/name kill
is ever issued at an adopted daemon.
- switchToWallet: gate start() on the port actually freeing; if it doesn't,
abort with a distinct switch_stop_failed_ reason instead of starting into a busy
port. Clear the external latch before relaunch so the fresh process is owned.
- EmbeddedDaemon::clearExternalDaemonDetected() (+ controller forwarder).
- Accurate revert message: "the running node didn't release its connection in
time" vs. the bad-wallet message.
Root-caused from live Windows logs; design + implementation adversarially
verified. Note: beginAdoptSeedWallet has the identical pattern and is left for a
separate fund-critical review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a note's stored record is corrupt or its tx isn't in the canonical chain,
z_sendmany fails to build a valid sapling spend proof even after a full -rescan,
because a plain rescan replays witnesses but keeps the existing tx/note records.
The zcashd repair for this is -zapwallettxes=2, which deletes all wallet tx/note
records and rebuilds them from the chain (keys/addresses preserved).
Adds a RepairWallet lifecycle operation that mirrors the existing -rescan plumbing
(one-shot zapOnNextStart flag on the embedded daemon; -zapwallettxes=2 implies and
supersedes -rescan), an App::repairWallet() that reuses the rescan status UI (so the
status bar + warmup-end completion detection apply), and a confirmed "Repair Wallet"
button + dialog in Settings → node maintenance (embedded daemon only).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>