A wallet.dat placed in the repo root for recovery was untracked but NOT ignored,
so a stray 'git add .' could commit private keys. *.bak already covered the
salvage backups; add wallet.dat / wallet-*.dat / wallet.dat.* explicitly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Restore original wallet" action picked the newest wallet.<ts>.bak — but the
daemon auto-salvages on every failed BDB verify, and each round SHRINKS the wallet
(salvage keeps only readable records + drops the dead-page bloat). In a cascade the
newest .bak is the most-degraded (seen in the wild as "Salvage found no records")
while the original is the oldest and by far the largest.
Pick by file SIZE instead: add largestWalletSalvageBak((name,size) pairs) — the
largest wallet.<digits>.bak is the least-salvaged, i.e. the pristine original (an
emptied salvage is tiny; a real wallet is large); ties break to the newest ts.
Factor the shared parse into parseWalletSalvageBakTs(). restoreOriginalWallet()
now gathers file sizes and uses it (still verifies the pick is a valid BDB before
swapping). newestWalletSalvageBak kept for reference.
Adds a cascade regression test (a 40KB emptied newest .bak must NOT win over the
194MB original). Suite green (1/1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the restore action to the wallet-auto-recovery warning: undo the daemon's
salvage by swapping the untouched original (wallet.<ts>.bak) back over the
salvaged copy and clearing the stale BDB env that triggered the false recovery,
then restarting. Modeled on beginAdoptSeedWallet (stop daemon → file ops →
restart on a worker; result pumped to the main thread for notifications).
Safety (fund-adjacent file ops on a real wallet — copy/rename only, never delete
user data):
- picks the newest wallet.<unixtime>.bak via the pure, unit-tested
newestWalletSalvageBak(); aborts if none.
- verifies the .bak is a real Berkeley DB (probeWalletFile) before touching
anything — won't overwrite a working wallet with a bad backup.
- stops the daemon first (stopDaemonForWalletSwitch) so wallet.dat is released.
- moves the salvaged copy aside to wallet.dat.salvaged-<ts>.dat (kept), COPIES
the .bak into place (the .bak stays), moves database/ aside to
database.pre-restore-<ts>.bak (kept), and drops only the transient __db.*
BDB region files. Rolls back the move if the copy fails.
- relaunches the node even on failure so it's never left down.
The warning dialog now offers Restore original wallet / Open data folder /
Keep salvaged copy. Full-node only; lite-safe. Build clean, suite green (1/1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dragonxd auto-recovers a wallet.dat that fails BDB verification on startup — no
flag needed (CWallet::Verify → CDBEnv::Verify(walletFile, CWalletDB::Recover)):
it moves the original to wallet.<timestamp>.bak, salvages readable keys into a
fresh wallet.dat, and keeps running. The salvage can be incomplete (or the whole
thing a FALSE POSITIVE from stale/cross-platform BDB env state — __db.* / the
database/ dir carried between machines), so the node silently comes up on a
possibly-empty wallet. To the user that reads as fund loss, with no warning.
Detect it and warn loudly instead:
- daemon/daemon_startup_diagnosis.h: pure walletAutoRecovered() (the salvage /
"Original wallet.dat saved as wallet.<ts>.bak" markers) + newestWalletSalvageBak()
(picks the wallet.<unixtime>.bak the recovery just made).
- onConnected() scans the node's captured output once per session; on a match it
shows a warning dialog + notification: the ORIGINAL is safe in wallet.<ts>.bak,
the shown balance may be incomplete, and here are the exact steps to restore it
(rename the .bak back + delete the stale database/ + __db.* env). One-click
"Open data folder" jumps straight there. Full-node only; lite-safe.
Deliberately does NOT auto-swap the wallet files (untested per-platform file
manipulation on a real wallet is not worth the risk) — it informs + guides.
Adds walletAutoRecovered / newestWalletSalvageBak coverage to
testBlockDbOutputDiagnosis. Suite green (1/1).
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>
The wallets list badged the active/linked row from the runtime seed status
(activeWalletSeedBadge → wallet_seed_status_, reset only on disconnect) in
preference to the offline on-disk probe. Two issues let a genuinely legacy
wallet render as "seed phrase":
- The offline probe's budget-fallback branch dropped the fMnemonicSeed flag:
res.mnemonic was set only in the (parsed && complete) branch. The probe shares
a 768 MB budget across all wallet files, so a large wallet (e.g. a 194 MB one)
probed after the budget is spent falls into the fallback, loses its seed/legacy
classification (mnemonic → 0), and the row defers to the runtime badge.
- With mnemonic == 0, the code used the runtime badge, which can still carry a
HasMnemonic from a previously-active mnemonic wallet — mislabelling the legacy
wallet.
Fix:
- Carry the definitive positives (fMnemonicSeed/hdSeed/mkey) from a cap-truncated
btree walk — a found marker is authoritative even when the scan didn't finish.
- Make the on-disk fMnemonicSeed read take precedence: it's the SAME flag the
daemon's IsMnemonicSeed()/z_exportmnemonic consult, so a definitive read wins;
the runtime badge is used only when the probe genuinely couldn't decide, and
never overrides a definitive on-disk classification.
Verified the wallet in question is truly legacy (fMnemonicSeed=false on disk,
matching the daemon's CHDChain serialization + IsMnemonicSeed). The flag reader
(hdChainMnemonicFlag) is already unit-tested; suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit of the full-node RPC response parsers and updater release-body parsers
(find -> adversarially-verify workflow) surfaced three worth fixing; three
others guard formats the project doesn't emit and are backstopped by signature
verification, so they're documented rather than churned.
- Price (Medium): parseCoinGeckoPriceResponse used .value(key, 0.0), which
throws type_error on a PRESENT null. CoinGecko emits null for usd_24h_change/
usd_24h_vol on illiquid tokens (DRGX is one) while still returning a valid
spot price; the outer catch turned that into no price update at all. Read
null-tolerantly so the valid usd/btc survives.
- Daemon updater (Medium): parseDaemonChecksums blanked '|'/backtick but not
markdown emphasis, so a bolded **archive.zip** checksum row was dropped and a
valid, correctly-signed release would be refused. Also blank '*'/'_' (cannot
cause a wrong-asset match; the 64-hex + .zip-suffix tests are unchanged).
- Opid poll (Low, severe failure mode): parseOperationStatusPoll read id/status
via .value() (throws on a present non-string) and the call site parsed OUTSIDE
its try/catch, so a throw left opid_poll_in_progress_ stuck true and wedged all
z-operation polling for the session. Type-check the reads and parse inside the
guard. (dragonxd can't emit non-string id/status; this is defense-in-depth.)
Regression tests: CoinGecko null field; opid non-string id/status; **bold**
checksum row. Suite green (1/1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The lite first-run restore wizard enabled Restore for {12,15,18,21,24}-word
phrases, but the SDXL backend only accepts 24-word / 32-byte-entropy seeds:
LightWallet::new does copy_from_slice(&phrase.entropy()) into a [u8;32]
(lightwallet.rs:231), which panics on 16/20/24/28-byte entropy. Mnemonic::
from_phrase accepts the shorter valid phrase, and the restore FFI
litelib_initialize_new_from_phrase (lib.rs:127) has no catch_unwind (unlike
litelib_execute), so the panic unwinds across extern "C" -> process abort
(UB on the pinned rustc 1.63). A user restoring a legitimate 12-word seed
from another wallet crashed the app.
The Settings restore gate was already tightened to == 24 (6ff1fda) but the
first-run wizard gate (df14533) was never updated — same restore path, two
verdicts, crash only via the more-common first-run path.
Add shared util/seed_phrase.{h,cpp} as the single source of truth:
- normalizeSeedPhrase: fold NBSP/en/em/ideographic/narrow spaces to ASCII,
strip zero-width marks, collapse+trim (word bytes untouched)
- seedPhraseWordCount
- isCompleteRecoveryPhrase(int) == 24 (the sole SDXL contract)
Both restore gates now count via the normalizer and gate via
isCompleteRecoveryPhrase, and both submit the normalized phrase. This closes
the crash, reconciles the two gates so they can't drift again, and — because
tiny-bip39 splits on literal ASCII space with no NFKD — makes an NBSP-pasted
24-word seed (common from PDFs/note apps) restore correctly instead of being
undercounted and rejected.
Adds testSeedPhraseHelpers. Suite green (1/1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit of the lite import path found no false-rejection defect (no client-side
gate; the two-command fallback in importKey makes the U/5/K/L prefix guess
non-binding; lite send reuses the now-P2SH-fixed send_tab helpers). But the
"zxview" viewing-key comment — which was WRONG in the full node (fixed earlier)
— is genuinely CORRECT here: SDXL's import takes an extended full viewing key
(zxviews…, hrp_sapling_viewing_key), whereas the full node's z_importviewingkey
takes an incoming viewing key (zivks…). The two are not interchangeable.
Add a note so nobody "harmonizes" the two gates and reintroduces the full-node
bug. Comment-only; no behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Same defect class as the import-key fix: a hardcoded prefix/length pre-filter
layered over the checksum validators rejected valid addresses before the daemon
saw them. The send-screen recipient gate required a[0]=='R', and the payment-URI
parser accepted only 'R'/'t' with rigid length bands — so every valid P2SH /
multisig address (DragonX SCRIPT_ADDRESS=85 → 'b…') was silently refused, leaving
the Send button disabled with no usable recipient.
Centralize recipient recognition in util/address_validation:
- isTransparentAddress: Base58Check with a 21-byte version+hash160 payload —
covers P2PKH ('R…', v60) AND P2SH ('b…', v85) on every network, rejects WIF
keys / typos by real checksum.
- isShieldedAddress: Bech32 + a Sapling payment-address HRP (zs / ztestsapling /
zregtestsapling), distinguishing a payment address from a viewing key.
- isValidRecipientAddress: either of the above.
send_tab's two validity helpers (the single choke point for all 5 call sites) and
the payment-URI format check now route through these. The URI parser now
checksum-validates the recipient (fail-fast on transcription errors) rather than
being prefix/length-only.
Tests use real checksummed vectors (P2PKH/P2SH/shielded, WIF- and typo-rejection);
testPaymentUri updated off its old fake fixed-char addresses. Suite green (1/1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The client-side pre-check rejected legitimate keys before the daemon ever
saw them, surfacing "Unrecognized key format" / a cryptic daemon "Invalid"
error. Two concrete defects plus the brittle heuristic behind them:
- Viewing keys: isViewingKey looked for Zcash's "zxview" extended-FVK
prefix, but DragonX's z_exportviewingkey emits a Sapling *incoming*
viewing key (HRP "zivks"), which z_importviewingkey is the only form the
daemon decodes. Every real DragonX viewing key was refused. (F1)
- Uncompressed transparent WIF: the length+first-char heuristic accepted
{5,K,L,U} only, but a version-188 uncompressed key starts with '7'. (F2)
Replace the heuristic with structural validation using the existing
checksum validators (F3): add util::decodeBase58Check (checksum-stripped
payload) and util::bech32Hrp (HRP of a valid Bech32 string). Transparent
keys are now accepted by decoding Base58Check and checking the payload is a
33/34-byte secret key with a DragonX SECRET_KEY version byte (188 main/
regtest, 128 testnet) — covering compressed and uncompressed, rejecting
addresses/typos by real checksum. Viewing keys are matched by the real
incoming-VK HRPs (zivks / zivktestsapling / zivkregtestsapling).
The Sweep gate and the dialog's live type indicator run off the same
predicates, so they are fixed too (F4). Messaging now names the likely
cause and appends a wrong-coin/network hint to the daemon's raw "Invalid"
error (F5).
Adds testPrivateKeyImportRecognition plus decodeBase58Check/bech32Hrp
coverage; suite green (1/1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The alert-history popup content sat flush against the popup's top and bottom edges. Add a
padY spacer above the header and below the content (on both the empty and populated paths).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full-node ObsidianDragon 2.0.0 -> 2.0.1 (single source of truth: the project() VERSION in
CMakeLists.txt). Verified the generated header renders "2.0.1 (ObsidianDragon)". The Lite
variant is versioned independently (DRAGONX_LITE_VERSION, unchanged at 1.0.0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Follow-up to the node-banner / staleness-badge / alert-history features — a 5-dimension
finder->verify review surfaced 4 real issues (the ImGui-stack-balance finder found none):
- Alert popup grew off the right edge: pivot (0,1) pinned the panel's LEFT edge at the
bell, which sits near the window's right edge, so a 320px panel overflowed rightward
(an explicit SetNextWindowPos pivot skips ImGui's on-screen clamp). Anchor the
bottom-RIGHT corner at the bell instead (pivot (1,1) at bellMax.x) so it grows left.
- Staleness badge could flash red on reconnect: WalletState::clear() reset everything
except the four last_*_update stamps, so the pre-outage timestamp survived and the
badge briefly showed "Updated Nm ago" the same frame the node banner cleared. Zero the
stamps in clear() (all readers treat 0 as "never"; app_network.cpp:1473 guards != 0).
- Banner min-height floor wasn't DPI-scaled: std::max(minH, baseH*vScale()) now uses
minH * dpiScale() so both operands are in scaled px.
- New i18n keys weren't in res/lang/: back-filled all 16 diagnostics/QoL keys into the 8
language files, additively (128 insertions, 0 deletions). zh/ja/ko reworded around 2
glyphs missing from the CJK subset and hard-asserted tofu-free against the subset font.
Build-clean both variants; ctest 1/1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Toasts fade in 1-4s, so anything that scrolled past was gone. Notifications now retains
every pushed alert in a capped (100) ring buffer with a wall-clock epoch (AlertRecord) —
separate from the 5-item live-toast deque — plus a monotonic total_pushed_ counter.
A bell in the status-bar right cluster opens an upward popup listing recent alerts
newest-first: severity icon + colour (reusing the toast palette), the message, and a
relative age (formatTimeAgoShort), with a Clear-all action. An unread dot on the bell,
coloured by the most-severe unseen alert, marks alerts that arrived since the panel was
last opened — driven by totalPushed() deltas so it survives capping/clearing.
Thread note: every push is on the UI thread (RPC results run as main-thread MainCb
callbacks), matching this class's existing lock-free model; documented as a
no-raw-worker-thread invariant.
New i18n keys (alerts_*). Build-clean; ctest 1/1 (adds testNotificationHistory: retention,
order, cap, monotonic counter, clear). Closes the QoL bundle and the Foundation tier.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the wallet is connected but the balance has quietly stopped refreshing — a busy
daemon can fail z_gettotalbalance without dropping the whole connection (only *both*
core RPCs failing 3x triggers a disconnect) — the old number sits on screen while the
node-status banner stays hidden. The Total Balance card now shows a small pill on its
status line ("Updated 2m ago", amber, escalating to red past 3 min) so the stale value
isn't silently trusted; hovering explains it and points at the node connection.
No refresh-path changes: WalletState::last_balance_update is already stamped only on a
successful fetch (network_refresh_service.cpp), so the badge reads it and computes age
against the same std::time clock via util::formatTimeAgoShort. The decision is a pure,
unit-tested helper (ui/staleness_badge.h::evaluateStalenessBadge, 45s/180s thresholds)
gated on connected so it never contradicts the banner.
Closes P2 (5/5). Build-clean; ctest 1/1 (adds testStalenessBadge).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A persistent horizontal strip now appears at the top of the content column whenever the
wallet can't reach its node — unlike the transient toasts it stays up for as long as the
fault persists, so an offline wallet is never silently mistaken for a working one.
The show/severity/action decision is a pure, unit-tested function
(ui/node_status_banner.h::evaluateNodeStatusBanner) fed a state snapshot by the new
App::renderNodeStatusBanner(). Three cases:
- full-node offline -> amber, "Reconnect" (App::tryConnect)
- embedded daemon crashed
& auto-restart gave up -> red, "Restart node" (App::restartDaemon)
- lite wallet open failed -> red, message-only
Suppressed during the wizard / wallet-switch / daemon-restart / screenshot-sweep / shutdown,
and while an expected startup phase (warmup / init / connect-in-progress) already owns the
screen. Banner height lives in res/themes/ui.toml (banners.node-status); colours come from the
material semantic palette; the detail text is ellipsis-clipped so it can't push the action
button off-screen. Drawn before the content edge-fade vertex capture so it stays fully opaque.
New i18n keys (node_banner_*). Build-clean both variants; ctest 1/1 (adds testNodeStatusBanner).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Settings (logging section) gains two support-friendly actions, now that the logging
foundation actually produces logs (W7-2):
- Open log folder: opens the config dir (Platform::openFolder) so users can find
dragonx-debug.log / dragonx-crash.log.
- Copy diagnostics: copies a plaintext support snapshot to the clipboard via the new
App::buildDiagnosticsReport() — version, build variant, platform, connection status,
active wallet path + existence + size, encryption/lock state, sync heights, and (full-
node) daemon status/running/crash-count/lastError, plus the log paths. No secrets.
Build-clean; ctest 1/1. Remaining QoL: persistent alert history, a daemon/RPC error
banner, and the W6-2 refresh-staleness badge. See docs/wallet-hardening.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Foundation tier — answers the original "make it easier to diagnose problems" ask.
- W7-2 (keystone): the app-level Logger file sink was never initialized, so LOG/LOGF/
VERBOSE_LOGF went nowhere and dragonx-debug.log didn't exist on Linux/macOS at all.
main() now calls Logger::init(<config>/dragonx-debug.log) on every platform. Fixed a
latent deadlock this exposed: init() wrote its banner via write(), which re-locks the
non-recursive mutex_ it already holds — now written directly. On Windows the raw
stdout/stderr freopen moved to a separate dragonx-stdout.log so the two writers don't
contend on one file. Added testLoggerFileSink (also a deadlock guard — it would hang if
the fix regressed).
- W7-3: no crash handler existed on Linux/macOS. Added an async-signal-safe sigaction
handler (SIGSEGV/ABRT/BUS/FPE/ILL) that writes the signal id + a backtrace_symbols_fd
backtrace to dragonx-crash.log, then re-raises the default disposition for a core dump —
the POSIX counterpart of the Windows SEH filter.
- W7-4: Logger::init now rotates the log to a single .1 backup past 10 MB, so a long or
verbose session can't grow it unbounded.
Build-clean; ctest 1/1. Remaining Foundation: the QoL bundle (mostly UI). See
docs/wallet-hardening.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
P2 robustness batch (localized):
- W5-1 (Med, lite): persistAfterBroadcast returned false on a persistent post-send/shield
save failure, but both callers discarded it and it never logged — completely silent. It
now liteLogs the failure (the spent note re-derives on the next sync, so it's a
robustness gap, not fund loss).
- W5-2 (Med, lite): the post-sync and post-rescan save results (in the detached scan
threads) were ignored; both now liteLog on failure. LiteDiagnostics::log is mutex-guarded,
so it's safe from those threads.
- W6-1 (Med): WalletState::clear() didn't reset mining/pool_mining, so a wallet switch could
briefly show the previous wallet's hashrate/blocks. Now reset in clear() (the daemon
restarts on switch, so mining genuinely stops).
- W6-3 (Low): AddressBook::load() cleared entries_ then threw on the first non-object array
element — discarding EVERY contact. It now guards is_object() + per-entry try/catch,
skipping and counting malformed entries.
Build-clean; ctest 1/1. Remaining P2: W6-2 (refresh-staleness badge — needs UI, overlaps
the diagnostics Foundation bundle). See docs/wallet-hardening.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
- W1-1 (High): switchToWallet never verified the target wallet file exists before
switching. dragonxd auto-creates a fresh empty wallet for a missing -wallet=<name>, so a
moved/deleted wallet file silently "opened" as a brand-new empty wallet with a zero
balance — looking exactly like fund loss. It now std::filesystem::exists-checks
datadir/<walletFile> before switching (ahead of the daemon-stop prompt) and blocks with a
"not found (moved or deleted?)" warning. Because the check runs regardless of 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,
which dragonxd also prints for DB_TOO_NEW (a newer-version wallet) — so a version mismatch
was offered a -salvagewallet repair that cannot fix it. The generic match is now excluded
when the output also contains "newer version".
Build-clean; ctest 1/1. Remaining P1-B: W1-3 (syncedHere timing) + the startup-path
existence check. See docs/wallet-hardening.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrate-to-seed (legacy -> mnemonic wallet) moves real funds; three correctness fixes:
- W3-1 (High): beginAdoptSeedWallet swapped a hardcoded datadir/wallet.dat instead of the
ACTIVE wallet file. With a non-default active wallet (e.g. wallet-2.dat) it installed the
swept seed wallet into an unloaded wallet.dat and left the daemon reloading the emptied
legacy wallet — swept funds only recoverable via the seed phrase. Now swaps
datadir + "/" + getActiveWalletFile(), captured on the main thread (switching is blocked
during migration, so no race).
- W3-2 (High): SeedWalletCreator::create() ran remove_all(<config>/seed-migrate)
unconditionally at the start, so a prior migration that swept funds into the temp wallet
but was abandoned/crashed before adopting would have that fund-bearing wallet destroyed.
It now refuses (with a clear message) when DRAGONX/wallet.dat already exists — a completed
migration removes the dir on adopt, so a leftover means an unfinished one.
- W3-4 (Med): switchToWallet blocked switching only while the migration dialog was open;
closing it via "Later" mid-migration dropped the guard. Now also blocks while
getSeedMigrationPending().
Build-clean; ctest 1/1. Remaining P1-A: W3-3 (persist the sweep opid). See
docs/wallet-hardening.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
P0-B encryption-integrity cluster.
W2-2: the first-run wizard's "encrypt" stored the passphrase only in memory and let the
user into the app immediately, so a quit/crash or a failed daemon connect before the
deferred encryption applied left the wallet unencrypted with NO record encryption was
ever requested — the user believing it was encrypted. A persisted encryption_pending
settings flag is now set the moment encryption is requested (never the passphrase, only
the fact). refreshWalletEncryptionState() reconciles it on every connect: wallet observed
encrypted -> clear the flag; wallet NOT encrypted while the flag is set and no deferred
encryption is pending/in-flight -> a once-per-session "your wallet is NOT encrypted — open
Settings to finish" warning (the flag stays set, so it recurs each launch until resolved).
The passphrase is deliberately never persisted to auto-complete — surfacing it is the
secure choice.
W2-4: lockWallet()'s continuation only handled success — a failed walletlock RPC silently
left the wallet UNLOCKED (an unfulfilled auto-lock). It now logs and warns once (reset on
the next successful lock) so a failing auto-lock is visible instead of leaving the wallet
exposed.
Touches settings.{h,cpp}, app_wizard.cpp, app_security.cpp, app.h. Not unit-testable at
this layer (RPC/connect-driven state). Build-clean; ctest 1/1. See docs/wallet-hardening.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The lite create/open/restore requests carried a passphrase field that the UI collected
(a labeled, masked "passphrase" input) but the backend initialize* calls never used —
so a user could believe their lite wallet was passphrase-protected at creation when it
did nothing. It now has a real meaning, wired in LiteWalletController:
- create / restore -> encryptWallet(passphrase): the backend encrypts + locks + saves
the brand-new wallet.
- open -> unlockWallet(passphrase), but only when encryptionStatus() reports the existing
wallet is actually encrypted + locked (no spurious unlock on an unencrypted wallet).
encryptWallet/unlockWallet take their own copy of the passphrase and wipe it; the
request copy is still wiped as before. A post-create encrypt failure is liteLog'd (the
wallet still exists, so the create is not failed).
Six existing lite-controller tests carried an incidental "hunter2" create passphrase from
when the field was dead; removed (they exercise non-encryption flows and want an
unencrypted wallet), and added testLiteWalletControllerCreateEncryptsWithPassphrase to
prove the new behavior. Completes the wallet-hardening P0-A cluster (7/7). ctest 1/1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The seed-phrase "Save" already wrote the file 0600 and zeroed the in-memory buffer, but
the success message was a bare "Saved to <path>" — no hint that it's a permanent
UNENCRYPTED copy of the seed at a predictable location. The message now reads
"Saved an UNENCRYPTED seed file — move it to secure offline storage and delete this
copy: <path>". English source updated; the res/lang back-fill of this changed key is
deferred to the batch i18n pass.
Also documents W5-3 (lite create-time passphrase) as a product decision rather than a
speculative change: the field is already wiped on every path (minimal security risk),
but the labeled masked "passphrase" input at lite create/open/restore is never consumed
by the backend — so either remove the dead UI or wire it into the lite encrypt flow.
Finishes the actionable part of the wallet-hardening P0-A cluster (docs/wallet-hardening.md).
Build-clean; ctest 1/1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The wallet-hardening memzero cluster. Uses the file's established sodium_memzero
pattern (matching the existing lambda-capture scrub at app_network.cpp:2885 and the JSON
scrub at :4025) rather than a new type, since importPrivateKey/sweepPrivateKey are
fund-moving code.
- W4-1 importPrivateKey / sweepPrivateKey: the spending/viewing key was copied ≥3×
(calling frame -> worker-lambda capture -> JSON params) and never scrubbed. Now zeroed
on all paths: the calling-frame copy after the worker post, the lambda's captured copy
(lambda made mutable, zeroed once the request is sent), and the request params copy.
- W4-3 exportAllKeys / backupWallet: the concatenated all-keys buffer is now zeroed after
the consumer uses it, and the backup is written via
Platform::writeFileAtomically(..., restrictPermissions=true) — atomic and owner-only
(0600) — instead of a umask-default std::ofstream that left it world-readable.
- W2-3 decrypt-wallet passphrase: std::move-captured into the worker lambda (no plaintext
copy left in the calling frame) and sodium_memzero'd right after unlockWallet, its only
use.
Not unit-testable (no observable RPC effect — the key value sent to the daemon is
unchanged; only post-use memory zeroing is added). Build-clean; ctest 1/1. See
docs/wallet-hardening.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removing wallet encryption z_exportwallet'd every private key in cleartext to
<datadir>/obsidiandecryptexport<ts>, re-imported it, and never deleted it — leaving a
full plaintext dump of every key on disk permanently. The decrypt flow now scrubs
(best-effort in-place zero-overwrite) and removes that file as soon as the
z_importwallet attempt resolves, on both the success and failure paths. Recovery, if
ever needed, remains the encrypted backup (wallet.dat.encrypted.bak), never this file.
Second fix in the wallet-hardening P0-A cluster (docs/wallet-hardening.md). Not
unit-testable (fs I/O in a deep worker lambda); build-clean, ctest 1/1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The RPC console echoed and stored typed commands verbatim, so `walletpassphrase
<secret>`, `z_importkey <key>`, `encryptwallet <pass>` etc. left the secret in the
visible log AND the 100-entry recall history (copyable). Adds a pure, unit-testable
RedactConsoleCommand()/ConsoleCommandCarriesSecret() (allowlist of 13 secret-bearing
first-tokens) in console_tab_helpers; submitConsoleCommand() now echoes and stores
`> walletpassphrase ****` while still executing the real command unredacted. Bare
secret commands and non-secret commands pass through unchanged.
Output-secret commands (dumpprivkey / z_exportkey / z_exportmnemonic) — whose secret is
in the RESULT — are a separate redaction concern, tracked as a follow-up.
First fix in the wallet-hardening P0-A cluster (see docs/wallet-hardening.md). New
testConsoleSecretRedaction (11 assertions); ctest 1/1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Links the real EmbeddedDaemon into the ObsidianDragonTests target (its deps were
already present) and adds two POSIX integration tests that exercise the actual
fork/exec/waitpid fixes headlessly:
- testExecFailureReported (F2): start() against a non-executable file must fail with a
precise "not executable or wrong architecture" reason.
- testDaemonCrashDetected (F1): a short-lived child that exits abnormally is still
detected (crash_count_ increments) while isRunning() is hammered from the test
thread — a regression test for the reap race.
Writing the F2 test surfaced a real bug: start()'s failure branch called
setState(State::Error, "Failed to start dragonxd process"), and setState stores the
Error message into last_error_ — clobbering the precise message startProcess() had
just set, so getLastError()/the UI only ever saw the generic string. Fixed to pass the
preserved detail to setState, so the precise reason survives and now also reaches the
state callback (crash panel / status).
ctest 1/1, green including the two new integration tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduces CHANGELOG.md (Keep a Changelog style, Unreleased section) covering this
batch, with F8's breaking change — remote plaintext RPC now refused by default —
called out front and center along with the rpctls=1 / rpcallowplaintext=1 recovery
steps. Also records the Security / Fixed / Added entries for F1-F7. Updates the
tracking doc's status to reflect the completed release-notes + i18n back-fill and the
remaining pre-release items (F1/F2 manual repros, CJK subset-font rebuild).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Additively adds translations for the 6 new keys from this batch
(sb_daemon_extract_failed, sb_daemon_files_failed, loading_stall_{title,body,hint},
sb_plaintext_remote_blocked) across res/lang/*.json. es/de/fr/pt/ru get all 6; for
zh/ja/ko a string is only added when every glyph is already in the current
NotoSansCJK-Subset.ttf, since the subset can't be rebuilt here — 6 zh/ja/ko entries
whose glyphs aren't yet subsetted are left on the English fallback rather than render
as tofu. Written sorted, indent=4, ensure_ascii=False (matching add_missing_translations.py);
purely additive (42 insertions, 0 removals). The remaining 6 need a
scripts/build_cjk_subset.py font rebuild.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
F8 (security). Two related fixes to how the wallet decides whether an RPC target is
safe to send Basic-auth credentials to:
- isLocalHost() was matching any host that merely *starts* "127." via
rfind("127.",0)==0, so "127.evil.com" (and "127.0.0.1.attacker", "127.300.0.1",
"1270.0.0.1") were misclassified as loopback and treated as local. It now uses a
strict isExactIPv4Loopback() parser: exactly four 0-255 dot-separated octets with
the first == 127. localhost / ::1 / [::1] handling is unchanged.
- A remote rpchost over plain HTTP (no rpctls=1) previously only produced a
dismissible warning and then sent rpcuser:rpcpassword in cleartext, where a
local-network MITM could capture them. tryConnect() now REFUSES that connection
(clear status line + one-time notification, no creds sent) unless the user opts in
explicitly with rpcallowplaintext=1 in DRAGONX.conf (new
ConnectionConfig::allow_plaintext_remote, parsed in parseConfFile; policy in the
new allowsPlaintextRemote()). Local/embedded daemons and rpctls=1 remotes are
unaffected.
BREAKING: a wallet configured for remote plaintext RPC will stop connecting until
rpcallowplaintext=1 (or rpctls=1) is added to DRAGONX.conf. Must be called out in the
release notes. The Settings-toggle UI is deferred (the conf-key opt-in is the recovery
path; see docs/daemon-startup-hardening.md).
Adds testIsLocalHost and testAllowsPlaintextRemote to test_phase4.cpp; one i18n key
(English) added to i18n.cpp.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
F3: the daemon connect loop retried forever with only an animated spinner when the
daemon was reachable-but-never-ready (stuck in RPC warmup / -28, or an external daemon
that never finishes init) -- no error, no guidance, no escape. It now stamps
connect_stall_since_ the moment the daemon first goes "reachable but not ready" (the
warmup branch + applyDaemonInitStatus) and clears it on connect / disconnect /
warmup-complete. A pure, unit-testable util::connectHasStalled() helper (new
util/connect_stall.h, 45s default from ui.toml [screens.loading].stall-timeout-sec)
drives a "Taking longer than expected" notice in renderLoadingOverlay(): a title, a
reassuring body with elapsed seconds, and a full-node hint to Settings > Restart Daemon
or the Console. The background retry keeps running underneath, so the notice self-clears
the instant it connects. Guarded off while the daemon is in State::Error (that case is
owned by the existing crash-count hint).
The overlay is a pure draw-list layer with no interactive widgets, so this follows the
existing crash-hint idiom (guidance text, not injected buttons); the stalled state is
computed locally in the overlay, so the only new App member is connect_stall_since_.
Adds testConnectHasStalled to test_phase4.cpp and three i18n keys to i18n.cpp (English
source of truth; the res/lang/*.json back-fill is deferred to a single
add_missing_translations.py run at the end of the batch).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three verified daemon-startup edge-case fixes centered on the config/params
filesystem path:
- F7: new non-throwing Platform::ensureDirectory(dir, outError) with one
consistent "Cannot create <dir>: <reason>. Check permissions / free space."
message. Replaces the unchecked/throwing create_directories sites at main.cpp
(pre-init: log + Windows MessageBox + return 1), connection.cpp's
autoDetectConfig (was the *throwing* overload -- could raise an uncaught
filesystem_error through its callers; now sets the new
ConnectionConfig::dir_error), and both app.cpp daemon-dir sites (surface via
daemon_status_ + return false). The primary connect path (app_network.cpp)
checks dir_error and shows it instead of mislabelling it "waiting for config".
embedded_resources.cpp already checked its error_code, so it is left as-is.
- F6: verifySaplingParams() now hash-verifies each param against its pinned
canonical SHA-256 (source of truth: scripts/build-lite-backend-artifact.sh)
instead of only checking existence, so a truncated / corrupt-but-present param
is rejected up front rather than failing later on a shielded operation. A
<params_dir>/.sapling_verified marker keyed on size:mtime avoids re-hashing
~48MB on every startup. Logic extracted to the injectable, unit-testable
verifySaplingParamsIn(dir, digests); reuses util::sha256Hex (no new hash impl).
- F5: startEmbeddedDaemon() now checks extractEmbeddedResources()'s return and
the previously-dropped copy_file error_code in the daemon-binary fallback loop,
aborting with a clear status (sb_daemon_extract_failed / sb_daemon_files_failed)
instead of failing opaquely at spawn. An absent source file stays non-fatal.
Adds testPlatformEnsureDirectory and testVerifySaplingParams to test_phase4.cpp.
i18n keys added to i18n.cpp (English source of truth); the res/lang/*.json
back-fill via add_missing_translations.py is deferred to a single run at the end
of the batch. Progress tracked in docs/daemon-startup-hardening.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three verified daemon-startup edge-case fixes in the embedded-daemon process
lifecycle (all in embedded_daemon.{cpp,h}):
- F1: EmbeddedDaemon::isRunning() (POSIX) now reads the atomic state_ instead of
calling waitpid(WNOHANG) from the UI thread, which raced monitorProcess()'s own
reap. waitpid is one-shot: whichever thread won consumed the exit status; if
isRunning() won, the monitor never saw the crash, so crash_count_/State::Error
and the 3-strike restart cap were silently lost. monitorProcess() is now the sole
reaper (predicate Running || Stopping keeps stop()'s wait loops correct). Mirrors
the existing XmrigManager::isRunning() fix.
- F2: startProcess() (POSIX) adds a close-on-exec self-pipe exec handshake. On a
non-executable / wrong-arch / corrupt binary, execv fails in the child and the
parent now learns synchronously (reads errno vs EOF), reaps the zombie, sets a
precise last_error_ ("not executable or wrong architecture"), and returns false
-- instead of reporting State::Running for a daemon that never started. Uses
pipe()+FD_CLOEXEC (not pipe2) so the branch stays shared with macOS. Parent-side
setpgid is now best-effort + logged.
- F4: start() gates on a lingering datadir lock after the port check. A graceful
shutdown releases the RPC port ~90s before the datadir .lock, so a rapid
stop->start spawned a daemon that died on the lock and, three times in ~12s,
tripped the 3-strike crash cap before the lock cleared. start() now polls
isDaemonProcessRunning() with a bounded ~300ms wait and bails with a distinct
non-crash Error (no crash_count_ bump) that the connect loop retries once the
lock clears. Isolated migrate-to-seed starts (skip_port_check_ / -datadir
override) are exempt.
Adds the testDatadirLockGate unit test (pure evaluateDatadirLockGate matrix) to
test_phase4.cpp. Plan and progress tracked in docs/daemon-startup-hardening.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Several related mining-tab pool improvements:
- Report the default pool fee correctly: pool.dragonx.is is 1%, not 0%.
The registry constant was hardcoded to 0. It now also fetches the live
poolFee from the pool's /api/stats alongside hashrate (no extra
request), so the displayed fee self-corrects and falls back to the
compile-time value only when the fetch hasn't landed.
- Show fractional fees: new FormatFeePercent trims trailing zeros so
whole fees read "1%" and fractional ones keep their decimals ("1.5%").
- Surface saved + custom pools in the pool list card: the list is now
the union of the official pools, the user's saved favorites, and the
currently-mined pool (effectivePools), each a selectable, endpoint-
deduped row. Previously the card only showed the hardcoded knownPools().
- Fix the xmrig "user" field: the "Payout Address" field now drives the
pool login rewards are credited to (resolveMiningUserAddress), instead
of being written only to "pass" while "user" was auto-derived from the
wallet's own first z-address -- which silently ignored a configured
payout address and could route rewards to the wrong address.
Unit tests cover parsePoolFee, FormatFeePercent, effectivePools, and
resolveMiningUserAddress; full app + ObsidianDragonTests build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The pinned librustzcash / transitive crates (notably traitobject 0.1.0) rely on
pre-1.70 trait coherence and fail to compile on newer rustc (E0119), so the backend
must build with 1.63. Add a rust-toolchain.toml in the vendored backend so rustup
auto-selects 1.63 when cargo runs there — no more manual RUSTUP_TOOLCHAIN=1.63.0.
The pin is scoped to the backend tree (repo-root cargo keeps the default toolchain).
Also symlink the pin into the prepared build root so --silentdragonxlitelib-dir
builds honor it too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- The macOS DMG filename used a separate hardcoded DMG_BASENAME ("DragonX_Wallet"),
so the export didn't match the .app/zip (ObsidianDragon). Derive it from
APP_BASENAME: full-node -> ObsidianDragon-*.dmg, lite -> ObsidianDragonLite-*.dmg.
The mounted volume + CFBundleName keep the "DragonX Wallet" display branding.
- build_release_mac did `rm -rf "$out"`, wiping all of release/mac on every build, so
building one variant destroyed the other's artifacts. Scope the cleanup to the
current variant's files (as the Linux/Windows release paths already do) so the
full-node and lite releases coexist.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The --mac-release path (full-node and --lite-backend) had never run on a real
Mac and broke on several Linux-only assumptions:
- build.sh version parser used GNU sed \+ (BSD sed reads it literally), so it
aborted before compiling — switched to POSIX [[:space:]][[:space:]]*.
- build.sh libsodium universal check used GNU grep \| — switched to grep -E.
- libwebp's cpu.cmake applies -mno-sse2 to its reference DSP files; under a
universal build (-arch arm64;x86_64) that lands on the x86_64 slice, where it
disables _Float16 and breaks the SDK 26 <math.h>. Added an idempotent
FetchContent PATCH_COMMAND (cmake/patch-libwebp-simd.cmake) to neutralize it.
- The SDXL lite backend static lib needs Security + CoreFoundation frameworks on
macOS; added them to the imported dragonx_lite_backend target for APPLE.
- Added a DRAGONX_MAC_ARCHS override plus auto-detect: with --lite-backend the
app is built for the arch(es) the backend .a actually provides (its pinned
ring 0.16.11 is x86_64-only), instead of failing to link a universal app.
- build-lite-backend-artifact.sh uses bash 4+ (mapfile); added a re-exec guard
for macOS's stock bash 3.2 and added bash to setup.sh's macOS core deps.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
setup.sh --win ran `sudo apt-get install` (and update-alternatives)
unconditionally, forcing the whole run under sudo even when mingw-w64 was
already present. Running setup as root makes the daemon cross-compile run as
root, leaving root-owned artifacts under external/dragonx that break `make
clean` on a later non-sudo build (stale objects relink -> mingw link failure
recurs). Gate the apt/update-alternatives block behind a presence check so
`./setup.sh --win` runs sudo-free (and the daemon build as the invoking user)
when the toolchain is already installed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds `sign-daemon-release.sh release <secret.key> <version>`: zips each staged
prebuilt-binaries/dragonxd-{linux,mac,win}/ into dragonx-<version>-{linux-amd64,
macos,win64}.zip, signs each (detached ed25519), and prints the SHA-256 checksum
table for the release body. Platforms with no staged daemon are skipped; warns
if the signing key doesn't match the pubkey pinned in daemon_updater.h. Keeps the
existing keygen/pubkey/sign subcommands.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Point the miner build at DragonX/drg-xmrig and rename the prebuilt staging
dir xmrig-hac -> drg-xmrig across setup.sh, build.sh, the legacy Windows
script, .gitignore and README.
setup.sh now clones/builds the miner from git.dragonx.is/DragonX/drg-xmrig
(was dragonx/xmrig-hac), and the prebuilt staging dir is renamed
prebuilt-binaries/xmrig-hac -> prebuilt-binaries/drg-xmrig across build.sh, the
legacy Windows build script, .gitignore and README.
Also aligns setup.sh's section-8 directory list with section 7 (it previously
used a bare "xmrig" token, creating a stray empty prebuilt-binaries/xmrig/),
and updates the xmrig_manager.cpp header comment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In table view the table is inset into its glass panel by tVpad and its
outer_size is listH-2*tVpad, so the post-table cursor ended tVpad (~10px) above
where the cards/list views leave it, pulling the "N address saved" footer up.
Land the cursor at the glass-panel bottom (tpMin.y + listH) so the footer lines
up across all three view styles.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- balance: inset the address-row favorite (star) button by the card's inner
padding so it mirrors the left margin instead of hugging the card edge.
- mining: remove pool.dragonx.cc from the built-in default pools (pool.dragonx.is
is now the sole default); update the pool-registry test accordingly.
- mining: middle-truncate saved pool-URL and payout-address dropdown rows (new
shared material::TruncateToWidth helper) so a full z-address no longer runs
under the trailing delete (X) button.
- mining: fix the thread-grid cells overflowing the card at >100% display
scaling — the reserved Mine-button width used a raw clamp that didn't scale;
scale it by dp so cols is estimated correctly (no-op at 100%).
Full-node build + test suite green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dev-branch audit (docs/_archive/security-audit-dev-2026-07-22.md) re-found the
master issues (absent on dev) plus new ones in dev-only code. Applied here;
full-node build + test suite green; the subtle fixes were adversarially re-verified.
Ported from the master remediation (adapted to dev's code):
- bootstrap: reject zip-slip / path-traversal archive members (isSafeArchivePath)
before writing (S2-1). (dev already fail-closes on a missing checksum.)
- xmrig updater: fail closed when a signature is required but no key is pinned (F1-1).
- http_download.httpGetString: 16 MiB hard cap + MAXFILESIZE on the shared
metadata/price fetch (F1-2 / caps the updater + exchange paths at one site).
- rpc_client: explicit SSL_VERIFYPEER/VERIFYHOST (F4-1) and a 256 MiB response
cap in WriteCallback (F4-3).
- lite_connection_service: reject remote http:// lite servers, loopback only (L1-1).
Loopback is matched by a strict dotted-decimal 127.0.0.0/8 check (not a
startsWith("127.") prefix, which would wrongly accept 127.0.0.1.evil.com), with
userinfo/fragment stripping.
- lite controller: propagate encrypt/decrypt save() failure instead of reporting
success (F7-1).
- xmrig_manager: chmod(0600) the pool config before writing secrets (F5-2).
- app: clear the copied secret from the OS clipboard on shutdown (F3b-1).
- export_transactions: neutralize CSV/spreadsheet formula injection (F13-1).
- build pipeline: build-from-source lite backend + remove the self-attested
CMake signature gate (F15-1); pinned+verified appimagetool (F15-3/4);
verified Sapling params in setup.sh (F15-6); build.sh exits 0 on success.
(F14-1 empty-quoted-arg and F8-2 NUL-termination were already fixed on dev.)
Dev-only findings:
- rpc_client.callRaw: scrub the raw buffer + parsed tree (templated scrubJsonSecrets
for ordered_json) so console dumpprivkey/z_exportkey keys don't linger in freed
heap (N1-1).
- seed_wallet_creator: wipe the exported mnemonic on the failure path so a discarded
failed result never carries a live seed (W1-2).
- export_all_keys: write the plaintext key dump 0600 + atomically via
writeFileAtomically (U1-2).
- chat_database: restrict chat_messages.sqlite and its WAL/SHM sidecars to owner-only (C3-1).
Not done (need a decision, documented in the report):
- Chat header metadata (cid/z/p) rides outside the AEAD (C1/C2) — binding it is a
wire-protocol change requiring SilentDragonXLite interop review.
- Bootstrap lacks an offline-rooted signature (S2-2 residual) — needs signing infra.
- Console scrollback retains console-typed key-export output in plaintext (N1-1
residual) — inherent to an echoing console; would need output redaction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>