fix(dash-spv): make backward coverage durable by rewinding synced_height instead of sweeping in memory - #1002
Draft
romchornyi wants to merge 4 commits into
Draft
fix(dash-spv): make backward coverage durable by rewinding synced_height instead of sweeping in memory#1002romchornyi wants to merge 4 commits into
romchornyi wants to merge 4 commits into
Conversation
…ght instead of sweeping in memory Since #866/#974 the filter sync covers scripts derived after a range committed by rescanning the committed range at the forward drain (`rescan_committed_range`). On a mixing-heavy wallet (~13k CoinJoin scripts derived during the scan) that is a single multi-minute pass over ~2.3M filters that matches tens of thousands of blocks, with no persisted progress: the sweep's block requests are charged to the tail batch's commit gate, and an iOS suspension a few seconds after the client reports "synced" drops the whole sweep. The user sees a synced wallet with the newly derived scripts' transactions missing until a manual rescan. On a relaunch with those blocks already in storage the same pass drains the matched blocks through the `SyncEvent` broadcast channel faster than the monitor consumes them, the monitor hits `Lagged` and the client shuts down. Replace the in-memory sweep with a durable re-walk: - `WalletInterface::rewind_wallet_synced_height(wallet_id, height)` — a new hook that lowers one wallet's committed sync checkpoint. It emits the same `SyncHeightAdvanced` persistence event an advance emits, so the persisters store the lowered height verbatim and the rewind survives a restart. Only lowers; a value at or above the current is ignored. Default no-op for implementations that predate backward coverage; implemented for `WalletManager` and the mock wallet. - `FilterSyncManager`: at the forward drain, when there are scripts that were derived after their range committed, rewind the affected wallets to `earliest_required_height - 1` instead of sweeping. The existing wallet-behind path ("Wallet synced_height fell below committed_height, restarting scan") then re-walks committed history in the normal 5,000-height batches, each persisting its own progress. Commit-time advance skips a wallet rewound at the same drain so the rewind is not clobbered by the batch's own `SyncHeightAdvanced`. - `rescan_committed_range` is kept (now unused) with progress logging and a `yield_now` per batch; it can be removed once the re-walk has soaked. Two sweep-shaped tests in `coinjoin_gap_discovery_tests` are `#[ignore]`d: their harness drives the filter manager directly and never runs the wallet-behind tick that now does the work. Cost: the re-walk starts at the wallet's birth height and re-delivers already-known transactions through the persistence channel, so it is slower than the targeted sweep (about +7 minutes on a 6.7k-transaction wallet from a fresh restore in the simulator). Rewinding to the lowest matched height and persisting only deltas are follow-ups. Verified with the same wallet: fresh restore, relaunch on an existing store, and a process kill mid re-walk with relaunch — every run reached the tip with the persisted store matching the chain, no `Lagged`.
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Contributor
Author
|
Local run on this branch (rebased onto The 2 ignored are the two sweep-shaped tests in |
…n birth height The forward drain rewinds every wallet with newly derived scripts to one floor, the earliest height any wallet requires. When a wallet with a lower birth height is added at runtime, that floor dragged an older wallet's checkpoint below its own birth (CI: `test_runtime_add_during_initial_sync`, W1 rewound 20999 -> 0), re-walking history the wallet cannot have touched and, on a persisted store, reading as a reset. `WalletManager` now clamps the rewind to `birth_height - 1` per wallet; the trait contract says so. Two dashd integration tests asserted the old invariant that a wallet's synced_height never decreases. It now legitimately dips at the drain and climbs back during the re-walk: - `tests_multi_wallet::test_runtime_add_during_initial_sync` checks that W1 never goes below its own birth height and still converges to the tip. - `dash-spv-ffi tests_callback::test_all_callbacks_during_sync` waits (up to 60 s) for `on_synced_height_updated` to report the tip again instead of sampling the last value once, which could land on the rewind.
Open
6 tasks
The callback test's wallet has transactions, so the scan derives scripts and a backward-coverage re-walk follows, completing as a later cycle. Track the cycle of the first on_sync_complete in the tracker and assert on that; the last cycle is logged.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## dev #1002 +/- ##
==========================================
+ Coverage 77.10% 77.28% +0.18%
==========================================
Files 329 329
Lines 83511 83567 +56
==========================================
+ Hits 64394 64588 +194
+ Misses 19117 18979 -138
|
…k is pending; cover the rewind CI (Ubuntu ARM / ffi): `test_ffi_multiple_transactions_across_blocks` read 24 transactions instead of 25 right after `wait_for_sync`. The forward drain rewound the wallet and then declared the filters complete in the same pass, so `SyncComplete` fired with the re-walk still to run; on a slow runner the tip block's transaction landed after the test read the count. The same ordering is what produced a spurious extra sync cycle in `test_all_callbacks_during_sync`. `try_process_batch` now skips `FiltersSyncComplete` while `rewalk_pending()` — a wallet below the committed frontier that the sync-manager tick will restart the scan for, tested exactly as the tick tests it (lowest stale synced_height + 1, floored at birth height and stored-header start, reaching the frontier). The state stays Syncing through the re-walk and completion is emitted once, after it. Coverage: - `backward_coverage_rewinds_and_holds_completion_until_rewalked` replaces the first ignored sweep test: the committed-batch shape now asserts the rewind to birth_height - 1, `rewalk_pending()`, no completion while behind, and completion once the wallet has caught up. The second ignored test keeps its `#[ignore]` with an updated reason. - `WalletManager::rewind_wallet_synced_height`: lowers and emits SyncHeightAdvanced, ignores a non-lowering value and an unknown wallet, clamps to the wallet's own birth_height - 1.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue being fixed or feature implemented
Since #866/#974 the filter sync covers scripts derived after a range committed by rescanning the committed range at the forward drain (
rescan_committed_range). On a mixing-heavy wallet (~6.7k transactions, ~13k CoinJoin scripts derived during the scan) that is one silent multi-minute pass over ~2.3M filters matching ~41k blocks, with no persisted progress:SyncEventbroadcast channel faster than the monitor consumes them,spawn_broadcast_monitorhitsLagged, treats it as fatal, and the client shuts down (SyncEvent monitor lagged, missed 1462 events→Storage shutdown completed; the host's run loop only logs it).This is the dash-spv half of the large-wallet "sync finished but transactions are missing" reports. Companion changes: dashpay/platform#4595 (linear persistence round) and dashpay/dashwallet-ios#1112 (UI gate on the durable watermark).
Draft because it changes an invariant (
synced_heightmay now be lowered by the sync layer) and needs the dash-spv owner's view on that before it is polished further.What was done?
WalletInterface::rewind_wallet_synced_height(wallet_id, height)(key-wallet-manager): new hook that lowers one wallet's committed sync checkpoint. Emits the sameSyncHeightAdvancedpersistence event an advance emits, so persisters store the lowered height verbatim and the rewind survives a restart. Only lowers; a value at or above the current is ignored. Default no-op; implemented forWalletManager(process_block.rs) andMockWallet.FilterSyncManager(sync/filters/manager.rs): at the forward drain, when backward scripts exist, rewind the affected wallets toearliest_required_height - 1instead of sweeping. The existing wallet-behind path ("Wallet synced_height fell below committed_height, restarting scan") re-walks committed history in the normal 5,000-height batches, each persisting its own progress. The commit-time advance skips a wallet rewound at the same drain so the batch's ownSyncHeightAdvanceddoes not clobber the rewind.FilterSyncManager::try_process_batchholdsFiltersSyncCompletewhilerewalk_pending()— a wallet below the committed frontier that the tick will restart the scan for (tested exactly as the tick tests it). The state staysSyncingthrough the re-walk andSyncCompletefires once, after it; hosts never see a "synced" cycle with a re-walk still pending.WalletManager::rewind_wallet_synced_heightclamps to the wallet's ownbirth_height - 1: the drain passes one floor for every wallet it rewinds, and a wallet added at runtime with a lower birth height must not drag an older wallet below its own start.rescan_committed_rangeis kept but unused (#[allow(dead_code)]), with progress logging and ayield_nowper batch; to be removed once the re-walk has soaked.backward_coverage_rewinds_and_holds_completion_until_rewalked(dash-spv) replaces the first sweep-shaped test incoinjoin_gap_discovery_tests— the committed-batch shape now asserts the rewind,rewalk_pending(), no completion while behind, completion once caught up; twoWalletManagerunit tests cover lowering + event, the non-lowering no-op, and the birth-height clamp. The second sweep test (sweep coalescing) stays#[ignore]d — it measured a mechanism that no longer exists; remove withrescan_committed_range. Two dashd integration tests asserted the old monotonicsynced_height(test_runtime_add_during_initial_sync,test_all_callbacks_during_sync) and now check "never below own birth height, converges to the tip" / the first completed cycle.How Has This Been Tested?
cargo test -p dash-spv --lib(569 passed, 3 ignored) andcargo test -p key-wallet-manager --lib(66 passed) on this branch; dashd integration tests via CI.Manual, same seed throughout, built into the iOS wallet via platform's swift-sdk:
BlocksNeeded≤ 343 per batch; reached the tip; store audited withgettxoutover every unspent row matched the chain, whereas the previous build's store carried 0.128 DASH of CoinJoin outputs that are spent on-chain.Lagged.Committed-range rescan found 41546 additional blocks→SyncEvent monitor lagged, missed 1462 events→ client shutdown.Known cost: the re-walk starts at birth height and re-delivers already-known transactions through the persistence channel, so it is slower than the targeted sweep (about +7 minutes for this wallet from a fresh restore in the simulator; more on device). Follow-ups: rewind to the lowest matched height for the new scripts; persist deltas only (rs-platform-wallet); treat
Laggedas recoverable in the event monitor and restart the run loop.Breaking Changes
None in the public API (
rewind_wallet_synced_heighthas a default no-op). Behavioural: a wallet'ssynced_heightis no longer monotonic across a sync cycle — it can be lowered at the forward drain and then re-advanced by the re-walk. Persisters that clamp the watermark to max would silently break the re-walk's resume; the two in-tree persisters apply it verbatim.Checklist:
For repository code-owners and collaborators only