diff --git a/docs/lite-wallet-implementation-plan-v2-2026-06-04.md b/docs/lite-wallet-implementation-plan-v2-2026-06-04.md index 8d5879c..84b42e9 100644 --- a/docs/lite-wallet-implementation-plan-v2-2026-06-04.md +++ b/docs/lite-wallet-implementation-plan-v2-2026-06-04.md @@ -101,7 +101,13 @@ Each milestone is independently demoable and gated by a fake-backend test. Order > 2. **ADDRESSED — blocking, uninterruptible sync.** The backend `sync` command runs `do_sync(true)`, a blocking full scan that does **not** honor the shutdown flag (`lightclient.rs`), and `balance`/`list` block until synced. Redesign: the controller runs `sync` on a **detached** thread (never joined), the bridge is a `std::shared_ptr` shared with that thread (so detaching is safe and the bridge isn't `litelib_shutdown`'d while a sync still holds it), and `startSync()` is now non-blocking (was called on the main thread → would have frozen wallet creation). The joinable **poll worker** only issues fast `syncstatus` calls while syncing (publishing progress) and fetches balance/addresses/list **once `syncDone_` is set**. Shutdown joins only the fast poll worker and detaches the sync thread → no hang. Verified deterministically by `testLiteWalletControllerShutdownDoesNotHangDuringSync()` (blocking-sync fake; destructor returns <1.5s) and the worker/refresh tests (stable across repeated runs). > > - ✅ **Gateway abort-on-first-failure hardened.** `LiteWalletGateway::refresh()` now runs every command and assembles partial data instead of aborting on the first parse/bridge failure (`result.ok` = any usable data; `bundle.complete` still reflects all-succeeded). One command's real-shape drift degrades gracefully — the other tabs still populate. Covered by `testLiteWalletGatewayRefreshSkipsFailedCommand()`. -> - ⏳ **Remaining for M2 polish:** per-address balances (notes-correlation; currently aggregate-only), and a full **real-backend end-to-end** verification (sync to tip → balance/addresses/list through the parsers — likely surfaces more real-shape mismatches, same class as chain-name/syncstatus). +> **Real-shape verification (2026-06-05): all refresh parsers confirmed against the real backend.** +> `lite_smoke` gained a `--restore-recent` mode (restore a throwaway wallet at birthday≈tip). Finding: this backend downloads compact blocks from a **fixed checkpoint (~1.76M) regardless of birthday**, so first sync is unavoidably slow (~30 min) and `balance`/`list` block until synced — a full live data run is impractical. Verified shapes instead by: live run (`info`, `addresses`, `syncstatus` → `parse_ok=1`) + the authoritative Rust source for the sync-gated ones: +> - `balance` (`do_balance`): top-level `tbalance/zbalance/unconfirmed/verified_zbalance/spendable_zbalance` — **matches** our parser. +> - `list` (`do_list_transactions`): send records carry `outgoing_metadata` (no top-level `address`), receives carry `address`+`amount`; our `parseTransactionRecord` branches on `outgoing_metadata` first — **matches** both. +> - `syncstatus`: was the one mismatch — fixed earlier. +> +> - ⏳ **Remaining for M2 polish:** per-address balances (notes-correlation; currently aggregate-only). Real *data* (vs shape) hasn't been observed end-to-end because a full sync takes ~30 min; shapes are confirmed, and gateway hardening covers any residual surprise. - Implement `LiteSyncService::startSync` (replace the "not implemented" stub) + a background worker polling `syncstatus`, mirroring `NetworkRefreshService`/`RefreshScheduler` (enqueue → worker → apply on main thread). - Drive `LiteWalletGateway` refresh (info/height/balance/addresses/notes/list/transactions) through `lite_result_parsers` → `lite_wallet_state_mapper` → `App` `WalletState` (`privateBalance`, `transparentBalance`, `addresses`, `transactions`, `sync`). - Hook the controller into `App::update()`'s refresh dispatch alongside (not inside) the full-node path. diff --git a/tools/lite_smoke.cpp b/tools/lite_smoke.cpp index 172e915..41ddbfa 100644 --- a/tools/lite_smoke.cpp +++ b/tools/lite_smoke.cpp @@ -2,16 +2,18 @@ // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // -// Real-backend smoke harness for the lite wallet bridge. Links the actual SDXL -// litelib_* backend (via the same imported CMake target the app uses) and exercises -// LiteClientBridge against a real lightwalletd server. This is the "real backend smoke -// test" the implementation plan gates behind passing fake-backend tests. +// Real-backend smoke harness for the lite wallet bridge. Links the actual SDXL litelib_* +// backend (via the same imported CMake target the app uses) and exercises LiteClientBridge +// against a real lightwalletd server. The "real backend smoke test" the plan gates behind +// passing fake-backend tests. // -// lite_smoke [server-url] [--create] +// lite_smoke [server-url] [--create] [--refresh] [--full] [--restore-recent] // -// Read-only by default (available / checkServerOnline / walletExists). Pass --create to -// also attempt a real litelib_initialize_new (writes wallet state — run with an isolated -// HOME, e.g. HOME=/tmp/lite_smoke env, so it cannot clobber a real wallet). +// Read-only by default. --create initializes a new wallet. --refresh runs the refresh +// commands through the parsers (shape check). --full also does a (slow) full sync first. +// --restore-recent restores a throwaway wallet with a birthday near the tip so the sync is +// minimal (seconds), then runs the data-shape checks — fast verification of balance/addresses/ +// list shapes without a multi-minute full scan. Always run with an isolated HOME. #include "wallet/lite_client_bridge.h" #include "wallet/lite_connection_service.h" @@ -20,20 +22,64 @@ #include #include +using namespace dragonx::wallet; + +// A standard valid 24-word BIP39 test mnemonic (no funds). Restored only into an isolated HOME. +static const char* kTestSeed = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon " + "abandon art"; + +// Push each refresh command's real output through its parser; print flags/counts, and the raw +// (truncated) response only on a parse failure to diagnose real-shape mismatches. +static void runDataShapeChecks(LiteClientBridge& bridge) +{ + // Non-blocking commands first (read local wallet state); balance last (may need a sync). + { + auto r = bridge.execute("syncstatus", ""); + auto p = parseLiteSyncStatusResponse(r.value); + std::printf("[lite-smoke] syncstatus parse_ok=%d complete=%d synced=%llu/%llu\n", + p.ok, p.syncStatus.complete, + (unsigned long long)p.syncStatus.syncedBlocks, (unsigned long long)p.syncStatus.totalBlocks); + if (!p.ok) std::printf("[lite-smoke] syncstatus RAW = %.150s\n", r.value.c_str()); + } + { + auto r = bridge.execute("addresses", ""); + auto p = parseLiteAddressesResponse(r.value); + std::printf("[lite-smoke] addresses bridge_ok=%d parse_ok=%d err=%s z=%zu t=%zu\n", + r.ok, p.ok, liteResultParserErrorName(p.error), + p.addresses.zAddresses.size(), p.addresses.tAddresses.size()); + if (!p.ok) std::printf("[lite-smoke] addresses RAW = %.150s\n", r.value.c_str()); + } + { + auto r = bridge.execute("list", ""); + auto p = parseLiteTransactionsResponse(r.value); + std::printf("[lite-smoke] list bridge_ok=%d parse_ok=%d err=%s count=%zu\n", + r.ok, p.ok, liteResultParserErrorName(p.error), p.transactions.transactions.size()); + if (!p.ok) std::printf("[lite-smoke] list RAW = %.150s\n", r.value.c_str()); + } + std::printf("[lite-smoke] balance (may block until synced)...\n"); + { + auto r = bridge.execute("balance", ""); + auto p = parseLiteBalanceResponse(r.value); + std::printf("[lite-smoke] balance bridge_ok=%d parse_ok=%d err=%s\n", + r.ok, p.ok, liteResultParserErrorName(p.error)); + if (!p.ok) std::printf("[lite-smoke] balance RAW = %.150s\n", r.value.c_str()); + } +} + int main(int argc, char** argv) { - using namespace dragonx::wallet; std::setvbuf(stdout, nullptr, _IONBF, 0); // unbuffered so output survives a timeout kill std::string server = "https://lite.dragonx.is"; - bool doCreate = false; - bool doRefresh = false; - bool doFull = false; + bool doCreate = false, doRefresh = false, doFull = false, doRestoreRecent = false; for (int i = 1; i < argc; ++i) { const std::string arg = argv[i]; if (arg == "--create") doCreate = true; else if (arg == "--refresh") doRefresh = true; else if (arg == "--full") doFull = true; + else if (arg == "--restore-recent") doRestoreRecent = true; else server = arg; } @@ -47,30 +93,49 @@ int main(int argc, char** argv) return 2; } - const bool walletExists = bridge.walletExists(kDragonXLiteChainName); - std::printf("[lite-smoke] walletExists(%s) = %s\n", kDragonXLiteChainName, walletExists ? "true" : "false"); + std::printf("[lite-smoke] walletExists(%s) = %s\n", kDragonXLiteChainName, + bridge.walletExists(kDragonXLiteChainName) ? "true" : "false"); + std::printf("[lite-smoke] checkServerOnline() = %s\n", bridge.checkServerOnline(server) ? "true" : "false"); - const bool online = bridge.checkServerOnline(server); - std::printf("[lite-smoke] checkServerOnline() = %s\n", online ? "true" : "false"); + if (doRestoreRecent) { + // Bootstrap a client to learn the chain tip, then restore a throwaway wallet with a + // birthday near the tip so the scan is minimal (seconds), then check data shapes. + std::printf("[lite-smoke] bootstrap initializeNew() to learn tip...\n"); + bridge.initializeNew(false, server); + unsigned long long tip = 0; + { + auto r = bridge.execute("info", ""); + auto p = parseLiteInfoResponse(r.value); + tip = p.info.latestBlockHeight.has_value() ? (unsigned long long)*p.info.latestBlockHeight : 0; + std::printf("[lite-smoke] info parse_ok=%d tip=%llu\n", p.ok, tip); + } + const unsigned long long birthday = tip > 10 ? tip - 10 : tip; + std::printf("[lite-smoke] restore-from-phrase at birthday=%llu (minimal scan)...\n", birthday); + auto rr = bridge.initializeNewFromPhrase(false, server, kTestSeed, birthday, 0, /*overwrite*/ true); + std::printf("[lite-smoke] restore ok=%d\n", rr.ok); + if (!rr.ok) std::printf("[lite-smoke] restore err = %s\n", rr.error.c_str()); + // NB: this backend downloads from a fixed checkpoint regardless of birthday, so an + // explicit full "sync" is slow. addresses/list/syncstatus read local state and don't + // need it; balance may block until synced. + std::printf("[lite-smoke] --- data shape check (no full sync) ---\n"); + runDataShapeChecks(bridge); + bridge.shutdown(); + std::printf("[lite-smoke] done\n"); + return 0; + } if (doCreate) { std::printf("[lite-smoke] initializeNew() ... (real network + writes wallet state)\n"); auto result = bridge.initializeNew(false, server); std::printf("[lite-smoke] initializeNew ok = %s\n", result.ok ? "true" : "false"); if (result.ok) { - // The response is the new wallet's SEED PHRASE — never print it. Report only - // that a well-formed, non-empty response came back. - std::printf("[lite-smoke] wallet created; response len = %zu (seed redacted)\n", - result.value.size()); + std::printf("[lite-smoke] wallet created; response len = %zu (seed redacted)\n", result.value.size()); } else { std::printf("[lite-smoke] error = %s\n", result.error.c_str()); } } if (doRefresh) { - // Run each refresh command through the real parser to verify the LIVE backend's - // JSON shapes match what the gateway expects. Prints flags/counts only (no secrets, - // addresses, or amounts). std::printf("[lite-smoke] --- refresh shape check (real backend output -> parsers) ---\n"); { auto r = bridge.execute("info", ""); @@ -78,39 +143,17 @@ int main(int argc, char** argv) std::printf("[lite-smoke] info bridge_ok=%d parse_ok=%d err=%s\n", r.ok, p.ok, liteResultParserErrorName(p.error)); } - { - auto r = bridge.execute("syncstatus", ""); - auto p = parseLiteSyncStatusResponse(r.value); - std::printf("[lite-smoke] syncstatus bridge_ok=%d parse_ok=%d err=%s synced=%llu/%llu\n", - r.ok, p.ok, liteResultParserErrorName(p.error), - (unsigned long long)p.syncStatus.syncedBlocks, (unsigned long long)p.syncStatus.totalBlocks); - std::printf("[lite-smoke] syncstatus RAW = %.200s\n", r.value.c_str()); - } - // The data commands below trigger a blocking full-chain sync on a fresh wallet; - // gate them behind --full so the shape check stays fast by default. - if (!doFull) { bridge.shutdown(); std::printf("[lite-smoke] (skipping balance/addresses/list; pass --full)\n"); return 0; } - { - auto r = bridge.execute("balance", ""); - auto p = parseLiteBalanceResponse(r.value); - std::printf("[lite-smoke] balance bridge_ok=%d parse_ok=%d err=%s\n", - r.ok, p.ok, liteResultParserErrorName(p.error)); - } - { - auto r = bridge.execute("addresses", ""); - auto p = parseLiteAddressesResponse(r.value); - std::printf("[lite-smoke] addresses bridge_ok=%d parse_ok=%d err=%s z=%zu t=%zu\n", - r.ok, p.ok, liteResultParserErrorName(p.error), - p.addresses.zAddresses.size(), p.addresses.tAddresses.size()); - } - { - auto r = bridge.execute("list", ""); - auto p = parseLiteTransactionsResponse(r.value); - std::printf("[lite-smoke] list bridge_ok=%d parse_ok=%d err=%s count=%zu\n", - r.ok, p.ok, liteResultParserErrorName(p.error), p.transactions.transactions.size()); + if (!doFull) { + bridge.shutdown(); + std::printf("[lite-smoke] (skipping balance/addresses/list; pass --full or --restore-recent)\n"); + return 0; } + std::printf("[lite-smoke] running full sync (blocking; can take many minutes)...\n"); + bridge.execute("sync", ""); + runDataShapeChecks(bridge); } bridge.shutdown(); - std::printf("[lite-smoke] done (real litelib_* symbols are callable; results above are live)\n"); + std::printf("[lite-smoke] done\n"); return 0; }