Repository navigation
fix(platform-wallet)!: persist chain truncations so a stored wallet follows a reorg #5384
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: fix/pr-5126
Are you sure you want to change the base?
Changes from all commits
56932d4
fce09b1
3da5fc2
e6c0f9d
e9d890e
d907f36
d2147e2
c9129f3
fce95af
e38368b
05e343b
af930d9
1367322
34a16e2
3a1aa90
29b65d7
ecf5e11
041da26
44ed537
8b63a1e
d582cff
5889000
d1a4c0f
3d6aeca
22d050f
e9b2aae
a20fdff
307a662
0a5d5f3
30bd704
cfa14d0
31b0df1
7286da4
dd0accc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -154,7 +154,8 @@ class PlatformWalletPersistenceHandler( | |
| CAPABILITY_WALLET_RESTORE or | ||
| CAPABILITY_DPNS_NAME_STATES or | ||
| CAPABILITY_TRACKED_ASSET_LOCKS or | ||
| CAPABILITY_CORE_SWEEP_REMOVAL | ||
| CAPABILITY_CORE_SWEEP_REMOVAL or | ||
| CAPABILITY_CORE_CHAIN_TRUNCATION | ||
|
|
||
| /** | ||
| * The single-thread executor created when no [dispatcher] is injected. | ||
|
|
@@ -1282,6 +1283,85 @@ class PlatformWalletPersistenceHandler( | |
| return !chainlockOverIsLock | ||
| } | ||
|
|
||
| // ── Chain truncations ───────────────────────────────────────────── | ||
|
|
||
| /** One chain truncation, unpacked from the JNI trampoline's flat arrays at the callback. */ | ||
| private class ChainTruncation( | ||
| val height: Int, | ||
| val removedTxids: List<ByteArray>, | ||
| val unconfirmedTxids: List<ByteArray>, | ||
| val restoredOutpoints: List<ByteArray>, | ||
| ) | ||
|
|
||
| /** | ||
| * Stage one chain truncation. Native fires this before the round's | ||
| * other core callbacks and the round replays its ops in order, so the | ||
| * truncation lands first and a transaction it removes can be stored | ||
| * again, in its new block, by the slices that follow. Outside a round | ||
| * it runs in its own transaction. | ||
| */ | ||
| override fun onWalletChangesetChainTruncated( | ||
| walletId: ByteArray, | ||
| height: Int, | ||
| txids: ByteArray, | ||
| txidCount: Int, | ||
| unconfirmedTxids: ByteArray, | ||
| unconfirmedTxidCount: Int, | ||
| restoredOutpoints: ByteArray, | ||
| restoredOutpointCount: Int, | ||
| ): Int = guarded { | ||
| require(height >= 0) { "truncation height must not be negative" } | ||
| val truncation = ChainTruncation( | ||
| height = height, | ||
| removedTxids = unpackFixed(txids, txidCount, 32, "truncated txids"), | ||
| unconfirmedTxids = unpackFixed(unconfirmedTxids, unconfirmedTxidCount, 32, "unconfirmed txids"), | ||
| restoredOutpoints = unpackFixed(restoredOutpoints, restoredOutpointCount, 36, "restored outpoints"), | ||
| ) | ||
| stage(walletId) { db -> applyChainTruncation(db, walletId, truncation) } | ||
| 0 | ||
| } | ||
|
|
||
| /** | ||
| * Cut the store back the way the wallet cut itself back when a fork | ||
| * replaced the chain above [ChainTruncation.height] — the Room mirror | ||
| * of the SQLite store's `apply_truncation`. | ||
| * | ||
| * Everything but the transaction row is this wallet's own. The row is | ||
| * shared across wallets and each wallet's round commits on its own, so | ||
| * another wallet may already have stored a removed transaction again, | ||
| * in its new block, by the time this truncation arrives: its coins are | ||
| * not this truncation's to take, and the row goes only once nothing | ||
| * references it. The row's block columns are the one thing wallets | ||
| * share; between two wallets' rounds they can show the branch the other | ||
| * wallet last wrote. | ||
| * | ||
| * A restored coin needs no veto from stored claims, unlike a sweep's | ||
| * release: its spender was confirmed, and no block the fork keeps can | ||
| * hold a second spend of it. | ||
| */ | ||
| private suspend fun applyChainTruncation(db: DashDatabase, walletId: ByteArray, truncation: ChainTruncation) { | ||
| // Drop stale post-deletion callbacks (can't resurrect a wallet). | ||
| if (db.walletDao().getByWalletId(walletId) == null) return | ||
| // Removed: this wallet's coins, links, claims and involvement | ||
| // first, then the rows nothing references any more. | ||
| chunked(truncation.removedTxids) { db.txoDao().deleteWalletTxosByTxids(it, walletId) } | ||
| chunked(truncation.removedTxids) { db.txoDao().detachWalletSpenders(it, walletId) } | ||
| chunked(truncation.removedTxids) { db.documentDao().deleteWalletClaimsBySpenders(walletId, it) } | ||
| chunked(truncation.removedTxids) { db.transactionDao().deleteWalletInvolvements(it, walletId) } | ||
| chunked(truncation.removedTxids) { db.transactionDao().deleteUnreferencedByTxids(it) } | ||
| // Kept: InstantSend-locked, so mined again. Stored until then the | ||
| // way a locked transaction no block holds yet is; the coins they | ||
| // spent stay spent. | ||
| chunked(truncation.unconfirmedTxids) { db.transactionDao().unconfirmByTxids(it) } | ||
| chunked(truncation.unconfirmedTxids) { db.txoDao().unconfirmInstantLockedByTxids(it, walletId) } | ||
|
Comment on lines
+1352
to
+1356
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Blocking: Do not globally demote a shared transaction row from one wallet's truncation
source: |
||
| // Restored: this wallet's coins the removed transactions spent. | ||
| chunked(truncation.restoredOutpoints) { db.txoDao().releaseByOutpoints(it, walletId) } | ||
|
lklimek marked this conversation as resolved.
|
||
| chunked(truncation.restoredOutpoints) { | ||
| db.documentDao().deleteWalletSweptTombstonesByOutpoints(walletId, it) | ||
| } | ||
| db.walletDao().lowerSyncedHeight(walletId, truncation.height, System.currentTimeMillis()) | ||
| } | ||
|
|
||
| // ── Sweeps ──────────────────────────────────────────────────────── | ||
|
|
||
| /** One sweep batch, unpacked from the JNI trampoline's flat arrays at the callback. */ | ||
|
|
@@ -4120,6 +4200,8 @@ class PlatformWalletPersistenceHandler( | |
|
|
||
| internal const val CAPABILITY_CORE_SWEEP_REMOVAL: Long = | ||
| NativePersistenceBridge.CAPABILITY_CORE_SWEEP_REMOVAL | ||
| internal const val CAPABILITY_CORE_CHAIN_TRUNCATION: Long = | ||
| NativePersistenceBridge.CAPABILITY_CORE_CHAIN_TRUNCATION | ||
|
|
||
| private const val TAG = "DashPersistence" | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.