Skip to content
6 changes: 0 additions & 6 deletions dash-spv-ffi/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,20 +181,14 @@ impl From<DetailedSyncProgress> for FFIDetailedSyncProgress {

#[repr(C)]
pub struct FFIChainState {
pub masternode_height: u32,
pub last_chainlock_height: u32,
pub last_chainlock_hash: FFIString,
pub current_filter_tip: u32,
}

impl From<ChainState> for FFIChainState {
fn from(state: ChainState) -> Self {
FFIChainState {
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(
&state.last_chainlock_hash.map(|h| h.to_string()).unwrap_or_default(),
),
current_filter_tip: 0, // FilterHeader not directly convertible to u32
}
}
Expand Down
11 changes: 0 additions & 11 deletions dash-spv-ffi/tests/unit/test_type_conversions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,24 +164,13 @@ mod tests {
fn test_chain_state_none_values() {
let state = dash_spv::ChainState {
last_chainlock_height: None,
last_chainlock_hash: None,
current_filter_tip: None,
masternode_engine: None,
last_masternode_diff_height: None,
sync_base_height: 0,
};

let ffi_state = FFIChainState::from(state);

assert_eq!(ffi_state.masternode_height, 0);
assert_eq!(ffi_state.last_chainlock_height, 0);
assert_eq!(ffi_state.current_filter_tip, 0);

unsafe {
let hash_str = FFIString::from_ptr(ffi_state.last_chainlock_hash.ptr).unwrap();
assert_eq!(hash_str, "");
dash_spv_ffi_string_destroy(ffi_state.last_chainlock_hash);
}
}

#[test]
Expand Down
8 changes: 4 additions & 4 deletions dash-spv/src/chain/chainlock_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ mod tests {
storage::{BlockHeaderStorage, DiskStorageManager},
types::ChainState,
};
use dashcore::{Header, Network};
use dashcore::Header;

#[tokio::test]
async fn test_chainlock_processing() {
// Create storage and ChainLock manager
let mut storage =
DiskStorageManager::with_temp_dir().await.expect("Failed to create tmp storage");
let chainlock_manager = ChainLockManager::new(true);
let chain_state = ChainState::new_for_network(Network::Testnet);
let chain_state = ChainState::new();

let chainlock = ChainLock::dummy(1000);

Expand Down Expand Up @@ -41,7 +41,7 @@ mod tests {
let mut storage =
DiskStorageManager::with_temp_dir().await.expect("Failed to create tmp storage");
let chainlock_manager = ChainLockManager::new(true);
let chain_state = ChainState::new_for_network(Network::Testnet);
let chain_state = ChainState::new();

let chainlock1 = ChainLock::dummy(1000);

Expand Down Expand Up @@ -69,7 +69,7 @@ mod tests {
#[tokio::test]
async fn test_reorganization_protection() {
let chainlock_manager = ChainLockManager::new(true);
let chain_state = ChainState::new_for_network(Network::Testnet);
let chain_state = ChainState::new();
let mut storage =
DiskStorageManager::with_temp_dir().await.expect("Failed to create tmp storage");

Expand Down
1 change: 0 additions & 1 deletion dash-spv/src/client/chainlock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ impl<W: WalletInterface, N: NetworkManager, S: StorageManager> DashSpvClient<W,

// Update our confirmed chain tip
state.last_chainlock_height = Some(chainlock.block_height);
state.last_chainlock_hash = Some(chainlock.block_hash);

tracing::info!(
"🔒 Updated confirmed chain tip to ChainLock at height {} ({})",
Expand Down
2 changes: 1 addition & 1 deletion dash-spv/src/client/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ impl<W: WalletInterface, N: NetworkManager, S: StorageManager> DashSpvClient<W,
// Reset in-memory chain state to a clean baseline for the current network
{
let mut state = self.state.write().await;
*state = ChainState::new_for_network(self.config.network);
*state = ChainState::new();
}

// Reset sync manager filter state (headers/filters progress trackers)
Expand Down
8 changes: 2 additions & 6 deletions dash-spv/src/client/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ impl<W: WalletInterface, N: NetworkManager, S: StorageManager> DashSpvClient<W,
config.validate().map_err(SpvError::Config)?;

// Initialize state for the network
let state = Arc::new(RwLock::new(ChainState::new_for_network(config.network)));
let state = Arc::new(RwLock::new(ChainState::new()));
let stats = Arc::new(RwLock::new(SpvStats::default()));

// Wrap storage in Arc<Mutex>
Expand Down Expand Up @@ -281,11 +281,7 @@ impl<W: WalletInterface, N: NetworkManager, S: StorageManager> DashSpvClient<W,
);
} else {
// Initialize chain state from checkpoint
chain_state.init_from_checkpoint(
checkpoint.height,
checkpoint_header,
self.config.network,
);
chain_state.init_from_checkpoint(checkpoint.height);

// Clone the chain state for storage
let chain_state_for_storage = (*chain_state).clone();
Expand Down
15 changes: 13 additions & 2 deletions dash-spv/src/client/status_display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,27 @@ impl<'a, S: StorageManager, W: WalletInterface> StatusDisplay<'a, S, W> {
// Calculate the actual header height considering checkpoint sync
let header_height = self.calculate_header_height(&state).await;

// Get filter header height from storage
let storage = self.storage.lock().await;

// Get filter header height from storage
let filter_header_height =
storage.get_filter_tip_height().await.ok().flatten().unwrap_or(0);

// Get masternode height from storage
let masternode_height = storage
.load_masternode_state()
.await
.ok()
.flatten()
.map(|state| state.last_height)
.unwrap_or(0);

drop(storage);

Ok(SyncProgress {
header_height,
filter_header_height,
masternode_height: state.last_masternode_diff_height.unwrap_or(0),
masternode_height,
peer_count: 1, // TODO: Get from network manager
filter_sync_available: false, // TODO: Get from network manager
filters_downloaded: filters_received,
Expand Down
16 changes: 0 additions & 16 deletions dash-spv/src/storage/chainstate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,6 @@ impl ChainStateStorage for PersistentChainStateStorage {
async fn store_chain_state(&mut self, state: &ChainState) -> StorageResult<()> {
let state_data = serde_json::json!({
"last_chainlock_height": state.last_chainlock_height,
"last_chainlock_hash": state.last_chainlock_hash,
"current_filter_tip": state.current_filter_tip,
"last_masternode_diff_height": state.last_masternode_diff_height,
"sync_base_height": state.sync_base_height,
});

Expand Down Expand Up @@ -76,19 +73,6 @@ impl ChainStateStorage for PersistentChainStateStorage {
.get("last_chainlock_height")
.and_then(|v| v.as_u64())
.map(|h| h as u32),
last_chainlock_hash: value
.get("last_chainlock_hash")
.and_then(|v| v.as_str())
.and_then(|s| s.parse().ok()),
current_filter_tip: value
.get("current_filter_tip")
.and_then(|v| v.as_str())
.and_then(|s| s.parse().ok()),
masternode_engine: None,
last_masternode_diff_height: value
.get("last_masternode_diff_height")
.and_then(|v| v.as_u64())
.map(|h| h as u32),
sync_base_height: value
.get("sync_base_height")
.and_then(|v| v.as_u64())
Expand Down
107 changes: 3 additions & 104 deletions dash-spv/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@ use std::time::{Duration, Instant, SystemTime};
use dashcore::{
block::Header as BlockHeader,
consensus::{Decodable, Encodable},
hash_types::FilterHeader,
network::constants::NetworkExt,
sml::masternode_list_engine::MasternodeListEngine,
Amount, BlockHash, Network, Transaction, Txid,
Amount, BlockHash, Transaction, Txid,
};
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -262,18 +259,6 @@ pub struct ChainState {
/// Last ChainLock height.
pub last_chainlock_height: Option<u32>,

/// Last ChainLock hash.
pub last_chainlock_hash: Option<BlockHash>,

/// Current filter tip.
pub current_filter_tip: Option<FilterHeader>,

/// Masternode list engine.
pub masternode_engine: Option<MasternodeListEngine>,

/// Last masternode diff height processed.
pub last_masternode_diff_height: Option<u32>,

/// Base height when syncing from a checkpoint (0 if syncing from genesis).
pub sync_base_height: u32,
}
Expand All @@ -284,105 +269,19 @@ impl ChainState {
Self::default()
}

/// Create a new chain state for the given network.
pub fn new_for_network(network: Network) -> Self {
let mut state = Self::default();

// Initialize masternode engine for the network
let mut engine = MasternodeListEngine::default_for_network(network);
if let Some(genesis_hash) = network.known_genesis_block_hash() {
engine.feed_block_height(0, genesis_hash);
}
state.masternode_engine = Some(engine);

// Initialize checkpoint fields
state.sync_base_height = 0;

state
}

/// Whether the chain was synced from a checkpoint rather than genesis.
pub fn synced_from_checkpoint(&self) -> bool {
self.sync_base_height > 0
}

/// Update chain lock status
pub fn update_chain_lock(&mut self, height: u32, hash: BlockHash) {
// Only update if this is a newer chain lock
if self.last_chainlock_height.is_none_or(|h| height > h) {
self.last_chainlock_height = Some(height);
self.last_chainlock_hash = Some(hash);
}
}

/// Check if a block at given height is chain-locked
pub fn is_height_chain_locked(&self, height: u32) -> bool {
self.last_chainlock_height.is_some_and(|locked_height| height <= locked_height)
}

/// Check if we have a chain lock
pub fn has_chain_lock(&self) -> bool {
self.last_chainlock_height.is_some()
}

/// Get the last chain-locked height
pub fn get_last_chainlock_height(&self) -> Option<u32> {
self.last_chainlock_height
}

/// Get filter matched heights (placeholder for now)
/// In a real implementation, this would track heights where filters matched wallet transactions
pub fn get_filter_matched_heights(&self) -> Option<Vec<u32>> {
// For now, return an empty vector as we don't track this yet
// This would typically be populated during filter sync when matches are found
Some(Vec::new())
}

/// Initialize chain state from a checkpoint.
pub fn init_from_checkpoint(
&mut self,
checkpoint_height: u32,
checkpoint_header: BlockHeader,
network: Network,
) {
pub fn init_from_checkpoint(&mut self, checkpoint_height: u32) {
// Set sync base height to checkpoint
self.sync_base_height = checkpoint_height;

tracing::info!(
"Initialized ChainState from checkpoint - height: {}, hash: {}, network: {:?}",
checkpoint_height,
checkpoint_header.block_hash(),
network
);

// Initialize masternode engine for the network, starting from checkpoint
let mut engine = MasternodeListEngine::default_for_network(network);
engine.feed_block_height(checkpoint_height, checkpoint_header.block_hash());
self.masternode_engine = Some(engine);
}

/// Get the absolute height for a given index in our headers vector.
pub fn index_to_height(&self, index: usize) -> u32 {
self.sync_base_height + index as u32
}

/// Get the index in our headers vector for a given absolute height.
pub fn height_to_index(&self, height: u32) -> Option<usize> {
if height < self.sync_base_height {
None
} else {
Some((height - self.sync_base_height) as usize)
}
tracing::info!("Initialized ChainState from checkpoint - height: {}", checkpoint_height);
Comment on lines +273 to +277
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reset chainlock height when re-initializing from a checkpoint.

init_from_checkpoint updates only sync_base_height. If a ChainState instance is reused, last_chainlock_height can carry over and misreport chainlock state after a checkpoint reset. Consider clearing it here.

🐛 Proposed fix
 pub fn init_from_checkpoint(&mut self, checkpoint_height: u32) {
     // Set sync base height to checkpoint
     self.sync_base_height = checkpoint_height;
+    self.last_chainlock_height = None;

     tracing::info!("Initialized ChainState from checkpoint - height: {}", checkpoint_height);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn init_from_checkpoint(&mut self, checkpoint_height: u32) {
// Set sync base height to checkpoint
self.sync_base_height = checkpoint_height;
tracing::info!(
"Initialized ChainState from checkpoint - height: {}, hash: {}, network: {:?}",
checkpoint_height,
checkpoint_header.block_hash(),
network
);
// Initialize masternode engine for the network, starting from checkpoint
let mut engine = MasternodeListEngine::default_for_network(network);
engine.feed_block_height(checkpoint_height, checkpoint_header.block_hash());
self.masternode_engine = Some(engine);
}
/// Get the absolute height for a given index in our headers vector.
pub fn index_to_height(&self, index: usize) -> u32 {
self.sync_base_height + index as u32
}
/// Get the index in our headers vector for a given absolute height.
pub fn height_to_index(&self, height: u32) -> Option<usize> {
if height < self.sync_base_height {
None
} else {
Some((height - self.sync_base_height) as usize)
}
tracing::info!("Initialized ChainState from checkpoint - height: {}", checkpoint_height);
pub fn init_from_checkpoint(&mut self, checkpoint_height: u32) {
// Set sync base height to checkpoint
self.sync_base_height = checkpoint_height;
self.last_chainlock_height = None;
tracing::info!("Initialized ChainState from checkpoint - height: {}", checkpoint_height);
}
🤖 Prompt for AI Agents
In `@dash-spv/src/types.rs` around lines 273 - 277, When re-initializing
ChainState in the init_from_checkpoint(&mut self, checkpoint_height: u32)
method, also reset the last_chainlock_height field so stale chainlock data isn't
carried over after a checkpoint; update the method to set
self.last_chainlock_height (or the appropriate ChainState field that tracks
chainlocks) to zero/None/default immediately when setting self.sync_base_height
to checkpoint_height and retain the tracing::info log.

}
}

impl std::fmt::Debug for ChainState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ChainState")
.field("last_chainlock_height", &self.last_chainlock_height)
.field("last_chainlock_hash", &self.last_chainlock_hash)
.field("current_filter_tip", &self.current_filter_tip)
.field("last_masternode_diff_height", &self.last_masternode_diff_height)
.field("sync_base_height", &self.sync_base_height)
.finish()
}
Expand Down
4 changes: 2 additions & 2 deletions dash-spv/tests/header_sync_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,14 @@ async fn test_prepare_sync(sync_base_height: u32, header_count: usize) {
let expected_tip_hash = headers.last().unwrap().block_hash();

// Create and store chain state
let mut chain_state = ChainState::new_for_network(Network::Dash);
let mut chain_state = ChainState::new();
chain_state.sync_base_height = sync_base_height;
storage.store_chain_state(&chain_state).await.expect("Failed to store chain state");
storage.store_headers(&headers).await.expect("Failed to store headers");

// Create HeaderSyncManager and load from storage
let config = ClientConfig::new(Network::Dash);
let chain_state_arc = Arc::new(RwLock::new(ChainState::new_for_network(Network::Dash)));
let chain_state_arc = Arc::new(RwLock::new(ChainState::new()));
let mut header_sync = HeaderSyncManager::<DiskStorageManager, PeerNetworkManager>::new(
&config,
ReorgConfig::default(),
Expand Down
Loading