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
- Wallet created with a birth height far below the chain tip (200000 vs ~2.5M).
- 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.
- App restarts (any reason).
handle_new_filter_headers → start_download with scan_start = 200000 (birth-height floor).
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
Symptom
Debug builds of an app embedding dash-spv abort in a crash-loop on startup (SIGABRT ~15s after launch, every launch):
Backtrace (rev 38c689d, iOS simulator, testnet):
Trigger state
stored_filters_tip = 2504884. Filters for the birth-height region were never downloaded/stored.handle_new_filter_headers→start_downloadwithscan_start = 200000(birth-height floor).start_downloaddecides to preload stored filters becausestored_filters_tip > 0 && scan_start <= stored_filters_tip— treating the tip watermark as if storage were CONTIGUOUS fromscan_start. It callsload_filters(200000, 204999); the segment(s) covering 200000 exist empty (first_valid_offset = None), andSegmentCache::get_itemshits 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 intosegment.get(seg_start..)over a never-populated segment — sentinel/garbage filter data silently zipped against real headers inFiltersManager::load_filters, so matches over that range are wrong rather than loudly absent.Suggested direction
stored_filters_tipis a tip watermark, not a proof of contiguity — storage is sparse when the scan floor is far behind the tip.start_downloadshould 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), orSegmentCache::get_itemsshould return a typed miss for empty segments soload_filterscan 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=1capture and reproduction state available on request; reproduces deterministically on the state above.🤖 Generated with Claude Code