fix: FAQ CJK glyphs, outgoing-tx history, lite encryption, migrate & sync safety
Bug-report fixes plus adversarial-audit follow-ups. Bumps full 2.0.1->2.0.2 and lite 1.1.0->1.1.1. i18n / fonts: - Rebuild the NotoSansCJK subset with the glyphs the FAQ back-fill introduced (什/么/门/做); Chinese FAQ titles rendered as "??" on zh (and ja/ko). Transaction history (enumeration gaps): - Surface outgoing shielded z->z sends via z_listsentbyaddress (listtransactions and z_listreceivedbyaddress never report them). - Surface z->t deshields by parsing z_listsentbyaddress transparentSends (which z_viewtransaction does not expose); dedupe t->t against listtransactions. - Key send-row dedup on address so equal-value multi-output sends aren't dropped. Lite wallet (key safety): - The async create/restore/open path the UI uses now applies the passphrase: encrypt a new/restored wallet, unlock an existing one. It previously discarded the passphrase, storing the seed/keys in PLAINTEXT (create/restore) or leaving an encrypted wallet locked (open). Migrate-to-seed (fund safety): - Persist a sweep-submitted marker at broadcast so a resumed migration can't treat a ~0 balance (an unconfirmed in-flight sweep) as an empty wallet and adopt (swap wallet.dat) before the sweep confirms; best-effort locate routes to the confirm gate. Sync / threading: - Gate isSynced() on a peer-derived tip (tip_known) so a peerless node isn't reported synced (Send against a stale balance); add a "no peers" status banner. - Guard daemon_status_ with a mutex (monitor-thread write vs UI-thread read). Tests: extends tests/test_phase4.cpp across all of the above; suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwycPDQSfEKtrTS2uC4JeT
This commit is contained in:
@@ -450,31 +450,47 @@ void LiteWalletController::pumpAsyncOpen()
|
||||
bool LiteWalletController::beginCreateWalletAsync(LiteWalletCreateRequest request)
|
||||
{
|
||||
auto req = std::make_shared<LiteWalletCreateRequest>(std::move(request));
|
||||
return beginLifecycleRequestAsync(
|
||||
// Encrypt the brand-new wallet with the user's passphrase once it opens. The async worker
|
||||
// can't (walletOpen_ isn't set until pumpLifecycleResult runs), so stash it and apply on the
|
||||
// main-thread finalize — otherwise the passphrase is discarded and the seed stored in
|
||||
// plaintext (audit C1). Mirrors the synchronous createWallet() wrapper.
|
||||
stashPendingLifecycleSecurity(req->passphrase, PendingSecurityOp::Encrypt);
|
||||
const bool started = beginLifecycleRequestAsync(
|
||||
"Create",
|
||||
[req](LiteWalletLifecycleService& svc, const std::string& url) {
|
||||
req->serverUrl = url;
|
||||
return svc.createWallet(*req);
|
||||
},
|
||||
[req]() { secureWipeLiteSecret(req->passphrase); });
|
||||
if (!started) clearPendingLifecycleSecurity();
|
||||
return started;
|
||||
}
|
||||
|
||||
bool LiteWalletController::beginOpenWalletAsync(LiteWalletOpenRequest request)
|
||||
{
|
||||
auto req = std::make_shared<LiteWalletOpenRequest>(std::move(request));
|
||||
return beginLifecycleRequestAsync(
|
||||
// An existing wallet may be encrypted+locked — unlock it with the supplied passphrase on the
|
||||
// main-thread finalize (the async worker can't; walletOpen_ isn't set yet). Without this an
|
||||
// encrypted wallet opens but stays locked and the passphrase is never verified (audit M1).
|
||||
stashPendingLifecycleSecurity(req->passphrase, PendingSecurityOp::Unlock);
|
||||
const bool started = beginLifecycleRequestAsync(
|
||||
"Open",
|
||||
[req](LiteWalletLifecycleService& svc, const std::string& url) {
|
||||
req->serverUrl = url;
|
||||
return svc.openWallet(*req);
|
||||
},
|
||||
[req]() { secureWipeLiteSecret(req->passphrase); });
|
||||
if (!started) clearPendingLifecycleSecurity();
|
||||
return started;
|
||||
}
|
||||
|
||||
bool LiteWalletController::beginRestoreWalletAsync(LiteWalletRestoreRequest request)
|
||||
{
|
||||
auto req = std::make_shared<LiteWalletRestoreRequest>(std::move(request));
|
||||
return beginLifecycleRequestAsync(
|
||||
// Encrypt the restored wallet with the user's passphrase once it opens (same rationale and
|
||||
// main-thread finalize as create — audit C1).
|
||||
stashPendingLifecycleSecurity(req->passphrase, PendingSecurityOp::Encrypt);
|
||||
const bool started = beginLifecycleRequestAsync(
|
||||
"Restore",
|
||||
[req](LiteWalletLifecycleService& svc, const std::string& url) {
|
||||
req->serverUrl = url;
|
||||
@@ -484,6 +500,8 @@ bool LiteWalletController::beginRestoreWalletAsync(LiteWalletRestoreRequest requ
|
||||
secureWipeLiteSecret(req->seedPhrase);
|
||||
secureWipeLiteSecret(req->passphrase);
|
||||
});
|
||||
if (!started) clearPendingLifecycleSecurity();
|
||||
return started;
|
||||
}
|
||||
|
||||
bool LiteWalletController::beginLifecycleRequestAsync(
|
||||
@@ -585,15 +603,54 @@ void LiteWalletController::pumpLifecycleResult()
|
||||
// log the failure otherwise (shared with the synchronous lifecycle path).
|
||||
onLifecycleResult(out);
|
||||
if (out.walletReady) {
|
||||
// Wallet is now open (walletOpen_ set by onLifecycleResult) — apply the stashed passphrase:
|
||||
// encrypt a freshly created/restored wallet, or unlock an existing encrypted one (C1/M1).
|
||||
applyPendingLifecycleSecurity();
|
||||
lastOpenError_.clear();
|
||||
lastOpenWarming_ = false;
|
||||
} else {
|
||||
clearPendingLifecycleSecurity(); // wallet didn't open — drop the stashed passphrase
|
||||
lastOpenError_ = out.error.empty() ? out.status.message : out.error;
|
||||
lastOpenWarming_ = liteOpenErrorIsWarmup(lastOpenError_);
|
||||
}
|
||||
lastLifecycleResult_ = std::move(out);
|
||||
}
|
||||
|
||||
void LiteWalletController::stashPendingLifecycleSecurity(const std::string& passphrase,
|
||||
PendingSecurityOp op)
|
||||
{
|
||||
clearPendingLifecycleSecurity(); // wipe any stale prior stash first
|
||||
pendingLifecyclePassphrase_ = passphrase; // independent copy; wiped in apply/clear
|
||||
pendingLifecycleSecurityOp_ = op;
|
||||
}
|
||||
|
||||
void LiteWalletController::applyPendingLifecycleSecurity()
|
||||
{
|
||||
const PendingSecurityOp op = pendingLifecycleSecurityOp_;
|
||||
if (op == PendingSecurityOp::None || pendingLifecyclePassphrase_.empty() ||
|
||||
!walletOpen_.load() || !bridge_) {
|
||||
clearPendingLifecycleSecurity();
|
||||
return;
|
||||
}
|
||||
if (op == PendingSecurityOp::Encrypt) {
|
||||
// encryptWallet() takes its own copy and wipes it; the backend encrypts + locks + saves.
|
||||
const auto enc = encryptWallet(pendingLifecyclePassphrase_);
|
||||
if (!enc.ok)
|
||||
liteLog("wallet created/restored but ENCRYPTION FAILED — wallet is NOT encrypted: " +
|
||||
enc.error);
|
||||
} else if (op == PendingSecurityOp::Unlock) {
|
||||
if (!unlockWallet(pendingLifecyclePassphrase_))
|
||||
liteLog("wallet opened but unlock failed (wrong passphrase?)");
|
||||
}
|
||||
clearPendingLifecycleSecurity();
|
||||
}
|
||||
|
||||
void LiteWalletController::clearPendingLifecycleSecurity()
|
||||
{
|
||||
secureWipeLiteSecret(pendingLifecyclePassphrase_); // no-op if already empty
|
||||
pendingLifecycleSecurityOp_ = PendingSecurityOp::None;
|
||||
}
|
||||
|
||||
void LiteWalletController::startSync()
|
||||
{
|
||||
if (syncLaunched_.exchange(true)) return;
|
||||
|
||||
Reference in New Issue
Block a user