From 57f2a9566de60c669653bbdafd107e85c2f9e660 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Mon, 9 Feb 2026 08:56:41 +0100 Subject: [PATCH] fix: avoid panic for sentinel blocks in debug builds The workaround with the available heights also doesn't work in debug builds. I just didn't realize since I was always running release builds lately. Now while working on integration tests, this became an issue again. --- dash-spv/src/storage/blocks.rs | 30 +++--------------------------- dash-spv/src/storage/segments.rs | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/dash-spv/src/storage/blocks.rs b/dash-spv/src/storage/blocks.rs index a7d368912..ad4981e95 100644 --- a/dash-spv/src/storage/blocks.rs +++ b/dash-spv/src/storage/blocks.rs @@ -1,10 +1,9 @@ //! Block storage for persisting full blocks that contain wallet-relevant transactions. -use std::collections::HashSet; use std::path::PathBuf; use crate::error::StorageResult; -use crate::storage::segments::{Persistable, SegmentCache}; +use crate::storage::segments::SegmentCache; use crate::storage::PersistentStorage; use crate::types::HashedBlock; use async_trait::async_trait; @@ -29,9 +28,6 @@ pub trait BlockStorage: Send + Sync + 'static { pub struct PersistentBlockStorage { /// Block storage segments. blocks: RwLock>, - /// Set of available block heights used for fast lookups and to bypass sentinel loading and gap - /// detection asserts (in debug builds) in the underlying segment implementation. - available_heights: HashSet, } impl PersistentBlockStorage { @@ -46,24 +42,10 @@ impl PersistentStorage for PersistentBlockStorage { tracing::debug!("Opening PersistentBlockStorage from {:?}", blocks_folder); - let mut blocks: SegmentCache = - SegmentCache::load_or_new(&blocks_folder).await?; - - let mut available_heights = HashSet::new(); - - if let (Some(start), Some(end)) = (blocks.start_height(), blocks.tip_height()) { - let hashed_blocks = blocks.get_items(start..end + 1).await?; - let sentinel = HashedBlock::sentinel(); - for (i, hashed_block) in hashed_blocks.iter().enumerate() { - if hashed_block != &sentinel { - available_heights.insert(start + i as CoreBlockHeight); - } - } - } + let blocks: SegmentCache = SegmentCache::load_or_new(&blocks_folder).await?; Ok(Self { blocks: RwLock::new(blocks), - available_heights, }) } @@ -78,17 +60,11 @@ impl PersistentStorage for PersistentBlockStorage { #[async_trait] impl BlockStorage for PersistentBlockStorage { async fn store_block(&mut self, height: u32, hashed_block: HashedBlock) -> StorageResult<()> { - self.available_heights.insert(height); self.blocks.write().await.store_items_at_height(&[hashed_block], height).await } async fn load_block(&self, height: u32) -> StorageResult> { - // This early return avoids unnecessary disk lookups and bypasses sentinel loading and gap - // detection asserts (in debug builds) in the underlying segment implementation. - if !self.available_heights.contains(&height) { - return Ok(None); - } - Ok(self.blocks.write().await.get_items(height..height + 1).await?.first().cloned()) + self.blocks.write().await.get_item(height).await } } diff --git a/dash-spv/src/storage/segments.rs b/dash-spv/src/storage/segments.rs index 6eb23dccf..f3247b56d 100644 --- a/dash-spv/src/storage/segments.rs +++ b/dash-spv/src/storage/segments.rs @@ -260,6 +260,20 @@ impl SegmentCache { Ok(items) } + /// Get a single item by height. Returns `None` for sentinel (empty) slots. + /// Unlike `get_items()`, this does not assert dense storage — safe for sparse data. + pub async fn get_item(&mut self, height: u32) -> StorageResult> { + let segment_id = Self::height_to_segment_id(height); + let offset = Self::height_to_offset(height); + let segment = self.get_segment_mut(&segment_id).await?; + let item = segment.get_single(offset); + if *item == I::sentinel() { + Ok(None) + } else { + Ok(Some(item.clone())) + } + } + pub async fn store_items(&mut self, items: &[I]) -> StorageResult<()> { self.store_items_at_height(items, self.next_height()).await } @@ -479,6 +493,12 @@ impl Segment { self.last_accessed = std::time::Instant::now(); } + /// Get a single item by offset, returning the raw value (may be a sentinel). + pub fn get_single(&mut self, offset: u32) -> &I { + self.last_accessed = Instant::now(); + &self.items[offset as usize] + } + pub fn get(&mut self, range: Range) -> &[I] { debug_assert!(range.start < self.items.len() as u32); debug_assert!(range.end <= self.items.len() as u32);