Skip to content

platform-wallet: CoinJoin wallet is zero on Swift SDK dev.1 but recovers on dev.6 #4575

Description

@llbartekll

Summary

An iOS support case involving a wallet with heavy CoinJoin history ended with a fully synchronized wallet showing a zero balance. The same wallet, synchronized on another device with a newer Swift SDK / platform-wallet build, recovers a non-zero confirmed CoinJoin balance.

The strongest evidence is not the interrupted sweep itself, but that the older and newer SDK builds attribute the same confirmed historical transaction differently. The older build consumes the entire recognized balance and ends at zero; the newer build discovers additional owned funds, attributes fewer owned inputs to that transaction, and leaves a confirmed CoinJoin remainder.

This may be a real-world instance of the CoinJoin restore/accounting gap tracked in #4474, although the exact overlap with its CoinJoin-input/BIP44-change variant is not yet proven. Exact amounts, transaction identifiers, and the original diagnostic bundles are intentionally omitted from this public issue and are available privately to maintainers.

User-reported sequence

  1. The wallet completed synchronization and displayed a small CoinJoin balance.
  2. The user started a CoinJoin sweep to a transparent address.
  3. The app became unresponsive during that attempt, and the user terminated it.
  4. After relaunch, the CoinJoin sweep prompt appeared again.
  5. The user then selected the shielded destination option.
  6. After subsequent full synchronization, the wallet displayed no funds.
  7. Support reported zero unconfirmed transactions.

The available logs do not establish that either sweep attempt was successfully broadcast.

Confirmed observations

Original device / older SDK build

  • production iOS device on mainnet
  • platform-wallet 4.2.0-dev.1
  • platform commit e1413684ca7fa9c1d3f7a686ab1d995d72838e0f
  • clean build
  • app logs show a non-zero CoinJoin balance after syncDone=true
  • later full rescans on this SDK finish with a zero wallet balance
  • no corresponding AssetLock transaction was found in the inspected Core history
  • support reported no locally visible unconfirmed transaction

Same wallet on another device / newer SDK build

  • mainnet full sync completed at the current tip
  • platform-wallet 4.2.0-dev.6
  • platform checkout 7c7724722c037f384d81d14229a6852b54be150a (dirty build; the hash does not fully describe the binary)
  • dash-spv 0.45.0 (4db5c36701b8)
  • final wallet state:
    • confirmed balance is non-zero
    • unconfirmed balance is zero
    • Standard/BIP44 balance is zero
    • the recovered balance belongs to the CoinJoin account
  • the CoinJoin sweep eligibility check becomes active after syncDone=true
  • no wallet-owned shielded activity/notes were found
  • no matching AssetLock was found

This confirms that CoinJoin funds remain recoverable as Core UTXOs and were not moved into the shielded pool by the reported attempt.

The original and newer builds differ slightly in the final non-zero amount they report before/after reconstruction. That difference has not yet been attributed to specific outpoints.

Deterministic cross-version accounting difference

An older confirmed transaction provides a useful checkpoint. It predates the reported sweep incident and is not being identified as the failed sweep.

For the same on-chain transaction:

  • the older SDK discovers a lower balance before the transaction, attributes all of that balance as sent, and ends at zero;
  • the newer SDK discovers additional owned funds before the transaction, attributes fewer owned inputs to it, and retains a confirmed CoinJoin balance afterward.

This demonstrates an ownership/UTXO-accounting difference during CoinJoin history reconstruction, independently of whether the later sweep attempts were ever constructed or broadcast.

The exact block, transaction identifier, amounts, and relevant log excerpts are available privately for investigation.

What is not yet established

  • Whether the transparent sweep reached transaction persistence or broadcast before the app was terminated.
  • Whether the shielded attempt completed proof construction, produced a Core transaction, or reached broadcast.
  • Whether upgrading the original device in place to the newer SDK heals the existing persisted state.
  • Whether the original device contains incorrectly spent TXOs, missed CoinJoin TXOs, incorrect address-pool watermarks, or another persistence inconsistency.
  • Whether this is exactly the BIP44-change variant described in platform-wallet: restore scan does not own CoinJoin-derivation change — CoinJoin-funded sends display at gross input value #4474. On the newer build, the recovered balance is assigned to the CoinJoin account, not BIP44.
  • Which outpoints explain the small difference between the balances reconstructed by the two builds.

Working hypothesis

The zero balance is more consistent with incorrect CoinJoin restore/accounting or stale persisted TXO state than with an unconfirmed AssetLock:

  • unconfirmed count is zero;
  • no corresponding on-chain AssetLock was found;
  • no owned shielded notes were found;
  • another device with a newer SDK reconstructs confirmed CoinJoin funds from the same history;
  • the two SDK builds demonstrably disagree about owned inputs and balance for the same historical transaction.

An interrupted operation may still have exposed or preserved an invalid local state, but the current evidence does not prove that the termination itself caused it.

spvRescanFilters rewinds the in-memory scan checkpoint; it does not itself clear and rebuild all persisted transaction/TXO state. A full filter rescan can therefore be insufficient as a healing mechanism for some already-persisted inconsistencies.

Proposed diagnostic run

Provide an app build containing the newer SDK and install it in place over the affected device, without first deleting/reimporting the wallet and without initiating another sweep.

Capture diagnostics:

  1. immediately after loadFromPersistor, before synchronization changes the state;
  2. again after a full mainnet sync/rescan.

Useful SDK diagnostics, grouped by wallet and account:

  • persisted TXO count and value split by isSpent;
  • in-memory Rust UTXO count and value;
  • persisted-only and memory-only outpoint differences;
  • isSpent / spendingTransaction relationship inconsistencies;
  • UTXOs skipped during restore and the reason;
  • CoinJoin address-pool highest-used indexes/watermarks;
  • AssetLock count/status and owned shielded-note count/value;
  • for outgoing confirmed transactions, owned input/output count and value per account and balance before/after;
  • in an explicit private diagnostic export, the differing outpoints needed to reconcile the two sets;
  • for future CoinJoin sweeps: selected-input count/value, constructed txid, persistence result, proof result, broadcast result, and rollback/unlock result.

The Swift example app's WalletMemoryExplorer already contains persisted-vs-memory UTXO comparison logic that could be reused for a structured support diagnostic.

Outpoint-level data should be emitted only in an explicit private diagnostic export because it can reveal wallet history. No keys, seed material, addresses, or private shielded data should be logged.

Expected behavior / acceptance

  1. A wallet upgraded in place and the same wallet synchronized on a clean device converge on the same confirmed UTXO set and account balances.
  2. A full reconstruction recognizes all remaining confirmed CoinJoin funds.
  3. Confirmed sends funded with CoinJoin inputs attribute only the wallet-owned inputs and outputs across all relevant derivation chains.
  4. Existing stores with born-wrong spent flags, missing CoinJoin outputs, or wrong transaction nets have a deterministic healing path.
  5. Diagnostics identify any persistent-vs-memory UTXO mismatch without exposing wallet secrets or publicly linkable wallet history.

Related

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions