fix(lite): address adversarial audit findings in session's lite work

Re-audited this session's lite-wallet changes (originally written at medium
effort) and fixed the genuine issues found:

- walletReady (open path): litelib_initialize_existing returns the bare string
  "OK", which is NOT valid JSON, so the previous `json::accept(value)` check
  marked a *successful* open as not-ready. Key off a non-empty success response
  instead (the bridge already maps "Error:"/null to failure). Drops the now
  unused nlohmann include.
- sync progress: while the detached sync thread is still running, syncDone_ is
  authoritative — don't surface the backend's transient idle syncstatus
  ({"syncing":"false"} -> parser progress=1.0/complete=true) as a misleading
  100%/done. Force complete=false and zero the bogus 1.0 in the progress model.
- per-address balance: also exclude `pending` outputs (notes/utxos from an
  unconfirmed received tx) so per-address figures match confirmed/available.
- secret wiping: the settings page left the page-local request copies
  (input.request.*Request.{passphrase,seedPhrase}) unwiped, and the
  validation-only fallback path wiped nothing. Replace the single-path memzero
  with an RAII scrubber that wipes both the UI char buffers and the request
  string copies on every return path.
- concurrency: document that concurrent bridge->execute() is intentionally
  unguarded — litelib serializes wallet access internally via
  Arc<RwLock<LightWallet>>, so a C++ mutex is unnecessary and would defeat the
  sync/syncstatus concurrency the design relies on. syncLaunched_ -> atomic.

Tests: fake backend now returns the real init shapes (seed object for
create/restore, bare "OK" for open) and a new open-path case guards the
walletReady regression. Removed an unreliable alloc==freed leak assert from the
thread-bearing controller test (kept in the thread-free bridge test). Also fixed
a stray CMake indent and removed ~220MB of untracked build/debug scratch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 11:28:37 -05:00
parent 2daea67a1e
commit 043cdc7128
7 changed files with 76 additions and 25 deletions

View File

@@ -51,13 +51,19 @@ inline char* liteFakeDup(const char* s)
}
inline bool liteFakeWalletExists(const char*) { return g_liteFakeWalletExists; }
inline char* liteFakeInitNew(bool, const char*) { return liteFakeDup("{\"result\":\"created\"}"); }
// Match the real litelib_* return shapes so tests exercise the production walletReady path:
// create/restore return a seed object ({"seed":..,"birthday":..}); open returns the bare
// string "OK" (NOT JSON) — see litelib_initialize_existing.
inline char* liteFakeInitNew(bool, const char*)
{
return liteFakeDup("{\"seed\":\"fake seed phrase words\",\"birthday\":0}");
}
inline char* liteFakeInitFromPhrase(bool, const char*, const char*,
unsigned long long, unsigned long long, bool)
{
return liteFakeDup("{\"result\":\"restored\"}");
return liteFakeDup("{\"seed\":\"fake seed phrase words\",\"birthday\":0}");
}
inline char* liteFakeInitExisting(bool, const char*) { return liteFakeDup("{\"result\":\"opened\"}"); }
inline char* liteFakeInitExisting(bool, const char*) { return liteFakeDup("OK"); }
inline char* liteFakeExecute(const char* command, const char* args)
{
// new-address generation returns a JSON array with the new address (type from args: zs/R).

View File

@@ -4458,7 +4458,10 @@ void testLiteWalletControllerLifecycle()
const auto liteCaps = makeWalletCapabilities(WalletBuildKind::Lite, /*embeddedDaemon*/ false, /*liteBackendLinked*/ true);
const LiteConnectionSettings conn = defaultLiteConnectionSettings();
// create -> wallet ready, walletOpen() true, persist callback fires once, no leak
// create -> wallet ready, walletOpen() true, persist callback fires once.
// (No alloc==freed leak assert here: a ready wallet launches the detached sync thread +
// refresh worker, which are still in flight at end-of-scope and legitimately hold owned
// strings. The leak/double-free invariant is checked in the thread-free bridge test.)
{
dragonx::test::resetLiteFakeCounters();
int persistCount = 0;
@@ -4474,7 +4477,22 @@ void testLiteWalletControllerLifecycle()
EXPECT_TRUE(result.operation == LiteWalletLifecycleOperation::CreateNew);
EXPECT_TRUE(controller.walletOpen());
EXPECT_EQ(persistCount, 1);
EXPECT_EQ(dragonx::test::g_liteFakeAlloc, dragonx::test::g_liteFakeFreed);
}
// open existing -> ready. Regression guard: the real backend's open path
// (litelib_initialize_existing) returns the bare string "OK", which is NOT valid JSON, so
// walletReady must key off a non-empty success response, not JSON validity.
{
dragonx::test::resetLiteFakeCounters();
dragonx::test::g_liteFakeWalletExists = true;
LiteWalletController controller(liteCaps, conn, LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
LiteWalletOpenRequest req;
req.passphrase = "hunter2";
const auto result = controller.openWallet(req);
EXPECT_TRUE(result.ok);
EXPECT_TRUE(result.walletReady);
EXPECT_TRUE(result.operation == LiteWalletLifecycleOperation::OpenExisting);
EXPECT_TRUE(controller.walletOpen());
}
// restore-from-seed round-trips to ready