fix(lite): lock/unlock parity — shared attempt-lockout, immediate unlock state, no dead PIN, surfaced encrypt failure
Lite-parity audit, FIX C (gating/validation/presentation only — no encrypt/unlock/send backend call changed): - Lite unlock went straight to the backend with no lockout. New App::unlockLiteWallet() runs the SAME escalating, persisted attempt-lockout as the full node (restoreLockoutState/applyUnlockFailure), wired into the Settings unlock, the send-time prompt and the lock screen. A wrong-passphrase failure counts; an already-unlocked/not-encrypted result does not. - state_.locked is now cleared immediately on a successful unlock (the ~2s status poll lagged, so the UI stayed "locked" after a correct passphrase). - PIN quick-unlock controls are hidden in lite Settings — lite has no PIN vault, so they were dead. - A create/restore whose passphrase encryption FAILS now raises a one-shot visible error (takeEncryptionFailureNotice -> lite_encrypt_failed_unencrypted) instead of only a log line; the wallet is left open but unencrypted. Fixed the flag on BOTH the lifecycle and the direct create/restore encrypt paths. - A background sync save is skipped while an encrypted wallet is unlocked (the backend save re-locks it), resuming once locked. Compile- and unit-verified (testLiteWalletControllerLockUnlockParity covers the save-deferral + encrypt-failure notice, run twice for the flaky guard); the lock-screen lockout/PIN-hiding UX itself is runtime/GUI-only and not exercised here — worth a manual lite GUI pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,7 @@ inline std::atomic<bool> g_liteFakeBlockBalance{false}; // "balance" blocks (a
|
||||
inline std::atomic<bool> g_liteFakeBalanceError{false}; // "balance" returns the backend's plain-text sync error
|
||||
inline std::atomic<long> g_liteFakeBalanceEntered{0}; // calls that reached "balance"
|
||||
inline std::atomic<bool> g_liteFakeBlockEncrypt{false}; // "encrypt" blocks (before changing state)
|
||||
inline std::atomic<bool> g_liteFakeEncryptFails{false}; // "encrypt" returns an error and changes no state
|
||||
inline std::atomic<bool> g_liteFakeBlockImport{false}; // "import"/"timport" block
|
||||
inline std::atomic<bool> g_liteFakeImportFails{false}; // "import"/"timport" reject the key (real do_import_key shape)
|
||||
inline std::atomic<bool> g_liteFakeImportEncrypted{false}; // "import"/"timport" refuse: the wallet is encrypted
|
||||
@@ -261,6 +262,8 @@ inline char* liteFakeExecute(const char* command, const char* args)
|
||||
// unlock/lock toggle locked; decrypt clears it; encryptionstatus reports {encrypted,locked}.
|
||||
if (std::strcmp(c, "encrypt") == 0) {
|
||||
liteFakeWaitWhile(g_liteFakeBlockEncrypt);
|
||||
if (g_liteFakeEncryptFails.load())
|
||||
return liteFakeDup("{\"result\":\"error\",\"error\":\"encrypt failed\"}");
|
||||
g_liteFakeEncrypted = true; g_liteFakeLocked = true;
|
||||
return liteFakeDup("{\"result\":\"success\"}");
|
||||
}
|
||||
|
||||
@@ -10974,6 +10974,66 @@ void testLiteWalletControllerCreateEncryptsWithPassphrase()
|
||||
EXPECT_FALSE(s2.locked);
|
||||
}
|
||||
|
||||
// FIX C: (e) a create-time passphrase whose encryption FAILS is surfaced once via takeEncryptionFailureNotice() and
|
||||
// leaves the wallet unencrypted; (d) the background scan save is skipped while an encrypted wallet is unlocked (the
|
||||
// backend save would re-lock it) and resumes once locked.
|
||||
void testLiteWalletControllerLockUnlockParity()
|
||||
{
|
||||
using namespace dragonx::wallet;
|
||||
const auto caps = makeWalletCapabilities(WalletBuildKind::Lite, false, true);
|
||||
const auto conn = defaultLiteConnectionSettings();
|
||||
dragonx::test::resetLiteFakeCounters();
|
||||
{ // (e)
|
||||
dragonx::test::g_liteFakeEncrypted = false;
|
||||
dragonx::test::g_liteFakeLocked = false;
|
||||
dragonx::test::g_liteFakeEncryptFails = true;
|
||||
auto c = std::make_unique<LiteWalletController>(
|
||||
caps, conn, LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
|
||||
LiteWalletCreateRequest req;
|
||||
req.passphrase = "hunter2hunter2";
|
||||
(void)c->createWallet(req);
|
||||
EXPECT_TRUE(c->takeEncryptionFailureNotice());
|
||||
EXPECT_FALSE(c->takeEncryptionFailureNotice()); // one-shot
|
||||
EXPECT_FALSE(c->encryptionStatus().encrypted);
|
||||
dragonx::test::g_liteFakeEncryptFails = false;
|
||||
}
|
||||
{ // (e) success path raises no notice
|
||||
dragonx::test::g_liteFakeEncrypted = false;
|
||||
dragonx::test::g_liteFakeLocked = false;
|
||||
auto c = std::make_unique<LiteWalletController>(
|
||||
caps, conn, LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
|
||||
LiteWalletCreateRequest req;
|
||||
req.passphrase = "hunter2hunter2";
|
||||
(void)c->createWallet(req);
|
||||
EXPECT_FALSE(c->takeEncryptionFailureNotice());
|
||||
EXPECT_TRUE(c->encryptionStatus().encrypted);
|
||||
}
|
||||
{ // (d)
|
||||
dragonx::test::resetLiteFakeCounters();
|
||||
dragonx::test::g_liteFakeEncrypted = false;
|
||||
dragonx::test::g_liteFakeLocked = false;
|
||||
auto c = std::make_unique<LiteWalletController>(
|
||||
caps, conn, LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
|
||||
EXPECT_TRUE(c->createWallet(LiteWalletCreateRequest{}).walletReady);
|
||||
for (int i = 0; i < 600 && !c->syncComplete(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
EXPECT_TRUE(c->encryptWallet("pw12345678").ok);
|
||||
EXPECT_TRUE(c->unlockWallet("pw12345678"));
|
||||
EXPECT_FALSE(c->encryptionStatus().locked);
|
||||
const long saves0 = dragonx::test::g_liteFakeSaveCount.load();
|
||||
EXPECT_TRUE(c->startRescan());
|
||||
for (int i = 0; i < 600 && !c->syncComplete(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
EXPECT_TRUE(c->syncComplete());
|
||||
EXPECT_EQ(dragonx::test::g_liteFakeSaveCount.load(), saves0); // no background save while unlocked
|
||||
EXPECT_FALSE(c->encryptionStatus().locked); // so the unlock survived the scan
|
||||
EXPECT_TRUE(c->lockWallet());
|
||||
EXPECT_TRUE(c->startRescan());
|
||||
for (int i = 0; i < 600 && !c->syncComplete(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
EXPECT_TRUE(dragonx::test::g_liteFakeSaveCount.load() > saves0); // saves resume once locked
|
||||
}
|
||||
dragonx::test::g_liteFakeEncrypted = false;
|
||||
dragonx::test::g_liteFakeLocked = false;
|
||||
}
|
||||
|
||||
void testLiteWalletControllerAsyncLifecycleFailover()
|
||||
{
|
||||
using namespace dragonx::wallet;
|
||||
@@ -19608,6 +19668,7 @@ int main()
|
||||
testLiteWalletControllerM5Persistence();
|
||||
testLiteWalletControllerEncryption();
|
||||
testLiteWalletControllerCreateEncryptsWithPassphrase();
|
||||
testLiteWalletControllerLockUnlockParity();
|
||||
testLiteWalletControllerAsyncAppliesPassphrase();
|
||||
testLiteNewPassphraseRules();
|
||||
testLiteChainNameMigration();
|
||||
|
||||
Reference in New Issue
Block a user