Repository navigation
platform-wallet: persist chain truncation (reorg) reported by key-wallet #5380
Description
TL;DR: When the blockchain reorganizes, the wallet now forgets the transactions from the abandoned blocks in memory, but the copy saved on disk keeps them until the next change overwrites it.
User story
As a wallet user, I want a chain reorganization to be reflected in my saved wallet, to achieve the same balances and history after a restart as before it.
Scenario
Base flow
The wallet records a transaction from a block. The network then replaces that block with a different branch that does not contain the transaction. The app is restarted.
Actual behavior
While running, the wallet drops the transaction and corrects the balance. On disk the transaction, the coins it created, the coins it spent and the higher sync height remain, so after a restart the wallet can show the abandoned transaction again and resume scanning above the fork point.
Expected behavior
The saved wallet mirrors the reorganization: removed transactions and their coins are deleted, InstantSend-locked ones go back to unconfirmed, the coins they spent are available again, and the saved sync height is lowered to the fork height.
Detailed discussion
rust-dashcore dev (dashpay/rust-dashcore#1104) adds WalletInfoInterface::truncate_above and emits WalletEvent::ChainTruncated { wallet_id, height, txids, unconfirmed_txids, restored_outpoints, balance, account_balances }. Its documentation states what a consumer mirroring wallet state to disk must do: delete the removed transactions with any UTXO they created, move the unconfirmed ones back to the mempool with their UTXOs, mark the restored coins unspent, and lower its synced height to height when it is above it.
Platform picks the event up with the rust-dashcore pin update in #5307. There, packages/rs-platform-wallet/src/changeset/core_bridge.rs (build_core_changeset) logs the event and persists nothing, because CoreChangeSet cannot express it today:
synced_height/last_processed_heightare monotonic-max on merge, so they cannot be lowered.- The only subtractive shape is
SweepBatch, which requires a winning transaction (superseded_by); a truncation has none. - There is no shape for demoting a confirmed record back to unconfirmed.
Scope of a fix: a new changeset shape for truncation; SQLite, SwiftData (FFI) and Room (JNI) persisters applying it atomically; the DashPay sent-payment evidence for payments whose confirmation was truncated; the spend-observation fences for coins that became unspent again; restart tests on each backend.
Before the pin update the wallet did not handle reorganizations in memory either, so the saved state is no worse than before; the new gap is that memory and disk now disagree until restart.
Prior work
- chore(platform)!: update rust-dashcore (incl secp256k1 0.33) #5307 — rust-dashcore pin update that introduces the event; handles the in-memory balance, leaves persistence as a marked TODO.
- fix(platform-wallet)!: restore Core spending state and repair persisted accounting #5150 — SQLite replay and spend-claim persistence the truncation shape will have to cooperate with.
Attribution
🤖 Co-authored by Claudius the Magnificent AI Agent
Activity
build!: pin rust-dashcore to dev d036d844
fix(platform-wallet): hold the engine still until a spend verdict is …