fix(key-wallet): prune observed spends during the initial sync - #1014
Conversation
|
Warning Review limit reachedNext included review available in 59 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Repository: dashpay/rust-dashcore/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (82)
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (7)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe change propagates buffered chainlock heights to wallets, enables observed-spend pruning before chainlock application, and prevents reprocessed spent outputs from returning to the UTXO set. Tests cover both behaviors. ChangesChainlock-aware wallet pruning
Priority: ➖ Normal Estimated code review effort: 2 (Simple) | ~10 minutes Change: Bug fix · Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to The available evidence identifies no confirmed behavior that would block merging. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Warning Git: CodeRabbit could not clone the repository, so clone-backed analysis was skipped and this review may be incomplete. Verify repository clone access, such as SSH credentials, before requesting another full review. If clone access is intentionally unavailable, use Thanks 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 |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## dev #1014 +/- ##
==========================================
+ Coverage 76.02% 77.26% +1.23%
==========================================
Files 255 329 +74
Lines 58413 83733 +25320
==========================================
+ Hits 44410 64696 +20286
- Misses 14003 19037 +5034
|
026fba0 to
3a427a4
Compare
|
Ready for review — needs QuantumExplorer or xdustinface. |
|
/self-reviewed |
`observed_spent_outpoints` records every input of every transaction in every block the wallet processes, false positives included, and is pruned only up to a chainlock the wallet has applied. The client defers chainlocks until `SyncComplete`, so nothing is pruned during an initial sync. On a mainnet restore of the bench wallet the map reached 1 487 967 entries (every input of 10 295 blocks, 505 946 transactions), above the 1 000 000 entries the serde adapter accepts on load, so a wallet persisted mid-sync could not be loaded back. The dispatcher still defers applying chainlocks, but now passes the deferred chainlock's height on through `note_chain_lock_height`. The wallet uses it only as a finality boundary for pruning: entries at or below min(synced_height, highest chainlock applied or noted) are evicted. As before, only chain-locked spends are forgotten, and no record is promoted. Pruning mid-sync opened one path: redelivering a funding transaction whose output sits in `spent_before_funded`, after its observed-spend entry was evicted, re-inserted the spent coin into `utxos`. `update_utxos` now keeps such an output held; the new test fails without that guard. Validated with 5 consecutive mainnet restores at 100 Mbit / 100 ms, all ending at 7112 records, 14 114 383 sat and 13 389 addresses. Peak RSS was 1378–1724 MiB against 1488 MiB before, within run-to-run spread. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DruChNTWXwJoWPartZwCf
With the committed-height release gone, the segment cache itself has to bound the resident set. Ten 50 000-item segments per cache kept most of a restore in memory; two keep the peak below what the release achieved, without costing time. Mainnet restore, mainnet.100mbi.100ms, #1015/#1016/#1014 applied, jemalloc heap profiling, wallet identical in every run (14114383 sat, 7112 records, 13389 addresses): segments peak RSS time segment loads from disk (headers/filter headers/filters/blocks) 10 + #946 982 MiB 8.2 min - 10 1490 MiB 8.8 min 84 / 2 / 28 / 0 2 930 MiB 8.8 min 212 / 103 / 74 / 0 1 1000 MiB 7.7 min 427 / 742 / 156 / 33 Each cache served ~2.34M requests from memory in every run. One segment starts reloading block segments of up to 87 MB from disk, which raises the peak again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DruChNTWXwJoWPartZwCf
A dirty segment leaving the resident set used to wait in `evicted`, in memory, until the next 5 s storage tick wrote it. The resident limit was therefore not a limit: every segment evicted between two ticks stayed in memory. Now eviction writes the segment first and only then drops it; if the write fails the segment stays resident and the error is returned. The `evicted` map is gone, and the tick only persists the resident segments. Mainnet restore, mainnet.100mbi.100ms, #1015/#1016/#1014 applied, two resident segments: 7.7 and 7.9 min, peak RSS 943 and 948 MiB, wallet identical (14114383 sat, 7112 records, 13389 addresses). No cache misses served from `evicted` any more; header, filter header and filter segments are reloaded from disk about as often as they used to come back from `evicted` (~270 / ~150 / ~90), block segments never. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DruChNTWXwJoWPartZwCf
… one buffer `Segment::persist` encoded the whole segment into a `Vec` before handing it to `atomic_write`. For a block segment near the tip (up to 87 MB on disk) that `Vec` grew to 128 MiB, and since dirty segments are now written on eviction this happened in the middle of the sync. `atomic_write_items` encodes one item at a time through a 1 MiB `BufWriter` into the temporary file, then syncs and renames it as before. `atomic_write` keeps its behaviour and shares the temporary file and rename logic. Mainnet restore, mainnet.100mbi.100ms, #1015/#1016/#1014 applied, two resident segments, wallet identical in every run (14114383 sat, 7112 records, 13389 addresses): whole-segment buffer peak RSS 943 / 948 MiB 7.7 / 7.9 min streamed peak RSS 845 / 855 / 856 MiB 9.4 / 7.7 / 6.5 min The persist buffer, 68–133 MiB in the earlier heap snapshots, no longer shows up. The 9.4 min run lost time to peers in the header and filter header phases. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DruChNTWXwJoWPartZwCf
Every segment cache held 50 000 items, whatever an item costs. Near the tip a block segment holds tens of MB of decoded blocks, while a header segment spanning the same heights is 5.6 MB and a filter header one 1.6 MB. `Persistable::ITEMS_PER_SEGMENT` lets each type choose: headers 10 000 (~1.1 MB per segment), filter headers 50 000 (~1.6 MB), filters 2 000 (~2 MB near the tip) and blocks 1 000. This changes the on-disk layout of the header, filter and block segments. Storage written with 50 000-item segments is misread by this layout and has to be deleted until a migration or a versioned folder lands. Mainnet restore, mainnet.100mbi.100ms, #1015/#1016/#1014 applied, two resident segments, jemalloc heap profiling, wallet identical in every run (14114383 sat, 7112 records, 13389 addresses): segment items time peak RSS segment caches 50 000 for every type 7.0 min 818 MiB 332 MiB 5 000 for every type 7.3-8.3 min 554-687 MiB 23-106 MiB 1 000 for every type 9.2-12.2 min 453-577 MiB 2-66 MiB per type (this commit) 7.5 min 538 MiB 38 MiB With 1 000 items everywhere, the header and filter header phases paid an fsync per evicted segment (105-141 s and 254-332 s instead of ~60 s and ~150 s). Blocks at 500 items saved ~18 MiB of cache but reloaded 50 % more block segments. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DruChNTWXwJoWPartZwCf
3a427a4 to
47f7edd
Compare
|
Bots are done — your move: post |
observed_spent_outpointsrecords every input of every transaction in every block the wallet processes, false positives included, and is pruned only up to a chainlock the wallet has applied. The client defers chainlocks untilSyncComplete, so nothing is pruned during an initial sync. On a mainnet restore of the bench wallet the map reached 1 487 967 entries (every input of 10 295 blocks, 505 946 transactions), above the 1 000 000 entries the serde adapter accepts on load, so a wallet persisted mid-sync could not be loaded back.The dispatcher still defers applying chainlocks, but now passes the deferred chainlock's height on through
note_chain_lock_height. The wallet uses it only as a finality boundary for pruning: entries at or below min(synced_height, highest chainlock applied or noted) are evicted. As before, only chain-locked spends are forgotten, and no record is promoted.Pruning mid-sync opened one path: redelivering a funding transaction whose output sits in
spent_before_funded, after its observed-spend entry was evicted, re-inserted the spent coin intoutxos.update_utxosnow keeps such an output held; the new test fails without that guard.Validated with 5 consecutive mainnet restores at 100 Mbit / 100 ms
Closes #899
Summary by CodeRabbit
Bug Fixes
Tests
PR Hygiene ·
47f7edd/self-revieweddash-spv(dash-spv/src/client/event_handler.rs) — QuantumExplorer or xdustinfacekey-wallet-manager(key-wallet-manager/src/process_block.rs,key-wallet-manager/src/wallet_interface.rs) — QuantumExplorer or xdustinfacekey-wallet(key-wallet/src/managed_account/managed_core_funds_account.rs,key-wallet/src/tests/observed_spent_outpoints_tests.rs,key-wallet/src/wallet/managed_wallet_info/mod.rsand 1 more) — QuantumExplorer or xdustinfaceWhen every box is checked the
PR Hygienecheck passes and this can merge.