Starting the daemon polls for an already-running node holding the
datadir, sleeping up to three 100 ms intervals to let a shutting-down
node clear out. When a start runs on the UI thread, that is up to 300 ms
of blocking sleep on the render thread.
- The lock-wait poll count is now a parameter. A start invoked on the UI
thread passes 0, which does a single check and never sleeps; every
other caller keeps the three-poll wait. The default is unchanged, so
background starts behave exactly as before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the daemon was restarted, the output-parse offset was reset to 0.
The daemon's output buffer is retained across runs, so the next poll
re-read the whole retained tail of the previous run and fed it to the
busy tracker as if it were fresh. A witness rebuild or rescan line left
over from the old run could then be mistaken for activity in the new
one.
- On restart the offset is now reset to the absolute end of the output
at that moment, captured on the lifecycle thread after the stop and
before the start, so the UI resumes parsing only the new run's output
rather than replaying the old tail.
- The capture is handed to the UI thread through a pending flag and
applied there before the next read, both on the normal poll and when
skipping the backlog, so the offset is only ever moved on the thread
that owns it.
Tests cover reading from the captured watermark (only the new run is
seen) versus the old reset-to-0 behaviour (the previous run replays).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The daemon's RSS was read on the UI thread: a scan of every /proc entry
on Linux (1-7 ms), a forked ps on macOS (up to ~30 ms). The mining poll
probed every 1-5 s on every tab, and the Mining tab's memory bar probed
again every 1.5 s.
util::BackgroundSampler now runs the probe on one lazily started thread
and caches the result. getDaemonMemoryUsageMB() returns the last sample
and asks for a new one at most every 3 s, and only while the Mining tab
memory bar or the Peers tab Memory cell is drawn: nothing on other tabs
or while minimized. EmbeddedDaemon::memoryProbe() hands the sampler a
pid snapshot (on Windows the value read from the owned process handle),
so nothing off-thread touches daemon state. The mining poll does no I/O
any more; it carries the last sample. Both shutdown paths stop the
sampler.
Fixed along the way: an unreadable /proc made the directory iterator
throw on the UI thread; the scan now just stops.
User-visible:
- the Peers tab Memory cell keeps updating during sync and shows an
external daemon's memory (it showed the mining poll's value, which
pauses during sync and covers only the embedded daemon);
- the Mining tab memory bar refreshes every ~3 s (was 1.5 s).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every operation that stops the node (restart, rescan, repair, delete
chain data, reinstall, bootstrap, wallet switch/adopt/restore/rebuild,
wizard restart, encrypt, decrypt) now:
- refuses up front while the node is rebuilding witnesses or has not
answered for 30s, with a "try again when it's done" warning, and sends
no stop;
- goes ahead only once the node's PROCESS has exited, not its RPC port.
A stop that doesn't finish within 120s cancels the operation (warning;
a cancelled rescan/repair disarms its flags); nothing is started over
the old node, chain data isn't deleted, wallet.dat isn't swapped.
Specifics:
- stopDaemonForWalletSwitch() no longer reports "stopped" when the app
is quitting (adopt could swap wallet.dat under a live node).
- Decrypt waited a fixed 3s after "stop" before renaming wallet.dat; it
now waits for the process to exit, and scrubs the key export if not.
- Bootstrap waits for the process before cleanChainData and stops the
node through the stop routine instead of the shared RPC client.
- StopScope::Connected lets switch/reinstall/decrypt stop an adopted or
external node through the same never-kill routine.
- A bounded stop that can't reach the node (a foreign node refusing our
credentials) gives up after 5s instead of 120s.
- macOS: isDaemonProcessRunning() checks processes (libproc), not the
RPC port.
- rpcBusyWhileAlive() also reads the busy gate: since the gate, the
first timeout usually lands on another lane and the 30s clock never
started (also weakened the quit prompt for a silent rebuild).
- Notifications render during the first-run wizard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closing the wallet sent "stop" at most twice, gated on a getinfo probe
that needs the daemon's cs_main, then killed the node after 20 s (Windows)
or 30 s (POSIX) whether or not stop had arrived. A node busy with an
hour-long witness rebuild therefore never got stop, was killed mid-
rebuild, and replayed everything since its last flush on every start.
Delivery (src/daemon/stop_delivery.h, RPCClient::open/trySendStop):
- stop goes over a dedicated client with no probe, so it never waits on
rpc_'s lock or a cs_main-bound getinfo
- every attempt is classified, and StopDeliveryPolicy decides the next:
Accepted / 503 = delivered; Queued (reply timeout) is resent at most
every 30 s; QueueFull / not listening are retried every 1.5 s; a
refusal after we were heard means the node is shutting down; a 401 on
our own node tries the next credential source
- POSIX: a group SIGTERM goes out as soon as an attempt fails, which
bypasses the HTTP queue entirely
Waiting (DaemonController::executeDaemonStop, pure, virtual-clock
tested):
- never kills on its own. Quitting waits as long as the node needs; the
shutdown screen offers "Close wallet — leave node running" after 3 s,
and "Kill node (unsafe)" (confirmed) only once stop was delivered and
the node showed no CPU/I-O progress for ~10 min outside a rebuild
- other callers (restart, rescan, reinstall, switch, ...) wait up to
120 s, let go once the app starts quitting, and keep watching a node
that is still running; wallet switch refuses to go ahead then
- EmbeddedDaemon gains stop-session primitives (beginStop,
pollOwnedProcess, signalGracefulStop, forceKillOwnedProcess,
ownedResourceUsage over the Linux process group / Windows handle,
endStop) and only one stop session runs at a time
- the Force Quit button and its stall heuristic are gone; "Leave
running" aborts a pending stop attempt and closes at once
Verified against the fake daemon with the real GUI: closing mid-rebuild
now delivers stop (or SIGTERM 2 ms after a queue-full refusal) and the
node exits cleanly after its rebuild, where it was SIGKILLed before. The
shutdown screen, its "Leave running" button and the idle Kill offer
(after ~11 min) were checked by screenshot. New strings are English-
only for now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POSIX stop() set should_stop_, which made the monitor thread return
without reaping the child, and then polled the state-based isRunning(),
which stays true while the state is Stopping. So every Linux/macOS stop
waited out its whole grace period, sent SIGTERM, waited 10 s more and
SIGKILLed, even when the node had exited seconds earlier. Closing the GUI
with an idle node took ~30 s; a node still flushing at 30 s was killed.
- stop() joins the monitor first, then polls waitpid(pid, WNOHANG) on
a cached pid, so it is the only reaper and sees the exit at once.
- markStopRequested() (set by App::stopEmbeddedDaemon before the RPC
stop, and by stop() itself; cleared by start()) makes an exit that
lands before stop() runs a clean stop instead of a crash, on POSIX
and in the Windows monitor. crash_count_ drives auto-restart, and a
crash recorded before the request keeps its count.
- The SIGTERM -> SIGKILL escalation stays for now, but only fires for
a daemon that has really not exited. Replacing it with a wait policy
and an explicit user choice is follow-up work.
Verified against the fake daemon: an idle node that exits 3.4 s after
stop used to be SIGTERMed at +20 s and SIGKILLed at +30 s (GUI closed
in 30.6 s); it is now reaped as "exited cleanly" ~50 ms after it exits
(GUI closes in 4.0 s). New POSIX tests cover the clean exit, the early
requested exit and a graceful SIGTERM, each time-bounded so the old
code fails fast.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>