Found during review of #1246 (the 2.5.0 back-merge). The code arrived via #1266 and is on master; nothing here blocks that PR.
What happens
WipeWalletUseCase.stopNode() calls backupRepo.reset() as its first action, which resets both VSS clients and clears the store-id cache. If a VSS setup() is still in flight at that moment, the wipe leaves the old wallet's store id cached again, and it survives until the process dies.
Sequence:
BackupRepo.startObservingBackups() launches vssBackupClient.setupWithRetry { … } in an untracked scope.launch. stopObservingBackups() cancels backupJobs, statusObserverJobs, dataListenerJobs and periodicCheckJob, but not this one.
setup() captures val gate = isSetup, reads the store id, and enters withTimeout(30.seconds) around the network call.
- The user starts a wipe.
reset() runs isSetup.cancel(), installs a fresh deferred under synchronized(this) (while setup() holds setupMutex, a different lock), and calls vssStoreIdProvider.clearCache().
- The network call returns.
gate.complete(Unit) is a no-op on the cancelled deferred, so runCatching still succeeds, onFailure never runs, and the isSetup === gate swap is skipped. setup() returns Result.success with the live deferred never completed.
- That success runs
.onSuccess { scope.launch { vssBackupClientLdk.setup() } }, which calls vssStoreIdProvider.getVssStoreId(walletIndex) again.
keychain.wipe() has not run yet — it comes after lightningRepo.stop() (up to 30 s) and cleanupRemote() — so the old mnemonic is still readable and the old store id is derived and cached.
clearCache() is only ever called from VssBackupClient.reset(), so nothing clears it afterwards.
Why it matters
If the user creates or restores a wallet in the same process, LightningService calls vssStoreIdProvider.getVssStoreId(walletIndex) and gets the old wallet's store id, then builder.buildWithVssStore(vssUrl, vssStoreId, lnurlAuthServerUrl, …). The new node is built against the old wallet's VSS store, and VssBackupClientLdk.setup() early-returns on the already-completed gate, so the LDK deletes in LightningRepo point there too.
Unverified: whether the VSS server binds a store id to the LNURL-auth identity. If it does, the new node fails to start until the app is restarted. If it does not, the new wallet's LDK state is written under the old wallet's store id.
The orphaned deferred on its own is harmless: every real caller runs setup() again before awaiting, so nothing hangs. The store-id re-caching is the part worth fixing.
Reachability
Low but real: the user has to reach Settings → Back up or reset → Reset and Restore → Reset while the first setup() after launch is still running. That window is a few seconds on a healthy network and up to 30 s on a slow one, which is exactly when the retry loop exists.
Suggested fix
One line in each client, on the success path — VssBackupClient.kt and VssBackupClientLdk.kt:
check(gate.complete(Unit)) { "VSS client reset during setup" }
A reset that lands mid-setup then turns the call into a failure: onFailure no-ops on the cancelled gate, the isSetup === gate swap is correctly skipped, setupWithRetry reports the failure, and the LDK setup launch never fires — so the old store id is never re-derived.
Worth pairing with either of:
- clearing the store-id cache on the wallet create/restore path, so a stale entry can never outlive a wipe;
- assigning that
scope.launch to a job and cancelling it in stopObservingBackups().
Files
app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt — setup(), reset()
app/src/main/java/to/bitkit/data/backup/VssBackupClientLdk.kt — same shape
app/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt — getVssStoreId(), clearCache()
app/src/main/java/to/bitkit/repositories/BackupRepo.kt — startObservingBackups(), stopObservingBackups()
app/src/main/java/to/bitkit/usecases/WipeWalletUseCase.kt — stopNode(), wipeLocal()
app/src/main/java/to/bitkit/services/LightningService.kt — buildWithVssStore call site
Found during review of #1246 (the 2.5.0 back-merge). The code arrived via #1266 and is on
master; nothing here blocks that PR.What happens
WipeWalletUseCase.stopNode()callsbackupRepo.reset()as its first action, which resets both VSS clients and clears the store-id cache. If a VSSsetup()is still in flight at that moment, the wipe leaves the old wallet's store id cached again, and it survives until the process dies.Sequence:
BackupRepo.startObservingBackups()launchesvssBackupClient.setupWithRetry { … }in an untrackedscope.launch.stopObservingBackups()cancelsbackupJobs,statusObserverJobs,dataListenerJobsandperiodicCheckJob, but not this one.setup()capturesval gate = isSetup, reads the store id, and enterswithTimeout(30.seconds)around the network call.reset()runsisSetup.cancel(), installs a fresh deferred undersynchronized(this)(whilesetup()holdssetupMutex, a different lock), and callsvssStoreIdProvider.clearCache().gate.complete(Unit)is a no-op on the cancelled deferred, sorunCatchingstill succeeds,onFailurenever runs, and theisSetup === gateswap is skipped.setup()returnsResult.successwith the live deferred never completed..onSuccess { scope.launch { vssBackupClientLdk.setup() } }, which callsvssStoreIdProvider.getVssStoreId(walletIndex)again.keychain.wipe()has not run yet — it comes afterlightningRepo.stop()(up to 30 s) andcleanupRemote()— so the old mnemonic is still readable and the old store id is derived and cached.clearCache()is only ever called fromVssBackupClient.reset(), so nothing clears it afterwards.Why it matters
If the user creates or restores a wallet in the same process,
LightningServicecallsvssStoreIdProvider.getVssStoreId(walletIndex)and gets the old wallet's store id, thenbuilder.buildWithVssStore(vssUrl, vssStoreId, lnurlAuthServerUrl, …). The new node is built against the old wallet's VSS store, andVssBackupClientLdk.setup()early-returns on the already-completed gate, so the LDK deletes inLightningRepopoint there too.Unverified: whether the VSS server binds a store id to the LNURL-auth identity. If it does, the new node fails to start until the app is restarted. If it does not, the new wallet's LDK state is written under the old wallet's store id.
The orphaned deferred on its own is harmless: every real caller runs
setup()again before awaiting, so nothing hangs. The store-id re-caching is the part worth fixing.Reachability
Low but real: the user has to reach Settings → Back up or reset → Reset and Restore → Reset while the first
setup()after launch is still running. That window is a few seconds on a healthy network and up to 30 s on a slow one, which is exactly when the retry loop exists.Suggested fix
One line in each client, on the success path —
VssBackupClient.ktandVssBackupClientLdk.kt:A reset that lands mid-setup then turns the call into a failure:
onFailureno-ops on the cancelled gate, theisSetup === gateswap is correctly skipped,setupWithRetryreports the failure, and the LDK setup launch never fires — so the old store id is never re-derived.Worth pairing with either of:
scope.launchto a job and cancelling it instopObservingBackups().Files
app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt—setup(),reset()app/src/main/java/to/bitkit/data/backup/VssBackupClientLdk.kt— same shapeapp/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt—getVssStoreId(),clearCache()app/src/main/java/to/bitkit/repositories/BackupRepo.kt—startObservingBackups(),stopObservingBackups()app/src/main/java/to/bitkit/usecases/WipeWalletUseCase.kt—stopNode(),wipeLocal()app/src/main/java/to/bitkit/services/LightningService.kt—buildWithVssStorecall site