You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
kotlin-sdk/platform-wallet: duplicated unspent TXO rows after SPV rescan following unclean shutdown (inflated balance) #4069
A wallet whose SDK view initially matched dashj exactly (154,427,919 duffs both sides, verified by a continuous parity harness) became inflated after several unclean app terminations and subsequent SPV restarts/rescans:
dashj spend candidates (source of truth): 128 outputs, 154,427,919 duffs
Delta: +258,696,190 duffs (~2.1× row duplication)
Distinct txids over the SDK's TXO rows stayed constant (370) throughout — the inflation is duplicated/unreconciled rows, not newly discovered transactions.
Reproduction context: repeated process kills without stopSpv (no teardown hook), with the SPV header store observed regressing between runs (resumed at a lower height than previously reached), followed by rescans over the already-persisted TXO set. Suspect: the rescan re-emits TXO persistence events that append rather than reconcile against existing rows (dedup key not enforced end-to-end through the changeset → Room bridge), and/or spent-markers lost when the header store regresses.
Requests:
Idempotent TXO persistence on rescan (outpoint-unique upsert semantics through the persistence bridge).
Crash-safe SPV storage (the header-store regression after SIGKILL suggests non-atomic writes).
A wallet-level rescan/reset API — integrators currently have to delete Room TXO/tx rows directly and clear SPV storage to recover (works, but the live Rust in-memory view stays stale until process restart).
The integrating app now detects this state (balance parity vs dashj) and auto-recovers by clearing + rescanning, but the corruption is SDK-side. cc @QuantumExplorer for review.
Additional evidence from the recovery attempts: after deleting the wallet's TXO/transaction Room rows AND recursively deleting the SPV dataDir (header store + watermark), a full re-download of headers/filters completed but re-discovered NOTHING (0 TXOs, 0 txs) — the wallet's per-wallet scan watermark evidently lives in the wallet state itself (Room wallet record / Rust wallet), not in the SPV storage, so the filter scan skips re-matching for a wallet it believes is already scanned. This makes partial-state recovery impossible for integrators: the only working recovery is removing the wallet entirely and re-creating it from the mnemonic. Strengthens the request for a first-class wallet rescan API (reset scan watermark + re-match from a given height).
Decisive third reproduction — cleanest form of the defect. After a full wallet re-creation (removeWallet → createWallet from the same mnemonic → identity re-discovery → fresh SPV scan on an empty dataDir), the scan completed and the live Rust wallet reported a balance (~1.69 DASH visible via platform_wallet_get_balance) — but zero TXO/transaction rows were ever persisted to Room, while WalletEntity.syncedHeight WAS advanced. On the next process restart the wallet rehydrates as empty-and-fully-scanned: funds gone from the SDK's view, unrecoverable without another full re-creation.
So the persistence bridge is unreliable in both directions: rescans over existing state DUPLICATE rows (original report), and clean scans fail to persist rows at all — while the watermark always advances. The changeset emission/consumption path for scan-discovered TXOs looks like the common locus.
This is now the sole blocker for the Android wallet's L1→shielded transfer testing (the app's parity gate correctly refuses to spend from a view that can't survive a restart). Happy to provide full device logs.
Activity
bfoss765 commentedon Jul 9, 2026
Additional evidence from the recovery attempts: after deleting the wallet's TXO/transaction Room rows AND recursively deleting the SPV dataDir (header store + watermark), a full re-download of headers/filters completed but re-discovered NOTHING (0 TXOs, 0 txs) — the wallet's per-wallet scan watermark evidently lives in the wallet state itself (Room wallet record / Rust wallet), not in the SPV storage, so the filter scan skips re-matching for a wallet it believes is already scanned. This makes partial-state recovery impossible for integrators: the only working recovery is removing the wallet entirely and re-creating it from the mnemonic. Strengthens the request for a first-class wallet rescan API (reset scan watermark + re-match from a given height).
bfoss765 commentedon Jul 9, 2026
Decisive third reproduction — cleanest form of the defect. After a full wallet re-creation (removeWallet → createWallet from the same mnemonic → identity re-discovery → fresh SPV scan on an empty dataDir), the scan completed and the live Rust wallet reported a balance (~1.69 DASH visible via platform_wallet_get_balance) — but zero TXO/transaction rows were ever persisted to Room, while WalletEntity.syncedHeight WAS advanced. On the next process restart the wallet rehydrates as empty-and-fully-scanned: funds gone from the SDK's view, unrecoverable without another full re-creation.
So the persistence bridge is unreliable in both directions: rescans over existing state DUPLICATE rows (original report), and clean scans fail to persist rows at all — while the watermark always advances. The changeset emission/consumption path for scan-discovered TXOs looks like the common locus.
This is now the sole blocker for the Android wallet's L1→shielded transfer testing (the app's parity gate correctly refuses to spend from a view that can't survive a restart). Happy to provide full device logs.
fix(platform-wallet-ffi): serialize the whole FFI persister round und…
fix(platform-wallet): subscribe-before-publish, per-wallet fault scop…
fix(platform-wallet): freeze sync watermark on persistence fault — TX…
Merge remote-tracking branch 'origin/v4.1-dev' into feat/kotlin-sdk-a…
fix(platform-wallet): batch wallet-event persistence so the sync wate…
feat(ffi/jni/kotlin): expose sync_fault_detected to the host
fix(platform-wallet): report only store-accepted heights as persisted
fix(platform-wallet): drain the lossless persistence channel (mpsc) s…
fix(platform-wallet): report only store-accepted heights as persisted
fix(platform-wallet): freeze a batch's wallets when its commit thread…
fix: fail closed when a persister has not attested sweep-removal support
docs(platform-wallet): correct four statements the #981 bump left stale
docs(platform-wallet): correct four statements the #981 bump left stale