fake_dragonxd now answers rescan, importprivkey, z_importkey and
z_importviewingkey, so the GUI's long wallet RPCs can be reproduced
without a real node.
- Each one holds cs_main for FAKE_RESCAN_SECS (default 0). The lock-free
getrescaninfo reports the scan while it runs, and, like v1.3.0, the
scan stops early when a shutdown is requested.
- Argument checks, error codes and return values follow dragonxd
(z_importkey returns null, importprivkey the t-address, the default
"whenkeyisnew" skips a known key, z_importviewingkey needs its zaddr).
Keys are only prefix-checked and never stored or logged; an imported
spending key gets a made-up address that the address lists then show.
- FAKE_ENCRYPTED=1 / FAKE_WALLET_PASSPHRASE: an encrypted wallet that
starts locked, with walletpassphrase (-14 on a wrong passphrase),
walletpassphrasechange, walletlock and getwalletinfo unlocked_until;
the imports answer -13 while it is locked. As in dragonxd, the right
passphrase with a non-integer timeout unlocks first and only then
fails (-1): the wallet stays unlocked with no relock timer of its
own. A timer pending from an earlier unlock keeps running; otherwise
only walletlock locks it again. Without FAKE_ENCRYPTED, those three
methods answer -15 like dragonxd.
- FAKE_METHODS_ALL call events show keys and passphrases as <redacted>.
A plain run is unchanged: the same 63 GUI calls before and after, and
identical replies to every one of them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Keep in each address" posted one task that called z_mergetoaddress for
every batch of every address back to back; each call is a whole-wallet
scan under cs_main, and the daemon runs the ops one at a time anyway.
It is now an App-owned services::ConsolidationRun:
- one RPC at a time; the next merge only after the previous batch left the
queue and a gap as long as the last call took (2-120 s);
- pauses while the node syncs, the wallet is locked, a send of ours is in
progress, or another wallet scan is running;
- failures classified: nothing that may have queued is ever resent; a
breaker stops after 3 dead addresses or 3 failed ops in a row;
- each merge is charged to the scan budget; one re-read and one forced
balance at the end;
- it keeps running when the dialog closes: a status-bar chip, a run view
with progress, the wait reason and a Stop button.
The "Into one" merge can no longer be confirmed twice while its call is
in flight. fake_dragonxd gains FAKE_SCAN_SECS, FAKE_MERGE_SERIAL and
FAKE_MERGE_FAIL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Connect gate: after the node starts answering, wallet scans wait until the
lock state is known and the node knows its tip (or the wallet has fewer
than 500 transactions), at most 20 s. A node already behind latches the
sync profile from the connect's getinfo. Meanwhile a list-only pass keeps
the address list and the cached history up, and the session's first
balance and per-address balances are read once even while catching up,
so the wallet shows its last-synced values instead of 0.
- Background: minimized, hidden, or covered without focus (3 s debounce,
any input cancels) polls only the chain state every 15 s. Wallet scans,
the mining poll, price, nudges and auto-lock interaction tracking pause;
auto-lock now fires while minimized. Leaving a held profile marks every
timer due again.
- Tab entry marks the timers due instead of forcing scans; a stale list or
a never-loaded history still loads on entry.
- The scan entry points own the hard gates, failed enumerations keep the
previous lists behind a retry hold, and failed balance calls never
overwrite known balances with 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The balance, address scan, history and probe were throttled one by one at
10% each, which added up to 30-40% of dragonxd's cs_main with all of them
due. services::WalletScanGovernor runs one wallet scan at a time across
both RPC lanes and keeps one shared quiet clock at 15% of wall-clock (cut
when the node is behind). Waiters are served first in, first out, with a
head start for what the page shows and a starvation floor of max(30 s
visible / 900 s hidden, 15x the stream's cost). Explicit work (a dirty
set, the user's send, a first load) is never refused but is charged.
Routine scans run only when the tip, the mempool size (getmempoolinfo:
no cs_main) or a wallet-mutation epoch changed since their last good scan,
or a safety sweep is due. Pages that don't show the balance poll it at
most every max(10 min, 50x its cost) (2 min with client-side auto-shield);
coming back into view with an old value fetches it at once. The staleness
badge counts from the last poll that confirmed nothing changed, and hides
while the budget holds the balance within its guarantee.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Users who keep funds apart under different labelled addresses can now
shed notes (smaller wallet, faster scans/witness rebuilds) without the
funds being merged into a single address.
- Merge/Consolidate dialog: "Where should the funds go?" - Into one
address (unchanged) or Keep in each address. The latter lists shielded
addresses with 2+ spendable notes (label, note count, amount;
too-small ones disabled) and merges each address's notes back into
that address with z_mergetoaddress [addr] addr. Addresses with more
notes than the batch size get several batches (the daemon locks a
batch's notes before returning, so calls take disjoint notes); the
dialog suggests a second run after those confirm. Per-address
progress; ops keep running if the dialog is closed. The Consolidate
(wallet-size) preset opens in this mode; Merge keeps "Into one".
Shielded only: a transparent address can't take a merge's output.
- services/note_consolidation: groupSpendableNotes, worthConsolidating,
estimateBatches, consolidateInPlace (pure; driven by a merge-call
function), with unit tests against a stand-in following dragonxd's
rules.
- fake_dragonxd: optional note wallet (FAKE_WALLET_NOTES,
FAKE_MERGE_SECS) with z_mergetoaddress and its operations.
- i18n: 17 new keys in all 8 languages; CJK subset rebuilt.
- UI sweep: modal-consolidate target.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- getrescaninfo takes no lock in dragonxd (it only reads wallet fields),
so the stub no longer holds it behind cs_main. Holding it there made
the GUI's rescan-progress poll look like an abandoned request every
30 s during a simulated rebuild.
- a `methods` event lists the 10 most-requested JSON-RPC methods with
counts (alongside `stats`, and at exit): what the GUI is polling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Holds cs_main like a witness rebuild but logs no rebuild markers: a node
that is busy while its output says nothing (an external node, or a
rebuild path that prints no progress), for exercising the GUI's
"busy but alive" handling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scripted runs nobody needs to watch can run the GUI without a window:
OpenGL still loads, and connecting and shutting down behave as usual.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Lite-configured tree builds ObsidianDragonLite and has no embedded
daemon, so any bin/ObsidianDragon left in it is a stale full-node binary
that the harness would otherwise run silently. Point --build-dir at a
full-node tree instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/test-windows.sh cross-compiles ObsidianDragonTests.exe (and the
fake_dragonxd stub) with mingw into build/windows-tests, reusing the
toolchain and dependency sources from build/windows, and runs it from
WSL through Windows interop (or wine). All tests pass on Windows.
CMake:
- Windows forced BUILD_TESTING OFF in the cache to keep curl's tests
out, which also removed ObsidianDragonTests from every Windows build.
Hide it from curl with a scoped variable instead, and restore our
cache value afterwards (curl's cmake_dependent_option FORCE-writes
it). Windows still defaults to OFF, so release builds are unchanged.
- ObsidianDragonTests now depends on copy_langs: i18n.cpp includes the
generated embedded/lang_*.h, so a tree that builds only the tests
failed (latent on Linux too).
- Link the Windows system libraries the tests need.
Tests:
- testTmpPath/fixtureDir/removeTree helpers: no hard-coded /tmp, a
runtime fixture-dir override (the compiled-in path is a Linux one),
and best-effort cleanup that can't abort the suite.
- Close the explorer cache before deleting its directory (Windows
can't delete an open file).
- Compile out the address-book and wallet-index round-trips on Windows:
getConfigDir() uses SHGetFolderPath there, which no environment
variable redirects, so they would overwrite the real
%APPDATA%\ObsidianDragon files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scriptable fake dragonxd that reproduces, in minutes, the incident where
the GUI hard-killed a node busy with an hour-long witness rebuild:
- dragonxd's HTTP-RPC queue (8 workers + 16 queued, -rpcthreads /
-rpcworkqueue honoured): a full queue answers an immediate plain-text
500 "Work queue depth exceeded", and a request whose client gave up
keeps its slot until a worker reaches it
- a global cs_main held by a "witness rebuild" that logs the real
markers; every method except stop/help takes it
- warmup -28 with the daemon's allow-list, an optional startup replay
- dragonxd's shutdown order: stop (or SIGTERM on POSIX) unbinds RPC,
503s open connections, waits for the rebuild, then logs
"Shutdown: In progress..." / "Shutdown: done" and exits 0
Everything is appended to <datadir>/fake_dragonxd.events; a missing
"clean-exit" line means the process was killed. One portable C++17 file
(POSIX + Winsock), OFF by default (-DDRAGONX_BUILD_FAKE_DAEMON=ON), built
into <build>/fake_dragonxd/ so it can't shadow the real bin/dragonxd.
run-gui-with-fake.sh runs the GUI against it in a throwaway HOME.
Against master's GUI on Linux it reproduces the bug: closing during a
rebuild queues stop behind cs_main-blocked workers, then the GUI sends
SIGTERM and SIGKILL mid-rebuild, with no clean exit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>