Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2e85243
removed chain storage an related code
ZocoLini Dec 18, 2025
3ca55b5
removed headers from ChainState
ZocoLini Dec 18, 2025
8f3d065
tip_height method removed
ZocoLini Dec 18, 2025
53be7f4
removed get_tip_hesh
ZocoLini Dec 18, 2025
eb32b7b
replaced header_at_height
ZocoLini Dec 18, 2025
2bf3a91
removed unused methods
ZocoLini Dec 18, 2025
f758a7f
init_from_checkpoint sync
ZocoLini Dec 18, 2025
f995b89
tip_header removed
ZocoLini Dec 18, 2025
7acccc0
removed two methos that where invovled in the same process
ZocoLini Dec 18, 2025
90957cf
fixed ffi
ZocoLini Dec 18, 2025
c443764
test updated to the changes
ZocoLini Dec 18, 2025
4be4cef
chore: removed fork detector and fork structs (#290)
ZocoLini Dec 19, 2025
0e98a8b
get_header now checks out of bound to return None instead of panics
ZocoLini Dec 19, 2025
06d35e9
start height was not being updated properly
ZocoLini Dec 19, 2025
abb37f8
fixed other test by correctly storing the headers in the storage
ZocoLini Dec 19, 2025
4a26a7f
removed genesis block creation in ChainState creation
ZocoLini Dec 19, 2025
bc544a5
fixed clippy warnings
ZocoLini Dec 19, 2025
0b05253
Merge branch 'remove-chain-storage' into remove-headers-from-chain-state
ZocoLini Dec 19, 2025
4be7e6e
dropped unuseed code
ZocoLini Dec 19, 2025
565c41b
Merge branch 'v0.41-dev' into remove-headers-from-chain-state
ZocoLini Dec 21, 2025
29654ab
merge main
ZocoLini Dec 23, 2025
4aa3ac8
merged dev branch
ZocoLini Jan 2, 2026
a4f8c0a
code review suggestions
ZocoLini Jan 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions dash-spv-ffi/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@ impl From<DetailedSyncProgress> for FFIDetailedSyncProgress {

#[repr(C)]
pub struct FFIChainState {
pub header_height: u32,
pub masternode_height: u32,
pub last_chainlock_height: u32,
pub last_chainlock_hash: FFIString,
Expand All @@ -191,7 +190,6 @@ pub struct FFIChainState {
impl From<ChainState> for FFIChainState {
fn from(state: ChainState) -> Self {
FFIChainState {
header_height: state.headers.len() as u32,
masternode_height: state.last_masternode_diff_height.unwrap_or(0),
last_chainlock_height: state.last_chainlock_height.unwrap_or(0),
last_chainlock_hash: FFIString::new(
Expand Down
3 changes: 1 addition & 2 deletions dash-spv-ffi/tests/unit/test_type_conversions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,6 @@ mod tests {
#[test]
fn test_chain_state_none_values() {
let state = dash_spv::ChainState {
headers: vec![],
last_chainlock_height: None,
last_chainlock_hash: None,
current_filter_tip: None,
Expand All @@ -173,7 +172,7 @@ mod tests {
};

let ffi_state = FFIChainState::from(state);
assert_eq!(ffi_state.header_height, 0);

assert_eq!(ffi_state.masternode_height, 0);
assert_eq!(ffi_state.last_chainlock_height, 0);
assert_eq!(ffi_state.current_filter_tip, 0);
Expand Down
6 changes: 5 additions & 1 deletion dash-spv/src/chain/chainlock_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,11 @@ impl ChainLockManager {
}

// Verify the block exists in our chain
if let Some(header) = chain_state.header_at_height(chain_lock.block_height) {
if let Some(header) = storage
.get_header(chain_lock.block_height)
.await
.map_err(ValidationError::StorageError)?
{
let header_hash = header.block_hash();
if header_hash != chain_lock.block_hash {
return Err(ValidationError::InvalidChainLock(format!(
Expand Down
11 changes: 7 additions & 4 deletions dash-spv/src/client/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,14 +189,17 @@ impl<

/// Returns the current chain tip hash if available.
pub async fn tip_hash(&self) -> Option<dashcore::BlockHash> {
let state = self.state.read().await;
state.tip_hash()
let storage = self.storage.lock().await;

let tip_height = storage.get_tip_height().await?;
let header = storage.get_header(tip_height).await.ok()??;

Some(header.block_hash())
}

/// Returns the current chain tip height (absolute), accounting for checkpoint base.
pub async fn tip_height(&self) -> u32 {
let state = self.state.read().await;
state.tip_height()
self.storage.lock().await.get_tip_height().await.unwrap_or(0)
}

/// Get current chain state (read-only).
Expand Down
35 changes: 8 additions & 27 deletions dash-spv/src/client/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,30 +168,12 @@ impl<
// This ensures the ChainState has headers loaded for both checkpoint and normal sync
let tip_height = {
let storage = self.storage.lock().await;
storage.get_tip_height().await.map_err(SpvError::Storage)?.unwrap_or(0)
storage.get_tip_height().await.unwrap_or(0)
};
if tip_height > 0 {
tracing::info!("Found {} headers in storage, loading into sync manager...", tip_height);
let loaded_count = {
let storage = self.storage.lock().await;
self.sync_manager.load_headers_from_storage(&storage).await
};

match loaded_count {
Ok(loaded_count) => {
tracing::info!("✅ Sync manager loaded {} headers from storage", loaded_count);
}
Err(e) => {
tracing::error!("Failed to load headers into sync manager: {}", e);
// For checkpoint sync, this is critical
let state = self.state.read().await;
if state.synced_from_checkpoint() {
return Err(SpvError::Sync(e));
}
// For normal sync, we can continue as headers will be re-synced
tracing::warn!("Continuing without pre-loaded headers for normal sync");
}
}
let storage = self.storage.lock().await;
self.sync_manager.load_headers_from_storage(&storage).await
}

// Connect to network
Expand All @@ -208,8 +190,7 @@ impl<
// Get initial header count from storage
let (header_height, filter_height) = {
let storage = self.storage.lock().await;
let h_height =
storage.get_tip_height().await.map_err(SpvError::Storage)?.unwrap_or(0);
let h_height = storage.get_tip_height().await.unwrap_or(0);
let f_height =
storage.get_filter_tip_height().await.map_err(SpvError::Storage)?.unwrap_or(0);
(h_height, f_height)
Expand Down Expand Up @@ -270,7 +251,7 @@ impl<
// Check if we already have any headers in storage
let current_tip = {
let storage = self.storage.lock().await;
storage.get_tip_height().await.map_err(SpvError::Storage)?
storage.get_tip_height().await
};

if current_tip.is_some() {
Expand Down Expand Up @@ -343,12 +324,12 @@ impl<

// Clone the chain state for storage
let chain_state_for_storage = (*chain_state).clone();
let headers_len = chain_state_for_storage.headers.len() as u32;
drop(chain_state);

// Update storage with chain state including sync_base_height
{
let mut storage = self.storage.lock().await;
storage.store_headers(&[checkpoint_header]).await?;
storage
.store_chain_state(&chain_state_for_storage)
.await
Expand All @@ -365,7 +346,7 @@ impl<
);

// Update the sync manager's cached flags from the checkpoint-initialized state
self.sync_manager.update_chain_state_cache(checkpoint.height, headers_len);
self.sync_manager.update_chain_state_cache(checkpoint.height);
tracing::info!(
"Updated sync manager with checkpoint-initialized chain state"
);
Expand Down Expand Up @@ -413,7 +394,7 @@ impl<
// Verify it was stored correctly
let stored_height = {
let storage = self.storage.lock().await;
storage.get_tip_height().await.map_err(SpvError::Storage)?
storage.get_tip_height().await
};
tracing::info!(
"✅ Genesis block initialized at height 0, storage reports tip height: {:?}",
Expand Down
2 changes: 1 addition & 1 deletion dash-spv/src/client/progress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ impl<
// Get current heights from storage
{
let storage = self.storage.lock().await;
if let Ok(Some(header_height)) = storage.get_tip_height().await {
if let Some(header_height) = storage.get_tip_height().await {
stats.header_height = header_height;
}

Expand Down
2 changes: 1 addition & 1 deletion dash-spv/src/client/status_display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ impl<'a, S: StorageManager + Send + Sync + 'static, W: WalletInterface + Send +
// For genesis sync: sync_base_height = 0, so height = 0 + storage_count
// For checkpoint sync: height = checkpoint_height + storage_count
let storage = self.storage.lock().await;
if let Ok(Some(storage_tip)) = storage.get_tip_height().await {
if let Some(storage_tip) = storage.get_tip_height().await {
let blockchain_height = storage_tip;
if with_logging {
tracing::debug!(
Expand Down
6 changes: 3 additions & 3 deletions dash-spv/src/client/sync_coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ impl<
let result = SyncProgress {
header_height: {
let storage = self.storage.lock().await;
storage.get_tip_height().await.map_err(SpvError::Storage)?.unwrap_or(0)
storage.get_tip_height().await.unwrap_or(0)
},
filter_header_height: {
let storage = self.storage.lock().await;
Expand Down Expand Up @@ -241,7 +241,7 @@ impl<
// Storage tip now represents the absolute blockchain height.
let current_tip_height = {
let storage = self.storage.lock().await;
storage.get_tip_height().await.ok().flatten().unwrap_or(0)
storage.get_tip_height().await.unwrap_or(0)
};
let current_height = current_tip_height;
let peer_best = self
Expand Down Expand Up @@ -315,7 +315,7 @@ impl<
// Emit filter headers progress only when heights change
let (abs_header_height, filter_header_height) = {
let storage = self.storage.lock().await;
let storage_tip = storage.get_tip_height().await.ok().flatten().unwrap_or(0);
let storage_tip = storage.get_tip_height().await.unwrap_or(0);
let filter_tip =
storage.get_filter_tip_height().await.ok().flatten().unwrap_or(0);
(storage_tip, filter_tip)
Expand Down
6 changes: 5 additions & 1 deletion dash-spv/src/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ pub trait StorageManager: Send + Sync {
async fn get_header(&self, height: u32) -> StorageResult<Option<BlockHeader>>;

/// Get the current tip blockchain height.
async fn get_tip_height(&self) -> StorageResult<Option<u32>>;
async fn get_tip_height(&self) -> Option<u32>;

async fn get_start_height(&self) -> Option<u32>;

async fn get_stored_headers_len(&self) -> u32;

/// Store filter headers.
async fn store_filter_headers(&mut self, headers: &[FilterHeader]) -> StorageResult<()>;
Expand Down
31 changes: 26 additions & 5 deletions dash-spv/src/storage/segments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ pub struct SegmentCache<I: Persistable> {
segments: HashMap<u32, Segment<I>>,
evicted: HashMap<u32, Segment<I>>,
tip_height: Option<u32>,
start_height: Option<u32>,
base_path: PathBuf,
}

Expand Down Expand Up @@ -133,12 +134,14 @@ impl<I: Persistable> SegmentCache<I> {
segments: HashMap::with_capacity(Self::MAX_ACTIVE_SEGMENTS),
evicted: HashMap::new(),
tip_height: None,
start_height: None,
base_path,
};

// Building the metadata
if let Ok(entries) = fs::read_dir(&items_dir) {
let mut max_segment_id = None;
let mut max_seg_id = None;
let mut min_seg_id = None;

for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str() {
Expand All @@ -149,19 +152,27 @@ impl<I: Persistable> SegmentCache<I> {
let segment_id_end = segment_id_start + 4;

if let Ok(id) = name[segment_id_start..segment_id_end].parse::<u32>() {
max_segment_id =
Some(max_segment_id.map_or(id, |max: u32| max.max(id)));
max_seg_id = Some(max_seg_id.map_or(id, |max: u32| max.max(id)));
min_seg_id = Some(min_seg_id.map_or(id, |min: u32| min.min(id)));
}
}
}
}

if let Some(segment_id) = max_segment_id {
if let Some(segment_id) = max_seg_id {
let segment = cache.get_segment(&segment_id).await?;

cache.tip_height = segment
.last_valid_offset()
.map(|offset| segment_id * Segment::<I>::ITEMS_PER_SEGMENT + offset);
.map(|offset| Self::segment_id_to_start_height(segment_id) + offset);
}

if let Some(segment_id) = min_seg_id {
let segment = cache.get_segment(&segment_id).await?;

cache.start_height = segment
.first_valid_offset()
.map(|offset| Self::segment_id_to_start_height(segment_id) + offset);
}
}

Expand Down Expand Up @@ -349,6 +360,11 @@ impl<I: Persistable> SegmentCache<I> {
None => Some(height - 1),
};

self.start_height = match self.start_height {
Some(current) => Some(current.min(start_height)),
None => Some(start_height),
};

Ok(())
}

Expand Down Expand Up @@ -377,6 +393,11 @@ impl<I: Persistable> SegmentCache<I> {
self.tip_height
}

#[inline]
pub fn start_height(&self) -> Option<u32> {
self.start_height
}

#[inline]
pub fn next_height(&self) -> u32 {
match self.tip_height() {
Expand Down
46 changes: 33 additions & 13 deletions dash-spv/src/storage/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ use super::manager::DiskStorageManager;
impl DiskStorageManager {
/// Store chain state to disk.
pub async fn store_chain_state(&mut self, state: &ChainState) -> StorageResult<()> {
// First store all headers
// For checkpoint sync, we need to store headers starting from the checkpoint height
self.store_headers_at_height(&state.headers, state.sync_base_height).await?;

// Store other state as JSON
let state_data = serde_json::json!({
"last_chainlock_height": state.last_chainlock_height,
Expand Down Expand Up @@ -48,7 +44,7 @@ impl DiskStorageManager {
crate::error::StorageError::Serialization(format!("Failed to parse chain state: {}", e))
})?;

let mut state = ChainState {
let state = ChainState {
last_chainlock_height: value
.get("last_chainlock_height")
.and_then(|v| v.as_u64())
Expand All @@ -71,14 +67,8 @@ impl DiskStorageManager {
.and_then(|v| v.as_u64())
.map(|h| h as u32)
.unwrap_or(0),
..Default::default()
};

let range_start = state.sync_base_height;
if let Some(tip_height) = self.get_tip_height().await? {
state.headers = self.load_headers(range_start..tip_height + 1).await?;
}

Ok(Some(state))
}

Expand Down Expand Up @@ -340,11 +330,40 @@ impl StorageManager for DiskStorageManager {
}

async fn get_header(&self, height: u32) -> StorageResult<Option<BlockHeader>> {
if self.get_tip_height().await.is_none_or(|tip_height| height > tip_height) {
return Ok(None);
}

if self.get_start_height().await.is_none_or(|start_height| height < start_height) {
return Ok(None);
}

Ok(self.block_headers.write().await.get_items(height..height + 1).await?.first().copied())
}

async fn get_tip_height(&self) -> StorageResult<Option<u32>> {
Ok(self.block_headers.read().await.tip_height())
async fn get_tip_height(&self) -> Option<u32> {
self.block_headers.read().await.tip_height()
}

async fn get_start_height(&self) -> Option<u32> {
self.block_headers.read().await.start_height()
}

async fn get_stored_headers_len(&self) -> u32 {
let headers_guard = self.block_headers.read().await;
let start_height = if let Some(start_height) = headers_guard.start_height() {
start_height
} else {
return 0;
};

let end_height = if let Some(end_height) = headers_guard.tip_height() {
end_height
} else {
return 0;
};

end_height - start_height + 1
}

async fn store_filter_headers(
Expand Down Expand Up @@ -575,6 +594,7 @@ mod tests {
storage.store_chain_state(&base_state).await?;

storage.store_headers_at_height(&headers, checkpoint_height).await?;
assert_eq!(storage.get_stored_headers_len().await, headers.len() as u32);

// Verify headers are stored at correct blockchain heights
let header_at_base = storage.get_header(checkpoint_height).await?;
Expand Down
Loading
Loading