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
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
The wallet completed synchronization and displayed a small CoinJoin balance.
The user started a CoinJoin sweep to a transparent address.
The app became unresponsive during that attempt, and the user terminated it.
After relaunch, the CoinJoin sweep prompt appeared again.
The user then selected the shielded destination option.
After subsequent full synchronization, the wallet displayed no funds.
Support reported zero unconfirmed transactions.
The available logs do not establish that either sweep attempt was successfully broadcast.
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.
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:
immediately after loadFromPersistor, before synchronization changes the state;
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;
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
A wallet upgraded in place and the same wallet synchronized on a clean device converge on the same confirmed UTXO set and account balances.
A full reconstruction recognizes all remaining confirmed CoinJoin funds.
Confirmed sends funded with CoinJoin inputs attribute only the wallet-owned inputs and outputs across all relevant derivation chains.
Existing stores with born-wrong spent flags, missing CoinJoin outputs, or wrong transaction nets have a deterministic healing path.
Diagnostics identify any persistent-vs-memory UTXO mismatch without exposing wallet secrets or publicly linkable wallet history.
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-walletbuild, 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
The available logs do not establish that either sweep attempt was successfully broadcast.
Confirmed observations
Original device / older SDK build
platform-wallet 4.2.0-dev.1e1413684ca7fa9c1d3f7a686ab1d995d72838e0fsyncDone=trueSame wallet on another device / newer SDK build
platform-wallet 4.2.0-dev.67c7724722c037f384d81d14229a6852b54be150a(dirty build; the hash does not fully describe the binary)dash-spv 0.45.0 (4db5c36701b8)syncDone=trueThis 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:
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
Working hypothesis
The zero balance is more consistent with incorrect CoinJoin restore/accounting or stale persisted TXO state than with an unconfirmed AssetLock:
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.
spvRescanFiltersrewinds 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:
loadFromPersistor, before synchronization changes the state;Useful SDK diagnostics, grouped by wallet and account:
isSpent;isSpent/spendingTransactionrelationship inconsistencies;The Swift example app's
WalletMemoryExploreralready 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
Related