Skip to content

dash-spv: filter restart with scan_start far below stored tip loads never-populated segments — debug abort / release sentinel reads #892

Description

@QuantumExplorer

Symptom

Debug builds of an app embedding dash-spv abort in a crash-loop on startup (SIGABRT ~15s after launch, every launch):

thread 'tokio-rt-worker' panicked at dash-spv/src/storage/segments.rs:271:
Trying to access invalid offset (0) in segment with first_valid_offset = None

Backtrace (rev 38c689d, iOS simulator, testnet):

SegmentCache::get_items                     segments.rs:271 (#[cfg(debug_assertions)] check)
← PersistentFilterStorage::load_filters
← FiltersManager::load_filters              sync/filters/manager.rs:160
← FiltersManager::start_download            sync/filters/manager.rs:280
← FiltersManager::handle_new_filter_headers sync/filters/manager.rs:976

Trigger state

Starting filter download (scan_start=200000, download_start=2504885, stored_filters_tip=2504884, target=2504885)
Loading stored filters 200000 to 204999 into current batch   ← panics
  1. Wallet created with a birth height far below the chain tip (200000 vs ~2.5M).
  2. First run: headers/filter-headers sync to tip; some filters near the TIP get stored → stored_filters_tip = 2504884. Filters for the birth-height region were never downloaded/stored.
  3. App restarts (any reason). handle_new_filter_headersstart_download with scan_start = 200000 (birth-height floor).
  4. start_download decides to preload stored filters because stored_filters_tip > 0 && scan_start <= stored_filters_tip — treating the tip watermark as if storage were CONTIGUOUS from scan_start. It calls load_filters(200000, 204999); the segment(s) covering 200000 exist empty (first_valid_offset = None), and SegmentCache::get_items hits the debug assertion.

Every relaunch resumes the same scan state → permanent crash-loop until SPV storage is cleared.

Release builds are arguably worse

The assertion is #[cfg(debug_assertions)]-gated; a release build proceeds into segment.get(seg_start..) over a never-populated segment — sentinel/garbage filter data silently zipped against real headers in FiltersManager::load_filters, so matches over that range are wrong rather than loudly absent.

Suggested direction

stored_filters_tip is a tip watermark, not a proof of contiguity — storage is sparse when the scan floor is far behind the tip. start_download should either intersect the preload range with what the filter storage actually holds (e.g. query the storage's valid range / lowest stored height instead of assuming [scan_start, stored_filters_tip] is dense), or SegmentCache::get_items should return a typed miss for empty segments so load_filters can fall back to downloading that range. The load-preload condition dates to #446 with the current shape from the #835-era restart handling, so this is long-latent — it needs the low-birth-height + restart combination to surface.

Full RUST_BACKTRACE=1 capture and reproduction state available on request; reproduces deterministically on the state above.

🤖 Generated with Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions