fix: prevent DualStore from clobbering VSS backups and mask-failing restores - #3
Merged
Merged
Conversation
29 tasks
…estores Two related fixes to the dual-store (VSS + local SQLite) backend: 1. Gate all VSS pushes on a safety check. The background bulk sync ran unconditionally at construction and pushed every local key to VSS with unconditional puts (version -1). If the local store held a fresh node built over an existing backup (e.g. a restore that fell back to an empty local store), the sync overwrote the real channel manager in VSS within seconds. All pushes (bulk sync, writes, removes) now first verify the poison signature is absent: local has no channel monitors (active or archived) while VSS holds at least one. On detection, VSS writes are disabled for the session and a CRITICAL message is logged. If VSS is unreachable during the check, the verdict stays undetermined and the push is skipped; the next push re-checks. 2. Propagate VSS errors during restore instead of masking them. In restore mode, read() collapsed any VSS failure into NotFound and list() into an empty namespace, so a fast-failing VSS error (auth rejection from clock skew, DNS failure) made the node come up fresh with no channels while reporting a successful build. Non-NotFound errors now fail the read/list — and thereby the build — so callers can surface the failure and retry. Also skips the bulk sync entirely in restore mode: local started empty this session, so there is nothing to catch up.
kaloudis
force-pushed
the
fix/dual-store-vss-clobber
branch
from
July 30, 2026 01:05
ec6d976 to
7ea48de
Compare
kaloudis
added a commit
to kaloudis/zeus
that referenced
this pull request
Jul 30, 2026
Pulls in ZeusLN/ldk-node#3: DualStore push-safety gate (prevents a fresh local store from overwriting an existing VSS backup) and restore-mode VSS error propagation (a failed VSS fetch now fails the build instead of silently producing a fresh node with no channels). Also includes the additive BOLT11 route-hints receive APIs from ZeusLN/ldk-node#2; the uniffi interface change is purely additive and the uniffi version is unchanged, so the vendored bindings remain compatible without regeneration.
kaloudis
added a commit
to kaloudis/zeus
that referenced
this pull request
Jul 30, 2026
…ding an empty wallet Users on poor connections reported restore-from-seed coming up with no channels. Root cause: the native VSS dual-store build had a 30/60s timeout and on timeout or failure silently fell back to building a fresh node with an empty local SQLite store. The restore flows ignored the returned vssError, so the wallet config was saved and the restore looked successful with zero channels. Worse, the fallback local DB shadows VSS on all later dual-store builds (local-first reads), so channels never appeared even after connectivity recovered — and the fork's background bulk sync would then overwrite the VSS backup with the fresh state (fixed separately in ZeusLN/ldk-node#3). Changes: - Add setVssFailOnError to the native LDK Node modules (Android + iOS). When set, a VSS build timeout or failure rejects with code vss_error instead of falling back to an empty local store. Reset with each createBuilder, so existing-node startups keep fallback behavior. - Thread failOnVssError through initializeNode/initNode/ createLdkNodeWallet. Hard-fail builds get a 180s timeout (restore time scales with node history and connection quality; failing now produces a visible error rather than a silently wrong wallet). - createLdkNodeWallet deletes the just-created node dir on failed init so a partially-written local DB cannot shadow VSS state on retry. - SeedRecovery passes failOnVssError: true and shows a localized error (new key views.Settings.SeedRecovery.vssRestoreFailed) so the user knows channels were NOT restored and can retry. - WalletConfiguration passes failOnVssError when an existing seed is imported; new-wallet creation keeps the non-fatal warning path. - startLdkNodeWallet treats a startup with a saved config but no local DB (e.g. config restored to a new device via cloud keychain sync) as a de-facto restore and hard-fails on VSS errors. Wallet.tsx catches the failure, surfaces it via SettingsStore.error/errorMsg, and releases fetchLock so the user can retry without restarting the app.
kaloudis
added a commit
to kaloudis/zeus
that referenced
this pull request
Jul 30, 2026
Pulls in ZeusLN/ldk-node#3: DualStore push-safety gate (prevents a fresh local store from overwriting an existing VSS backup) and restore-mode VSS error propagation (a failed VSS fetch now fails the build instead of silently producing a fresh node with no channels). Also includes the additive BOLT11 route-hints receive APIs from ZeusLN/ldk-node#2; the uniffi interface change is purely additive and the uniffi version is unchanged, so the vendored bindings remain compatible without regeneration.
kaloudis
added a commit
to kaloudis/zeus
that referenced
this pull request
Jul 30, 2026
…ding an empty wallet Users on poor connections reported restore-from-seed coming up with no channels. Root cause: the native VSS dual-store build had a 30/60s timeout and on timeout or failure silently fell back to building a fresh node with an empty local SQLite store. The restore flows ignored the returned vssError, so the wallet config was saved and the restore looked successful with zero channels. Worse, the fallback local DB shadows VSS on all later dual-store builds (local-first reads), so channels never appeared even after connectivity recovered — and the fork's background bulk sync would then overwrite the VSS backup with the fresh state (fixed separately in ZeusLN/ldk-node#3). Changes: - Add setVssFailOnError to the native LDK Node modules (Android + iOS). When set, a VSS build timeout or failure rejects with code vss_error instead of falling back to an empty local store. Reset with each createBuilder, so existing-node startups keep fallback behavior. - Thread failOnVssError through initializeNode/initNode/ createLdkNodeWallet. Hard-fail builds get a 180s timeout (restore time scales with node history and connection quality; failing now produces a visible error rather than a silently wrong wallet). - createLdkNodeWallet deletes the just-created node dir on failed init so a partially-written local DB cannot shadow VSS state on retry. - SeedRecovery passes failOnVssError: true and shows a localized error (new key views.Settings.SeedRecovery.vssRestoreFailed) so the user knows channels were NOT restored and can retry. - WalletConfiguration passes failOnVssError when an existing seed is imported; new-wallet creation keeps the non-fatal warning path. - startLdkNodeWallet treats a startup with a saved config but no local DB (e.g. config restored to a new device via cloud keychain sync) as a de-facto restore and hard-fails on VSS errors. Wallet.tsx catches the failure, surfaces it via SettingsStore.error/errorMsg, and releases fetchLock so the user can retry without restarting the app.
kaloudis
added a commit
to kaloudis/zeus
that referenced
this pull request
Jul 30, 2026
Pulls in ZeusLN/ldk-node#3: DualStore push-safety gate (prevents a fresh local store from overwriting an existing VSS backup) and restore-mode VSS error propagation (a failed VSS fetch now fails the build instead of silently producing a fresh node with no channels). Also includes the additive BOLT11 route-hints receive APIs from ZeusLN/ldk-node#2; the uniffi interface change is purely additive and the uniffi version is unchanged, so the vendored bindings remain compatible without regeneration.
kaloudis
added a commit
to kaloudis/zeus
that referenced
this pull request
Jul 31, 2026
…ding an empty wallet Users on poor connections reported restore-from-seed coming up with no channels. Root cause: the native VSS dual-store build had a 30/60s timeout and on timeout or failure silently fell back to building a fresh node with an empty local SQLite store. The restore flows ignored the returned vssError, so the wallet config was saved and the restore looked successful with zero channels. Worse, the fallback local DB shadows VSS on all later dual-store builds (local-first reads), so channels never appeared even after connectivity recovered — and the fork's background bulk sync would then overwrite the VSS backup with the fresh state (fixed separately in ZeusLN/ldk-node#3). Changes: - Add setVssFailOnError to the native LDK Node modules (Android + iOS). When set, a VSS build timeout or failure rejects with code vss_error instead of falling back to an empty local store. Reset with each createBuilder, so existing-node startups keep fallback behavior. - Thread failOnVssError through initializeNode/initNode/ createLdkNodeWallet. Hard-fail builds get a 180s timeout (restore time scales with node history and connection quality; failing now produces a visible error rather than a silently wrong wallet). - createLdkNodeWallet deletes the just-created node dir on failed init so a partially-written local DB cannot shadow VSS state on retry. - SeedRecovery passes failOnVssError: true and shows a localized error (new key views.Settings.SeedRecovery.vssRestoreFailed) so the user knows channels were NOT restored and can retry. - WalletConfiguration passes failOnVssError when an existing seed is imported; new-wallet creation keeps the non-fatal warning path. - startLdkNodeWallet treats a startup with a saved config but no local DB (e.g. config restored to a new device via cloud keychain sync) as a de-facto restore and hard-fails on VSS errors. Wallet.tsx catches the failure, surfaces it via SettingsStore.error/errorMsg, and releases fetchLock so the user can retry without restarting the app.
kaloudis
added a commit
to kaloudis/zeus
that referenced
this pull request
Jul 31, 2026
Pulls in ZeusLN/ldk-node#3: DualStore push-safety gate (prevents a fresh local store from overwriting an existing VSS backup) and restore-mode VSS error propagation (a failed VSS fetch now fails the build instead of silently producing a fresh node with no channels). Also includes the additive BOLT11 route-hints receive APIs from ZeusLN/ldk-node#2; the uniffi interface change is purely additive and the uniffi version is unchanged, so the vendored bindings remain compatible without regeneration.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Background
ZEUS users reported restore-from-seed coming up with no channels. Investigation traced it to the app-side VSS build timeout silently falling back to an empty local store (fixed separately in the ZEUS app), which exposed two independent defects in
DualStore:Problem 1: bulk sync clobbers VSS backups
DualStore::newunconditionally spawnsbulk_sync_to_vss, which pushes every local key to VSS using unconditional puts (version: -1skips the server's conflict check). If the local store contains a fresh node built over an existing backup — e.g. a restore that fell back to an empty local store — the sync overwrites the realmanagerkey in VSS within seconds of the next launch. No user activity is required. The old channel monitors survive only because the fresh node has no monitor keys and the sync never deletes; recovery is then limited to orphan-monitor force-closes.Problem 2: restore mode masks VSS failures as success
In restore mode,
read()collapsed any VSS error intoNotFoundandlist()into an empty namespace. A fast-failing VSS error — auth rejection from device clock skew (InvalidRequestErrorskips retries entirely), DNS failure — therefore made the node build fresh with no channels while reporting success, indistinguishable from a real restore at the API surface.Changes
vss_push_allowed): all VSS pushes (bulk sync, per-key writes, removes) first verify the poison signature is absent: local has no channel monitors (active or archived) while VSS holds at least one. On detection, VSS writes are disabled for the session and a CRITICAL message is logged directing the user to re-restore. If VSS is unreachable during the check, the verdict stays undetermined: the current push is skipped (harmless — local retains the data and a later bulk sync catches up) and the check reruns on the next push. Healthy nodes short-circuit on the local monitor listing with no network call; restore-mode sessions pre-arm the gate open since their state derives from VSS.NotFoundVSS errors inread()/list()now propagate, failing the build so callers surface the error instead of silently producing a fresh node.Notes
cargo checkclean. Rebuilding LDKNodeFFI binaries + ZEUSfetch-libraries.shchecksum bump required to ship.Known limitations of the push gate