Skip to content

kotlin-sdk/platform-wallet: duplicated unspent TXO rows after SPV rescan following unclean shutdown (inflated balance) #4069

Open

Description

@bfoss765

Observed live (Android wallet integration, testnet, Galaxy S22, kotlin-sdk 4.0.0 @ feat/kotlin-sdk-and-example-app, rust-dashcore 647fa982):

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:

  • SDK unspent TXO rows: 271, summing 413,124,109 duffs
  • 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:

  1. Idempotent TXO persistence on rescan (outpoint-unique upsert semantics through the persistence bridge).
  2. Crash-safe SPV storage (the header-store regression after SIGKILL suggests non-atomic writes).
  3. 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.

Context: dashpay/dash-wallet#1507 Phase 5a shadow-sync harness. 🤖 Filed with Claude Code

Activity

  1. bfoss765 commented on Jul 9, 2026

    @bfoss765
    CollaboratorAuthor

    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).

  2. bfoss765 commented on Jul 9, 2026

    @bfoss765
    CollaboratorAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions