Skip to content

fix(dash-spv): stop the filter rescan loop when the chain anchors above the wallet - #933

Merged
QuantumExplorer merged 3 commits into
devfrom
fix/mainnet-checkpoint-rescan-loop
Aug 8, 2026
Merged

fix(dash-spv): stop the filter rescan loop when the chain anchors above the wallet#933
QuantumExplorer merged 3 commits into
devfrom
fix/mainnet-checkpoint-rescan-loop

Conversation

@xdustinface

@xdustinface xdustinface commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Mainnet filter sync never progressed. hd_wallet_sync_floor anchors the chain at checkpoint 200000 while the CLI creates wallets with birth height 0, so synced_height stayed at 0 and two gates jammed. Over 40 minutes: 4846 scan restarts, 51784 batch scans, zero sync-height advances, ~350% CPU.

tick restarted the scan whenever wallets_behind reported anyone below the committed frontier, wiping in-flight batches before any could commit. It now compares against the height a restart would actually resume from, which is floored at the wallets' birth heights and the stored headers' start.

The commit-time contiguity guard required synced_height + 1 >= batch_start, which reads 1 >= 200000 under an anchor. It now substitutes scan_floor - 1 for an unreachable synced_height. The floor excludes the committed_height + 1 term of scan_start, since tracking the scan frontier would reduce the guard to batch_start >= batch_start and delete the height check #649 relies on. A genuine gap above the floor still blocks the advance, and the account-generation guard is untouched.

The floor comes from the stored header start rather than hd_wallet_sync_floor, so --start-height is covered too. That path bypasses the network clamp and reproduces the same loop on testnet.

Also warns when the anchor exceeds the wallet's birth height. That range is never scanned, and because BIP44 discovery is sequential, usage hidden in it can stop the gap-limit window from advancing and mask later addresses.

Summary by CodeRabbit

  • Bug Fixes
    • Improved wallet synchronization across checkpoint-anchored chains.
    • Prevented unnecessary rescans when wallet-reported progress falls below the applicable scan range.
    • Preserved in-progress filter synchronization during wallet advancement and rescan decisions.
    • Improved handling of stored filter-header boundaries and scan floors.
    • Enhanced wallet convergence so reported sync progress more reliably reaches the correct chain position.

…ve the wallet

A wallet whose `synced_height` sits below the chain anchor could never be certified, and the resulting rescan never terminated. On mainnet `hd_wallet_sync_floor` pushes the anchor to the checkpoint at 200000 while the CLI creates wallets with birth height 0, so `synced_height` stayed at 0 for the whole run: 4846 restarts and 51784 batch scans produced zero sync-height advances and burned ~350% CPU indefinitely.

Two independent gates jammed, both because neither knew that heights below the anchor are out of scope rather than pending work.

`tick` restarted the scan whenever `wallets_behind` reported anyone below the committed frontier, wiping in-flight batches before any could commit. It now compares against the height a restart would actually resume from, which is floored at the wallets' birth heights and the stored headers' start.

The commit-time contiguity guard required `synced_height + 1 >= batch_start`, which reads `1 >= 200000` under an anchor and never holds. It now substitutes `scan_floor - 1` for an unreachable `synced_height`. The floor deliberately excludes the `committed_height + 1` term of `scan_start`: tracking the scan frontier would reduce the guard to `batch_start >= batch_start` and delete the height check that `#649` relies on. A genuine gap above the floor still blocks the advance, and the account-generation guard is untouched.

The floor comes from the stored header start rather than `hd_wallet_sync_floor`, so `--start-height` is covered too. That path bypasses the network clamp entirely and reproduces the same loop on testnet.

Also warn when the anchor exceeds the wallet's birth height. That range is never scanned, and because BIP44 discovery is sequential, usage hidden in it can stop the gap-limit window from advancing and mask later addresses.
…llet

`hd_wallet_sync_floor` is non-zero only for mainnet, so every existing test ran with a scan floor of 0 where the contiguity guard reads `0 + 1 >= 0` and passes. The suite was structurally unable to reach the stall.

`create_anchored_test_manager` stores headers at a non-zero start height instead, which is where the floor is read from. That reproduces the mainnet shape without depending on `Network`, so it runs in normal CI.

Four cases fail before the fix: a wallet below the anchor reaching `Synced`, the boundary sweep over `synced_height` in `test_tick_does_not_rescan_when_no_wallets_behind` (199998 included so a fix keyed on `synced_height == 0` does not pass), no re-request of in-flight filters after a tick, and a wallet added at runtime converging. The last one is the case that raising the birth height at construction would not have covered.

Two more extend the existing `#649` guard tests and pass both before and after. They pin the behavior a wrong floor would break: a gap between the anchor and the batch must still block certification, and with contiguity satisfied at the floor the generation guard must be the only thing left holding the advance.
The multi-wallet test built a throwaway manager only to move three storage handles onto another one, which orphaned the storage `FiltersManager::new` had already read its progress from. `seed_anchored_storage` seeds whichever manager it is given, so both wallet types share one path.

It also added wallet B before sync ever started, so it never exercised the runtime add its name claimed. B now joins after the frontier is reached, which covers the other half of the fix: a wallet that genuinely needs a rescan must still get one, since a floor that suppressed every restart would pass the other tests just as well.

`drain_getcfilters` replaces the two copies of the request-drain destructure.
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The filter manager now uses the stored filter-header start as a scan floor. Wallet commits accept heights below that floor while retaining gap and generation checks. Rescan logic computes an effective restart height and anchored-chain tests cover wallet convergence and request handling.

Changes

Anchored filter synchronization

Layer / File(s) Summary
Scan-floor commit rules
dash-spv/src/sync/filters/manager.rs
The wallet commit path reads the header-storage start height as the scan floor. Heights below the floor are treated as covered, while gaps at or above the floor still block advancement. Checkpoint diagnostics identify ranges below the stored anchor.
Effective rescan positioning
dash-spv/src/sync/filters/sync_manager.rs
tick combines the wallet birth height and stored filter-header start height to calculate the effective rescan position. It restarts scanning only when that position is within the committed range.
Anchored-chain validation
dash-spv/src/sync/filters/manager.rs
Test helpers and cases cover anchored-chain contiguity, generation guards, wallet advancement, rescan suppression, in-flight filter requests, and convergence for wallets added after anchoring.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

  • dashpay/platform#4130 — Addresses scan-floor and checkpoint behavior that can cause unnecessary filter rescans below the stored filter-header floor.
  • dashpay/rust-dashcore#894 — Addresses checkpoint and filter-storage floors during wallet scan restarts.

Possibly related PRs

Suggested labels: ready-for-review

Suggested reviewers: zocolini, quantumexplorer

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main fix: stopping the filter rescan loop when the chain anchor is above the wallet birth height.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mainnet-checkpoint-rescan-loop

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
dash-spv/src/sync/filters/manager.rs (2)

216-229: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider limiting the repetition of this warning.

start_download runs on every restart, rescan trigger, and FilterHeadersStored-driven reinit. On mainnet, where the checkpoint floor is always above a birth height of 0, this emits the same warning on each call. The information is valuable once per wallet, so a repeated warn! reduces log signal.

Options: emit it at debug! for repeats, or track the last reported (header_start_height, wallet_birth_height) pair and warn only when it changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dash-spv/src/sync/filters/manager.rs` around lines 216 - 229, Limit the
warning in start_download to once per distinct (header_start_height,
wallet_birth_height) pair. Track the last reported pair across calls, emit
tracing::warn! only when the pair changes, and use a lower-severity log or no
log for repeated occurrences while preserving the existing warning details for
new pairs.

1569-1592: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the anchored scenarios into separate tests.

Both blocks append a second, independent scenario to an existing test. Each block builds a new wallet, a new manager, and a new anchored header store, so it shares no state with the first half. A separate #[tokio::test] per scenario reports failures more precisely and keeps each test focused on one rule.

Also applies to: 1787-1812

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dash-spv/src/sync/filters/manager.rs` around lines 1569 - 1592, The anchored
scenarios in the existing test should be split into separate focused
#[tokio::test] functions because each creates independent wallet, manager, and
header state. Extract the scenario around the visible wallet_b setup and the
corresponding block near the second referenced range into standalone tests,
preserving their setup, try_commit_batches call, and assertions unchanged.
dash-spv/src/sync/filters/sync_manager.rs (1)

211-233: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider committing the floored restart height instead of the raw stale height.

Line 231 lowers committed_height to stale_min_synced, which can sit far below scan_floor on an anchored chain. start_download normally overwrites it with scan_start - 1, but its early-return path (scan_start > filter_header_tip_height) does not, so the reported progress can regress below the anchor until the next batch commits. restart_at.saturating_sub(1) matches the height the scan actually resumes from.

♻️ Proposed change
                     self.reset_for_rescan();
-                    self.progress.update_committed_height(stale_min_synced);
+                    self.progress.update_committed_height(restart_at.saturating_sub(1));
                     return self.start_download(requests).await;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dash-spv/src/sync/filters/sync_manager.rs` around lines 211 - 233, Update the
restart handling in the stale-min-synced branch of the sync flow to call
progress.update_committed_height with restart_at.saturating_sub(1) instead of
stale_min_synced. Keep the existing restart_at calculation and start_download
behavior unchanged so reported progress remains aligned with the actual floored
scan start, including the early-return path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@dash-spv/src/sync/filters/manager.rs`:
- Around line 216-229: Limit the warning in start_download to once per distinct
(header_start_height, wallet_birth_height) pair. Track the last reported pair
across calls, emit tracing::warn! only when the pair changes, and use a
lower-severity log or no log for repeated occurrences while preserving the
existing warning details for new pairs.
- Around line 1569-1592: The anchored scenarios in the existing test should be
split into separate focused #[tokio::test] functions because each creates
independent wallet, manager, and header state. Extract the scenario around the
visible wallet_b setup and the corresponding block near the second referenced
range into standalone tests, preserving their setup, try_commit_batches call,
and assertions unchanged.

In `@dash-spv/src/sync/filters/sync_manager.rs`:
- Around line 211-233: Update the restart handling in the stale-min-synced
branch of the sync flow to call progress.update_committed_height with
restart_at.saturating_sub(1) instead of stale_min_synced. Keep the existing
restart_at calculation and start_download behavior unchanged so reported
progress remains aligned with the actual floored scan start, including the
early-return path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 85b494c6-c3d3-45ef-a848-b28f58a934a9

📥 Commits

Reviewing files that changed from the base of the PR and between 6be2407 and 5eb4960.

📒 Files selected for processing (2)
  • dash-spv/src/sync/filters/manager.rs
  • dash-spv/src/sync/filters/sync_manager.rs

@codecov

codecov Bot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.91304% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.20%. Comparing base (6be2407) to head (5eb4960).

Files with missing lines Patch % Lines
dash-spv/src/sync/filters/manager.rs 98.91% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #933      +/-   ##
==========================================
- Coverage   75.52%   75.20%   -0.33%     
==========================================
  Files         328      328              
  Lines       78017    78194     +177     
==========================================
- Hits        58923    58805     -118     
- Misses      19094    19389     +295     
Flag Coverage Δ
core 77.29% <ø> (ø)
ffi 48.58% <ø> (-2.92%) ⬇️
rpc 20.00% <ø> (ø)
spv 91.43% <98.91%> (+0.12%) ⬆️
wallet 76.88% <ø> (ø)
Files with missing lines Coverage Δ
dash-spv/src/sync/filters/sync_manager.rs 100.00% <ø> (ø)
dash-spv/src/sync/filters/manager.rs 97.94% <98.91%> (-0.02%) ⬇️

... and 19 files with indirect coverage changes

@github-actions github-actions Bot added the ready-for-review CodeRabbit has approved this PR label Aug 8, 2026
@QuantumExplorer
QuantumExplorer merged commit b056d07 into dev Aug 8, 2026
39 of 40 checks passed
@QuantumExplorer
QuantumExplorer deleted the fix/mainnet-checkpoint-rescan-loop branch August 8, 2026 12:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-review CodeRabbit has approved this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants