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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions