From 288b0855467cf73eceacd0ef20f1b2d2f44a08c4 Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Mon, 19 Jan 2026 19:04:29 +0000 Subject: [PATCH] checkpoint rewrite --- dash-spv-ffi/FFI_API.md | 18 +- dash-spv-ffi/include/dash_spv_ffi.h | 50 +- dash-spv-ffi/src/checkpoints.rs | 121 +--- dash-spv/checkpoints/mainnet.checkpoints | Bin 0 -> 812 bytes dash-spv/checkpoints/testnet.checkpoints | Bin 0 -> 464 bytes dash-spv/src/chain/checkpoint_test.rs | 182 ------ dash-spv/src/chain/checkpoints.rs | 678 ++++++++------------ dash-spv/src/chain/mod.rs | 9 +- dash-spv/src/client/lifecycle.rs | 129 ++-- dash-spv/src/sync/legacy/headers/manager.rs | 75 +-- dash-spv/src/test_utils/checkpoint.rs | 32 +- 11 files changed, 377 insertions(+), 917 deletions(-) create mode 100644 dash-spv/checkpoints/mainnet.checkpoints create mode 100644 dash-spv/checkpoints/testnet.checkpoints delete mode 100644 dash-spv/src/chain/checkpoint_test.rs diff --git a/dash-spv-ffi/FFI_API.md b/dash-spv-ffi/FFI_API.md index 043c69396..7cf70b090 100644 --- a/dash-spv-ffi/FFI_API.md +++ b/dash-spv-ffi/FFI_API.md @@ -4,7 +4,7 @@ This document provides a comprehensive reference for all FFI (Foreign Function I **Auto-generated**: This documentation is automatically generated from the source code. Do not edit manually. -**Total Functions**: 66 +**Total Functions**: 65 ## Table of Contents @@ -125,7 +125,7 @@ Functions: 2 ### Utility Functions -Functions: 19 +Functions: 18 | Function | Description | Module | |----------|-------------|--------| @@ -133,7 +133,6 @@ Functions: 19 | `dash_spv_ffi_checkpoint_before_height` | Get the last checkpoint at or before a given height | checkpoints | | `dash_spv_ffi_checkpoint_before_timestamp` | Get the last checkpoint at or before a given UNIX timestamp (seconds) | checkpoints | | `dash_spv_ffi_checkpoint_latest` | Get the latest checkpoint for the given network | checkpoints | -| `dash_spv_ffi_checkpoints_between_heights` | Get all checkpoints between two heights (inclusive) | checkpoints | | `dash_spv_ffi_client_clear_storage` | Clear all persisted SPV storage (headers, filters, metadata, sync state) | client | | `dash_spv_ffi_client_get_stats` | Get current runtime statistics for the SPV client | client | | `dash_spv_ffi_client_get_tip_hash` | Get the current chain tip hash (32 bytes) if available | client | @@ -949,19 +948,6 @@ Get the latest checkpoint for the given network. # Safety - `out_height` must b --- -#### `dash_spv_ffi_checkpoints_between_heights` - -```c -dash_spv_ffi_checkpoints_between_heights(network: FFINetwork, start_height: u32, end_height: u32,) -> FFIArray -``` - -**Description:** -Get all checkpoints between two heights (inclusive). Returns an `FFIArray` of `FFICheckpoint` items. The caller owns the memory and must free the array buffer using `dash_spv_ffi_array_destroy` when done. - -**Module:** `checkpoints` - ---- - #### `dash_spv_ffi_client_clear_storage` ```c diff --git a/dash-spv-ffi/include/dash_spv_ffi.h b/dash-spv-ffi/include/dash_spv_ffi.h index 934b2597c..3aecf1e67 100644 --- a/dash-spv-ffi/include/dash_spv_ffi.h +++ b/dash-spv-ffi/include/dash_spv_ffi.h @@ -42,25 +42,6 @@ typedef enum FFIMempoolStrategy { typedef struct FFIDashSpvClient FFIDashSpvClient; -/** - * FFI-safe array that transfers ownership of memory to the C caller. - * - * # Safety - * - * This struct represents memory that has been allocated by Rust but ownership - * has been transferred to the C caller. The caller is responsible for: - * - Not accessing the memory after it has been freed - * - Calling `dash_spv_ffi_array_destroy` to properly deallocate the memory - * - Ensuring the data, len, and capacity fields remain consistent - */ -typedef struct FFIArray { - void *data; - uintptr_t len; - uintptr_t capacity; - uintptr_t elem_size; - uintptr_t elem_align; -} FFIArray; - typedef struct FFIClientConfig { void *inner; uint32_t worker_threads; @@ -186,6 +167,25 @@ typedef struct FFIResult { const char *error_message; } FFIResult; +/** + * FFI-safe array that transfers ownership of memory to the C caller. + * + * # Safety + * + * This struct represents memory that has been allocated by Rust but ownership + * has been transferred to the C caller. The caller is responsible for: + * - Not accessing the memory after it has been freed + * - Calling `dash_spv_ffi_array_destroy` to properly deallocate the memory + * - Ensuring the data, len, and capacity fields remain consistent + */ +typedef struct FFIArray { + void *data; + uintptr_t len; + uintptr_t capacity; + uintptr_t elem_size; + uintptr_t elem_align; +} FFIArray; + /** * FFI-safe representation of an unconfirmed transaction * @@ -264,18 +264,6 @@ int32_t dash_spv_ffi_checkpoint_before_timestamp(FFINetwork network, uint8_t *out_hash) ; -/** - * Get all checkpoints between two heights (inclusive). - * - * Returns an `FFIArray` of `FFICheckpoint` items. The caller owns the memory and - * must free the array buffer using `dash_spv_ffi_array_destroy` when done. - */ - -struct FFIArray dash_spv_ffi_checkpoints_between_heights(FFINetwork network, - uint32_t start_height, - uint32_t end_height) -; - /** * Create a new SPV client and return an opaque pointer. * diff --git a/dash-spv-ffi/src/checkpoints.rs b/dash-spv-ffi/src/checkpoints.rs index 177a67d81..169825f34 100644 --- a/dash-spv-ffi/src/checkpoints.rs +++ b/dash-spv-ffi/src/checkpoints.rs @@ -1,7 +1,6 @@ -use crate::{set_last_error, FFIArray, FFIErrorCode}; -use dash_spv::chain::checkpoints::{mainnet_checkpoints, testnet_checkpoints, CheckpointManager}; +use crate::{set_last_error, FFIErrorCode}; +use dash_spv::chain::CheckpointManager; use dashcore::hashes::Hash; -use dashcore::Network; use key_wallet_ffi::FFINetwork; /// FFI representation of a checkpoint (height + block hash) @@ -11,15 +10,6 @@ pub struct FFICheckpoint { pub block_hash: [u8; 32], } -fn manager_for_network(network: FFINetwork) -> Result { - let net: Network = network.into(); - match net { - Network::Dash => Ok(CheckpointManager::new(mainnet_checkpoints())), - Network::Testnet => Ok(CheckpointManager::new(testnet_checkpoints())), - _ => Err("Checkpoints are only available for Dash and Testnet".to_string()), - } -} - /// Get the latest checkpoint for the given network. /// /// # Safety @@ -31,26 +21,7 @@ pub unsafe extern "C" fn dash_spv_ffi_checkpoint_latest( out_height: *mut u32, out_hash: *mut u8, // expects at least 32 bytes ) -> i32 { - if out_height.is_null() || out_hash.is_null() { - set_last_error("Null output pointer provided"); - return FFIErrorCode::NullPointer as i32; - } - let mgr = match manager_for_network(network) { - Ok(m) => m, - Err(e) => { - set_last_error(&e); - return FFIErrorCode::InvalidArgument as i32; - } - }; - if let Some(cp) = mgr.last_checkpoint() { - *out_height = cp.height; - let hash = cp.block_hash.to_byte_array(); - std::ptr::copy_nonoverlapping(hash.as_ptr(), out_hash, 32); - FFIErrorCode::Success as i32 - } else { - set_last_error("No checkpoints available for network"); - FFIErrorCode::NotImplemented as i32 - } + dash_spv_ffi_checkpoint_before_height(network, u32::MAX, out_height, out_hash) } /// Get the last checkpoint at or before a given height. @@ -69,22 +40,14 @@ pub unsafe extern "C" fn dash_spv_ffi_checkpoint_before_height( set_last_error("Null output pointer provided"); return FFIErrorCode::NullPointer as i32; } - let mgr = match manager_for_network(network) { - Ok(m) => m, - Err(e) => { - set_last_error(&e); - return FFIErrorCode::InvalidArgument as i32; - } - }; - if let Some(cp) = mgr.last_checkpoint_before_height(height) { - *out_height = cp.height; - let hash = cp.block_hash.to_byte_array(); - std::ptr::copy_nonoverlapping(hash.as_ptr(), out_hash, 32); - FFIErrorCode::Success as i32 - } else { - set_last_error("No checkpoint at or before given height"); - FFIErrorCode::ValidationError as i32 - } + + let mgr = CheckpointManager::new(network.into()); + + let (height, cp) = mgr.last_checkpoint_before_height(height); + *out_height = height; + let hash = cp.hash().to_byte_array(); + std::ptr::copy_nonoverlapping(hash.as_ptr(), out_hash, 32); + FFIErrorCode::Success as i32 } /// Get the last checkpoint at or before a given UNIX timestamp (seconds). @@ -103,60 +66,12 @@ pub unsafe extern "C" fn dash_spv_ffi_checkpoint_before_timestamp( set_last_error("Null output pointer provided"); return FFIErrorCode::NullPointer as i32; } - let mgr = match manager_for_network(network) { - Ok(m) => m, - Err(e) => { - set_last_error(&e); - return FFIErrorCode::InvalidArgument as i32; - } - }; - if let Some(cp) = mgr.last_checkpoint_before_timestamp(timestamp) { - *out_height = cp.height; - let hash = cp.block_hash.to_byte_array(); - std::ptr::copy_nonoverlapping(hash.as_ptr(), out_hash, 32); - FFIErrorCode::Success as i32 - } else { - set_last_error("No checkpoint at or before given timestamp"); - FFIErrorCode::ValidationError as i32 - } -} -/// Get all checkpoints between two heights (inclusive). -/// -/// Returns an `FFIArray` of `FFICheckpoint` items. The caller owns the memory and -/// must free the array buffer using `dash_spv_ffi_array_destroy` when done. -#[no_mangle] -pub extern "C" fn dash_spv_ffi_checkpoints_between_heights( - network: FFINetwork, - start_height: u32, - end_height: u32, -) -> FFIArray { - match manager_for_network(network) { - Ok(mgr) => { - // Collect checkpoints within inclusive range - let mut out: Vec = Vec::new(); - for &h in mgr.checkpoint_heights() { - if h >= start_height && h <= end_height { - if let Some(cp) = mgr.get_checkpoint(h) { - out.push(FFICheckpoint { - height: cp.height, - block_hash: cp.block_hash.to_byte_array(), - }); - } - } - } - FFIArray::new(out) - } - Err(e) => { - set_last_error(&e); - // Return empty array on error - FFIArray { - data: std::ptr::null_mut(), - len: 0, - capacity: 0, - elem_size: std::mem::size_of::(), - elem_align: std::mem::align_of::(), - } - } - } + let mgr = CheckpointManager::new(network.into()); + + let (height, cp) = mgr.last_checkpoint_before_timestamp(timestamp); + *out_height = height; + let hash = cp.hash().to_byte_array(); + std::ptr::copy_nonoverlapping(hash.as_ptr(), out_hash, 32); + FFIErrorCode::Success as i32 } diff --git a/dash-spv/checkpoints/mainnet.checkpoints b/dash-spv/checkpoints/mainnet.checkpoints new file mode 100644 index 0000000000000000000000000000000000000000..949328481414a926d5e83b58283473df44dde75e GIT binary patch literal 812 zcmZQzU|?VbVnX0}(z396n*)iZ7vbXl8?CfKDz@&RS=)-@0 zxkL6l8MjqAd_Eh&;#?)rFA!>6JjeW*((Hq4Pi<}$;g0;v&%jVG%)r0|v~bzPd~IDn zwp~GwlBybZ=~vv-yQ2)lAtm*aB2giHOQyd3WJ?8;)MX2)W|>W0HFEdv*>BU=yzum` z{6inNb+_;CmX_E8wvmBh!3RmOopKy^o*3Of=iDKXzouXd$E`WM`<66bHw=@4s$*>~ ze&g-%cfDEvlJ=|pYU`D(^z`!_U))bUWc}w{_`h3;zmF#IJyw^LN!{_-ip4YQ;q=!_ z@+WF9Q~TE7GUsp8^TNfIrLs`79{iO7+sVt-CI9;0?s-p*@19;z@!vCBjcJGM*I8D4 zS)YXF9!}l1Zs*c7;T_9kOiZ7vbXl8?CfKDaPQ&cpb!7~ zBVX z`0C4UhSnW+(^zJO#jTT@ul35de4g<&prh{3PwRc1^3bbL_UWn#Y+DtI*z+{2rt*FI z+8lmld3jIbQpfoT($P$EH#Ha-mQOjuw{D%o)I;wym&eRDiIPe6Pn!4m;$pkG%AYQP zY%}En+c}ec_l5V9_q*0#ui0zesP6iI@zOc`s}J>-%{Zhe3AA&{Z{JRtnpbx=3VvR0 zF1(1>aNcyG1t-3q4LrB&%zK9BtEYwMCQEiR%4xcCGVuIfv7&vcq_d#Gn*(C+q?%)Q zdb6{&c<6<4{W@C;wC%wO0kEC&=N}1h+to-epCIsh!Ob7s7WGYb(89 literal 0 HcmV?d00001 diff --git a/dash-spv/src/chain/checkpoint_test.rs b/dash-spv/src/chain/checkpoint_test.rs deleted file mode 100644 index f7d2d9268..000000000 --- a/dash-spv/src/chain/checkpoint_test.rs +++ /dev/null @@ -1,182 +0,0 @@ -//! Comprehensive tests for checkpoint functionality - -#[cfg(test)] -mod tests { - use super::super::checkpoints::*; - use dashcore::BlockHash; - - #[test] - fn test_wallet_creation_time_checkpoint_selection() { - let checkpoints = - [1500000000, 1600000000, 1700000000, 1800000000].map(Checkpoint::dummy).to_vec(); - - let manager = CheckpointManager::new(checkpoints); - - // Test wallet created in 2019 - let wallet_time_2019 = 1550000000; - let checkpoint = manager.get_sync_checkpoint(Some(wallet_time_2019)); - assert_eq!(checkpoint.unwrap().height, 1500000000); - - // Test wallet created in 2022 - let wallet_time_2022 = 1650000000; - let checkpoint = manager.get_sync_checkpoint(Some(wallet_time_2022)); - assert_eq!(checkpoint.unwrap().height, 1600000000); - - // Test wallet created before any checkpoint - should return None - let wallet_time_ancient = 0; - let checkpoint = manager.get_sync_checkpoint(Some(wallet_time_ancient)); - assert!(checkpoint.is_none()); - - // Test no wallet creation time (should use latest) - let checkpoint = manager.get_sync_checkpoint(None); - assert_eq!(checkpoint.unwrap().height, 1800000000); - } - - #[test] - fn test_fork_rejection_logic() { - let checkpoints = - vec![Checkpoint::dummy(0), Checkpoint::dummy(100000), Checkpoint::dummy(200000)]; - - let manager = CheckpointManager::new(checkpoints.clone()); - - // Should reject forks before or at last checkpoint - assert!(manager.should_reject_fork(0)); - assert!(manager.should_reject_fork(50000)); - assert!(manager.should_reject_fork(100000)); - assert!(manager.should_reject_fork(200000)); - - // Should not reject forks after last checkpoint - assert!(!manager.should_reject_fork(200001)); - assert!(!manager.should_reject_fork(300000)); - } - - #[test] - fn test_checkpoint_protocol_version_extraction() { - let mut checkpoint = Checkpoint::dummy(100000); - - // Test with masternode list name - checkpoint.masternode_list_name = Some("ML100000__70227".to_string()); - assert_eq!(checkpoint.protocol_version(), Some(70227)); - - // Test with explicit protocol version (should take precedence) - checkpoint.protocol_version = Some(70230); - assert_eq!(checkpoint.protocol_version(), Some(70230)); - - // Test with invalid masternode list format - checkpoint.protocol_version = None; - checkpoint.masternode_list_name = Some("ML100000_invalid".to_string()); - assert_eq!(checkpoint.protocol_version(), None); - - // Test with no masternode list - checkpoint.masternode_list_name = None; - assert_eq!(checkpoint.protocol_version(), None); - } - - #[test] - fn test_checkpoint_binary_search_efficiency() { - // Create many checkpoints to test binary search - let checkpoints = [0, 100, 5000, 90000, 999000, 1000000].map(Checkpoint::dummy).to_vec(); - - let manager = CheckpointManager::new(checkpoints.clone()); - - // Test various heights - assert_eq!(manager.last_checkpoint_before_height(0).unwrap().height, 0); - assert_eq!(manager.last_checkpoint_before_height(5500).unwrap().height, 5000); - assert_eq!(manager.last_checkpoint_before_height(999999).unwrap().height, 999000); - - // Test edge case: height before first checkpoint - assert!(manager.last_checkpoint_before_height(0).is_some()); - } - - #[test] - fn test_empty_checkpoint_manager() { - let manager = CheckpointManager::new(vec![]); - - assert!(manager.get_checkpoint(0).is_none()); - assert!(manager.last_checkpoint().is_none()); - assert!(manager.last_checkpoint_before_height(100000).is_none()); - assert!(manager.last_checkpoint_before_timestamp(1700000000).is_none()); - assert!(manager.checkpoint_heights().is_empty()); - assert!(!manager.should_reject_fork(100000)); - } - - #[test] - fn test_checkpoint_validation_edge_cases() { - let checkpoints = vec![Checkpoint::dummy(100000)]; - let manager = CheckpointManager::new(checkpoints.clone()); - - let correct_hash = manager.get_checkpoint(100000).unwrap().block_hash; - let wrong_hash = BlockHash::from([0u8; 32]); - - // Test validation at checkpoint height - assert!(manager.validate_block(100000, &correct_hash)); - assert!(!manager.validate_block(100000, &wrong_hash)); - - // Test validation at non-checkpoint height (should always pass) - assert!(manager.validate_block(99999, &wrong_hash)); - assert!(manager.validate_block(100001, &wrong_hash)); - } - - #[test] - fn test_checkpoint_sorting_and_lookup() { - // Create checkpoints in random order - let checkpoints = vec![ - Checkpoint::dummy(200000), - Checkpoint::dummy(0), - Checkpoint::dummy(300000), - Checkpoint::dummy(100000), - ]; - - let manager = CheckpointManager::new(checkpoints.clone()); - - // Verify heights are sorted - let heights = manager.checkpoint_heights(); - assert_eq!(heights, &[0, 100000, 200000, 300000]); - - // Verify lookups work correctly - assert_eq!(manager.get_checkpoint(0).unwrap().height, 0); - assert_eq!(manager.get_checkpoint(100000).unwrap().height, 100000); - assert_eq!(manager.get_checkpoint(200000).unwrap().height, 200000); - assert_eq!(manager.get_checkpoint(300000).unwrap().height, 300000); - } - - #[test] - fn test_mainnet_checkpoint_consistency() { - let checkpoints = mainnet_checkpoints(); - let manager = CheckpointManager::new(checkpoints.clone()); - - // Verify all checkpoints are properly ordered - let heights = manager.checkpoint_heights(); - for i in 1..heights.len() { - assert!(heights[i] > heights[i - 1], "Checkpoints not in ascending order"); - } - - // Verify all checkpoints have valid data - for checkpoint in &checkpoints { - assert!(checkpoint.timestamp > 0); - assert!(checkpoint.nonce > 0); - assert!(!checkpoint.chain_work.is_empty()); - - if checkpoint.height > 0 { - assert_ne!(checkpoint.prev_blockhash, BlockHash::from([0u8; 32])); - } - } - } - - #[test] - fn test_testnet_checkpoint_consistency() { - let checkpoints = testnet_checkpoints(); - let manager = CheckpointManager::new(checkpoints.clone()); - - // Similar validations as mainnet - let heights = manager.checkpoint_heights(); - for i in 1..heights.len() { - assert!(heights[i] > heights[i - 1]); - } - - for checkpoint in &checkpoints { - assert!(checkpoint.timestamp > 0); - assert!(!checkpoint.chain_work.is_empty()); - } - } -} diff --git a/dash-spv/src/chain/checkpoints.rs b/dash-spv/src/chain/checkpoints.rs index bbb198cb9..d71930a13 100644 --- a/dash-spv/src/chain/checkpoints.rs +++ b/dash-spv/src/chain/checkpoints.rs @@ -1,463 +1,341 @@ -//! Checkpoint system for chain validation and sync optimization -//! -//! Checkpoints are hardcoded blocks at specific heights that help: -//! - Prevent accepting blocks from invalid chains -//! - Optimize initial sync by starting from recent checkpoints -//! - Protect against deep reorganizations -//! - Bootstrap masternode lists at specific heights - -use dashcore::{BlockHash, CompactTarget, Target}; -use dashcore_hashes::{hex, Hash}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -/// A checkpoint representing a known valid block -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct Checkpoint { - /// Block height - pub height: u32, - /// Block hash - pub block_hash: BlockHash, - /// Previous block hash - pub prev_blockhash: BlockHash, - /// Block timestamp - pub timestamp: u32, - /// Difficulty target - pub target: Target, - /// Merkle root (optional for older checkpoints) - pub merkle_root: Option, - /// Cumulative chain work up to this block (as hex string) - pub chain_work: String, - /// Masternode list identifier (e.g., "ML1088640__70218") - pub masternode_list_name: Option, - /// Protocol version at this checkpoint - pub protocol_version: Option, - /// Nonce value for the block - pub nonce: u32, +//! Checkpoints are hardcoded blocks at specific heights that help sync from a given height + +use dashcore::{ + consensus::{encode, Decodable, Encodable}, + constants::genesis_block, + prelude::CoreBlockHeight, + Network, +}; + +use crate::types::HashedBlockHeader; + +// This files must exist in the checkpoint directory. If you don't have them, create +// empty ones and execute ``` cargo test generate_checkpoints_files -- --ignored ``` +const MAINNET_CHECKPOINTS_BYTES: &[u8] = include_bytes!("../../checkpoints/mainnet.checkpoints"); +const TESTNET_CHECKPOINTS_BYTES: &[u8] = include_bytes!("../../checkpoints/testnet.checkpoints"); + +// If you modify the heights you must regenerate the checkpoints files by +// executing ``` cargo test generate_checkpoints_files -- --ignored ``` +const MAINNET_CHECKPOINTS_HEIGHTS: [CoreBlockHeight; 7] = + [0, 4991, 107996, 750000, 1700000, 1900000, 2300000]; +const TESTNET_CHECKPOINTS_HEIGHTS: [CoreBlockHeight; 4] = [0, 500000, 800000, 1100000]; + +fn checkpoints_bytes(network: Network) -> &'static [u8] { + match network { + Network::Dash => MAINNET_CHECKPOINTS_BYTES, + Network::Testnet => TESTNET_CHECKPOINTS_BYTES, + // Other networks do not have hardcoded checkpoints, this will help + // trigger the CheckpointManager to add genesis as the only checkpoint + _ => &[], + } +} + +fn checkpoints_heights(network: Network) -> &'static [CoreBlockHeight] { + match network { + Network::Dash => &MAINNET_CHECKPOINTS_HEIGHTS, + Network::Testnet => &TESTNET_CHECKPOINTS_HEIGHTS, + // Other networks do not have hardcoded checkpoints, this will help + // trigger the CheckpointManager to add genesis as the only checkpoint + _ => &[], + } +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct Checkpoint { + height: CoreBlockHeight, + hashed_block_header: HashedBlockHeader, +} + +impl PartialOrd for Checkpoint { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.height.cmp(&other.height)) + } } impl Checkpoint { - /// Extract protocol version from masternode list name or use stored value - pub fn protocol_version(&self) -> Option { - // Prefer explicitly stored protocol version - if let Some(version) = self.protocol_version { - return Some(version); + pub fn new(height: CoreBlockHeight, hashed_block_header: HashedBlockHeader) -> Self { + Self { + height, + hashed_block_header, } + } +} - // Otherwise extract from masternode list name - self.masternode_list_name.as_ref().and_then(|name| { - // Format: "ML{height}__{protocol_version}" - name.split("__").nth(1).and_then(|s| s.parse().ok()) - }) +impl Encodable for Checkpoint { + #[inline] + fn consensus_encode( + &self, + writer: &mut W, + ) -> Result { + Ok(self.height.consensus_encode(writer)? + + self.hashed_block_header.consensus_encode(writer)?) } +} - /// Check if this checkpoint has an associated masternode list - pub fn has_masternode_list(&self) -> bool { - self.masternode_list_name.is_some() +impl Decodable for Checkpoint { + #[inline] + fn consensus_decode( + reader: &mut R, + ) -> Result { + Ok(Self { + height: CoreBlockHeight::consensus_decode(reader)?, + hashed_block_header: HashedBlockHeader::consensus_decode(reader)?, + }) } } -/// Manages checkpoints for a specific network pub struct CheckpointManager { - /// Checkpoints indexed by height - checkpoints: HashMap, - /// Sorted list of checkpoint heights for efficient searching - sorted_heights: Vec, + // checkpoints collection sorted by height, lowest first + checkpoints: Vec, } impl CheckpointManager { - /// Create a new checkpoint manager from a list of checkpoints - pub fn new(checkpoints: Vec) -> Self { - let mut checkpoint_map = HashMap::new(); - let mut heights = Vec::new(); + pub fn new(network: Network) -> Self { + let bytes = checkpoints_bytes(network); + let heights_len = checkpoints_heights(network).len(); + + let mut checkpoints = { + let mut items = Vec::with_capacity(heights_len); + let mut reader = bytes; + + loop { + match Checkpoint::consensus_decode(&mut reader) { + Ok(item) => items.push(item), + Err(encode::Error::Io(ref e)) + if e.kind() == std::io::ErrorKind::UnexpectedEof => + { + break + } + Err(_) => { + unreachable!("The bytes are hardcoded in the bin, decode cannot fail") + } + } + } - for checkpoint in checkpoints { - heights.push(checkpoint.height); - checkpoint_map.insert(checkpoint.height, checkpoint); - } + items + }; - heights.sort_unstable(); + debug_assert_eq!( + checkpoints.len(), + heights_len, + "Could not load checkpoints for all lengths, maybe the checkpoints files are not updated" + ); - Self { - checkpoints: checkpoint_map, - sorted_heights: heights, + #[cfg(debug_assertions)] + { + let heights = checkpoints_heights(network); + for (i, cp) in checkpoints.iter().enumerate() { + debug_assert_eq!( + cp.height, + heights[i], + "Checkpoint height does not match expected height, maybe the checkpoints files are not updated"); + } + } + + // If the list is empty (maybe we dont have any checkpoints) but we still + // want to be able to sync, we add the genesis block of the network as + // a fallback + if checkpoints.is_empty() { + let genesis = HashedBlockHeader::from(genesis_block(network).header); + let genesis_checkpoint = Checkpoint::new(0, genesis); + checkpoints.push(genesis_checkpoint); } - } - /// Get a checkpoint at a specific height - pub fn get_checkpoint(&self, height: u32) -> Option<&Checkpoint> { - self.checkpoints.get(&height) + Self::new_with_checkpoints(checkpoints) } - /// Check if a block hash matches the checkpoint at the given height - pub fn validate_block(&self, height: u32, block_hash: &BlockHash) -> bool { - match self.checkpoints.get(&height) { - Some(checkpoint) => checkpoint.block_hash == *block_hash, - None => true, // No checkpoint at this height, so it's valid + /// The input must be sorted by height in ascending order + pub(crate) fn new_with_checkpoints(checkpoints: Vec) -> Self { + debug_assert!(!checkpoints.is_empty(), "Checkpoints must contain, at least, genesis"); + + // We need to ensure the first checkpoint is at height 0, genesis, + // with it we ensure we have a valid checkpoint for any other height + // since there is no value lower than 0 for u32 ;D + debug_assert_eq!(checkpoints[0].height, 0, "The first checkpoint must be at height 0"); + + debug_assert!( + checkpoints.is_sorted(), + "The checkpoints must be sorted by height in ascending order" + ); + + Self { + checkpoints, } } /// Get the last checkpoint at or before the given height - pub fn last_checkpoint_before_height(&self, height: u32) -> Option<&Checkpoint> { - // Binary search for the highest checkpoint <= height - let pos = self.sorted_heights.partition_point(|&h| h <= height); - if pos > 0 { - let checkpoint_height = self.sorted_heights[pos - 1]; - self.checkpoints.get(&checkpoint_height) - } else { - None + pub fn last_checkpoint_before_height( + &self, + height: u32, + ) -> (CoreBlockHeight, &HashedBlockHeader) { + match self.checkpoints.binary_search_by_key(&height, |checkpoint| checkpoint.height) { + Ok(index) => { + (self.checkpoints[index].height, &self.checkpoints[index].hashed_block_header) + } + Err(index) => ( + self.checkpoints[index - 1].height, + &self.checkpoints[index - 1].hashed_block_header, + ), } } - /// Get the last checkpoint - pub fn last_checkpoint(&self) -> Option<&Checkpoint> { - self.sorted_heights.last().and_then(|&height| self.checkpoints.get(&height)) - } + /// Get the last checkpoint before a given timestamp + pub fn last_checkpoint_before_timestamp( + &self, + timestamp: u32, + ) -> (CoreBlockHeight, &HashedBlockHeader) { + let mut checkpoints = self.checkpoints.iter(); - /// Get all checkpoint heights - pub fn checkpoint_heights(&self) -> &[u32] { - &self.sorted_heights - } + let mut best_checkpoint = + checkpoints.next().expect("CheckpointManager should never be empty"); - /// Get the last checkpoint before a given timestamp - pub fn last_checkpoint_before_timestamp(&self, timestamp: u32) -> Option<&Checkpoint> { - let mut best_checkpoint = None; - let mut best_height = 0; - - for checkpoint in self.checkpoints.values() { - if checkpoint.timestamp <= timestamp && checkpoint.height >= best_height { - best_height = checkpoint.height; - best_checkpoint = Some(checkpoint); + for checkpoint in checkpoints { + if checkpoint.hashed_block_header.header().time <= timestamp + && checkpoint.height >= best_checkpoint.height + { + best_checkpoint = checkpoint; } } - best_checkpoint + (best_checkpoint.height, &best_checkpoint.hashed_block_header) } +} - /// Get the checkpoint to use for sync chain based on override settings - pub fn get_sync_checkpoint(&self, wallet_creation_time: Option) -> Option<&Checkpoint> { - // Default to checkpoint based on wallet creation time - if let Some(creation_time) = wallet_creation_time { - self.last_checkpoint_before_timestamp(creation_time) - } else { - self.last_checkpoint() - } - } +#[cfg(test)] +mod tests { + use std::{ + fs::OpenOptions, + io::{BufWriter, Write}, + path::PathBuf, + sync::Arc, + }; + + use key_wallet::wallet::ManagedWalletInfo; + use key_wallet_manager::WalletManager; + use tokio::sync::RwLock; + use tokio_util::sync::CancellationToken; + use tracing::level_filters::LevelFilter; + + use crate::{ + init_console_logging, + network::PeerNetworkManager, + storage::{BlockHeaderStorage, DiskStorageManager}, + types::SyncStage, + ClientConfig, DashSpvClient, + }; + + use super::*; + + // This must be manually executed every time we modify checkpoints heights to allow the library + // to generate checkpoint files by requesting the block headers using its own client + #[tokio::test] + #[ignore = "This tests is meant to re-generate checkpoints files"] + async fn generate_checkpoints_files() { + const SUPPORTED_NETWORKS: [Network; 2] = [Network::Dash, Network::Testnet]; + + const MAINNET_CHECKPOINTS_FILE: &str = "checkpoints/mainnet.checkpoints"; + const TESTNET_CHECKPOINTS_FILE: &str = "checkpoints/testnet.checkpoints"; - /// Check if a fork at the given height should be rejected due to checkpoint - pub fn should_reject_fork(&self, fork_height: u32) -> bool { - if let Some(last_checkpoint) = self.last_checkpoint() { - fork_height <= last_checkpoint.height - } else { - false + let _logging_guard = init_console_logging(LevelFilter::INFO).unwrap(); + + for network in SUPPORTED_NETWORKS { + generate_checkpoints_file(network) + .await + .unwrap_or_else(|_| panic!("Error generating checkpoints for network {network}")); } - } -} -/// Create mainnet checkpoints -pub fn mainnet_checkpoints() -> Vec { - vec![ - // Genesis block (required) - create_checkpoint( - 0, - "00000ffd590b1485b3caadc19b22e6379c733355108f107a430458cdf3407ab6", - "0000000000000000000000000000000000000000000000000000000000000000", - 1390095618, - 0x1e0ffff0, - "0x0000000000000000000000000000000000000000000000000000000100010001", - "e0028eb9648db56b1ac77cf090b99048a8007e2bb64b68f092c03c7f56a662c7", - 28917698, - None, - ), - // Early network checkpoint (1 week after genesis) - create_checkpoint( - 4991, - "000000003b01809551952460744d5dbb8fcbd6cbae3c220267bf7fa43f837367", - "000000001263f3327dd2f6bc445b47beb82fb8807a62e252ba064e2d2b6f91a6", - 1390163520, - 0x1e0fffff, - "0x00000000000000000000000000000000000000000000000000000000271027f0", - "7faff642d9e914716c50e3406df522b2b9a10ea3df4fef4e2229997367a6cab1", - 357631712, - None, - ), - // 3 months checkpoint - create_checkpoint( - 107996, - "00000000000a23840ac16115407488267aa3da2b9bc843e301185b7d17e4dc40", - "000000000006fe4020a310786bd34e17aa7681c86a20a2e121e0e3dd599800e8", - 1395522898, - 0x1b04864c, - "0x0000000000000000000000000000000000000000000000000056bf9caa56bf9d", - "15c3852f9e71a6cbc0cfa96d88202746cfeae6fc645ccc878580bc29daeff193", - 10049236, - None, - ), - // 2017 checkpoint - create_checkpoint( - 750000, - "00000000000000b4181bbbdddbae464ce11fede5d0292fb63fdede1e7c8ab21c", - "00000000000001e115237541be8dd91bce2653edd712429d11371842f85bd3e1", - 1491953700, - 0x1a075a02, - "0x00000000000000000000000000000000000000000000000485f01ee9f01ee9f8", - "0ce99835e2de1240e230b5075024817aace2b03b3944967a88af079744d0aa62", - 2199533779, - None, - ), - // Recent checkpoint with masternode list (2022) - create_checkpoint( - 1700000, - "000000000000001d7579a371e782fd9c4480f626a62b916fa4eb97e16a49043a", - "000000000000001a5631d781a4be0d9cda08b470ac6f108843cedf32e4dc081e", - 1657142113, - 0x1927e30e, - "000000000000000000000000000000000000000000007562df93a26b81386288", - "dafe57cefc3bc265dfe8416e2f2e3a22af268fd587a48f36affd404bec738305", - 3820512540, - Some("ML1700000__70227"), - ), - // Latest checkpoint with masternode list (2022/2023) - create_checkpoint( - 1900000, - "000000000000001b8187c744355da78857cca5b9aeb665c39d12f26a0e3a9af5", - "000000000000000d41ff4e55f8ebc2e610ec74a0cbdd33e59ebbfeeb1f8a0a0d", - 1688744911, - 0x192946fd, - "000000000000000000000000000000000000000000008798ed692b94a398aa4f", - "3a6ff72336cf78e45b23101f755f4d7dce915b32336a8c242c33905b72b07b35", - 498598646, - Some("ML1900000__70230"), - ), - // Block 2300000 (2025) - recent checkpoint - create_checkpoint( - 2300000, - "00000000000000186f9f2fde843be3d66b8ae317cabb7d43dbde943d02a4b4d7", - "000000000000000d51caa0307836ca3eabe93068a9007515ac128a43d6addd4e", - 1751767455, - 0x1938df46, - "0x00000000000000000000000000000000000000000000aa3859b6456688a3fb53", - "b026649607d72d486480c0cef823dba6b28d0884a0d86f5a8b9e5a7919545cef", - 972444458, - Some("ML2300000__70232"), - ), - ] -} + async fn generate_checkpoints_file(network: Network) -> crate::error::Result<()> { + let storage_path = format!("./.tmp/{network}-checkpoints-generation-storage"); -/// Create testnet checkpoints -pub fn testnet_checkpoints() -> Vec { - vec![ - // Genesis block - create_checkpoint( - 0, - "00000bafbc94add76cb75e2ec92894837288a481e5c005f6563d91623bf8bc2c", - "0000000000000000000000000000000000000000000000000000000000000000", - 1390666206, - 0x1e0ffff0, - "0x0000000000000000000000000000000000000000000000000000000100010001", - "e0028eb9648db56b1ac77cf090b99048a8007e2bb64b68f092c03c7f56a662c7", - 3861367235, - None, - ), - // Height 500000 - create_checkpoint( - 500000, - "000000d0f2239d3ea3d1e39e624f651c5a349b5ca729eec29540aeae0ecc94a7", - "000001d6339e773dea2a9f1eae5e569a04963eb885008be9d553568932885745", - 1621049765, - 0x1e025b1b, - "0x000000000000000000000000000000000000000000000000022f14e45fc51a2e", - "618c77a7c45783f5f20e957a296e077220b50690aae51d714ae164eb8d669fdf", - 10457, - None, - ), - // Height 800000 - create_checkpoint( - 800000, - "00000075cdfa0a552e488406074bb95d831aee16c0ec30114319a587a8a8fb0c", - "0000011921c298768dc2ab0f9ca5a3ff4527813bbd7cd77f45bf93efd0bb0799", - 1671238603, - 0x1e018b19, - "0x00000000000000000000000000000000000000000000000002d68bf1d7e434f6", - "d58300efccbace51cdf5c8a012979e310da21337a7f311b1dcea7c1c894dfb94", - 607529, - None, - ), - // Height 1100000 - create_checkpoint( - 1100000, - "000000078cc3952c7f594de921ae82fcf430a5f3b86755cd72acd819d0001015", - "00000068da3dc19e54cefd3f7e2a7f380bf8d9a0eb1090a7197c3e0b10e2cf1f", - 1725934127, - 0x1e017da4, - "0x000000000000000000000000000000000000000000000000031c3fcb33bc3a48", - "4cc82bf21c5f1e0e712ca1a3d5bde2f92eee2700b86019c6d0ace9c91a8b9bd8", - 251545, - None, - ), - ] -} + let config = ClientConfig::new(network) + .with_storage_path(PathBuf::from(&storage_path)) + .without_filters() + .without_masternodes(); -/// Helper to parse hex block hash strings -fn parse_block_hash(s: &str) -> Result { - use hex::FromHex; - let bytes = Vec::::from_hex(s).map_err(|e| format!("Invalid hex: {}", e))?; - if bytes.len() != 32 { - return Err("Invalid hash length: expected 32 bytes".to_string()); - } - let mut hash_bytes = [0u8; 32]; - hash_bytes.copy_from_slice(&bytes); - // Reverse for little-endian - hash_bytes.reverse(); - Ok(BlockHash::from_byte_array(hash_bytes)) -} + let network_manager = PeerNetworkManager::new(&config).await?; + let storage_manager = DiskStorageManager::new(&storage_path).await?; + let wallet = + Arc::new(RwLock::new(WalletManager::::new(config.network))); + let mut client = + DashSpvClient::new(config, network_manager, storage_manager, wallet).await?; -/// Helper to parse hex block hash strings, returning zero hash on error -fn parse_block_hash_safe(s: &str) -> BlockHash { - parse_block_hash(s).unwrap_or_else(|e| { - tracing::error!("Failed to parse checkpoint block hash '{}': {}", s, e); - BlockHash::from_byte_array([0u8; 32]) - }) -} + client.start().await?; + let (_command_sender, command_receiver) = tokio::sync::mpsc::unbounded_channel(); + let shutdown_token = CancellationToken::new(); -/// Helper to create a checkpoint with common defaults -#[allow(clippy::too_many_arguments)] -fn create_checkpoint( - height: u32, - hash: &str, - prev_hash: &str, - timestamp: u32, - bits: u32, - chain_work: &str, - merkle_root: &str, - nonce: u32, - masternode_list: Option<&str>, -) -> Checkpoint { - Checkpoint { - height, - block_hash: parse_block_hash_safe(hash), - prev_blockhash: parse_block_hash_safe(prev_hash), - timestamp, - target: Target::from_compact(CompactTarget::from_consensus(bits)), - merkle_root: Some(parse_block_hash_safe(merkle_root)), - chain_work: chain_work.to_string(), - masternode_list_name: masternode_list.map(|s| s.to_string()), - protocol_version: masternode_list.and_then(|ml| { - // Extract protocol version from masternode list name - ml.split("__").nth(1).and_then(|s| s.parse().ok()) - }), - nonce, - } -} + let mut progress_receiver = client.take_progress_receiver().unwrap(); -#[cfg(test)] -mod tests { - use super::*; + { + let shutdown_token = shutdown_token.clone(); + tokio::spawn(async move { + client.run(command_receiver, shutdown_token).await.unwrap(); + }); + } - #[test] - fn test_checkpoint_validation() { - let checkpoints = mainnet_checkpoints(); - let manager = CheckpointManager::new(checkpoints); - - // Test genesis block - let genesis_checkpoint = - manager.get_checkpoint(0).expect("Genesis checkpoint should exist"); - assert_eq!(genesis_checkpoint.height, 0); - assert_eq!(genesis_checkpoint.timestamp, 1390095618); - - // Test validation - let genesis_hash = - parse_block_hash("00000ffd590b1485b3caadc19b22e6379c733355108f107a430458cdf3407ab6") - .expect("Failed to parse genesis hash for test"); - assert!(manager.validate_block(0, &genesis_hash)); - - // Test invalid hash - let invalid_hash = BlockHash::from_byte_array([1u8; 32]); - assert!(!manager.validate_block(0, &invalid_hash)); - - // Test no checkpoint at height - assert!(manager.validate_block(1, &invalid_hash)); // No checkpoint at height 1 - } + while let Some(progress) = progress_receiver.recv().await { + if matches!(progress.sync_stage, SyncStage::Complete) { + shutdown_token.cancel(); + } + } - #[test] - fn test_last_checkpoint_before() { - let checkpoints = mainnet_checkpoints(); - let manager = CheckpointManager::new(checkpoints); + let storage_manager = DiskStorageManager::new(&storage_path).await?; - // Test finding checkpoint before various heights - assert_eq!( - manager.last_checkpoint_before_height(0).expect("Should find checkpoint").height, - 0 - ); - assert_eq!( - manager.last_checkpoint_before_height(1000).expect("Should find checkpoint").height, - 0 - ); - assert_eq!( - manager.last_checkpoint_before_height(5000).expect("Should find checkpoint").height, - 4991 - ); - assert_eq!( - manager.last_checkpoint_before_height(200000).expect("Should find checkpoint").height, - 107996 - ); - } + let checkpoints_file_path = match network { + Network::Dash => MAINNET_CHECKPOINTS_FILE, + Network::Testnet => TESTNET_CHECKPOINTS_FILE, + _ => panic!("There is no checkpoints file for network {network}"), + }; - #[test] - fn test_protocol_version_extraction() { - let checkpoint = create_checkpoint( - 1088640, - "0000000000000000000000000000000000000000000000000000000000000000", - "0000000000000000000000000000000000000000000000000000000000000000", - 0, - 0, - "", - "0000000000000000000000000000000000000000000000000000000000000000", - 0, - Some("ML1088640__70218"), - ); + let checkpoints_file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(checkpoints_file_path) + .expect("Should open checkpoints file for writing"); + let mut writer = BufWriter::new(checkpoints_file); - assert_eq!(checkpoint.protocol_version(), Some(70218)); - assert!(checkpoint.has_masternode_list()); - - let checkpoint_no_version = create_checkpoint( - 0, - "0000000000000000000000000000000000000000000000000000000000000000", - "0000000000000000000000000000000000000000000000000000000000000000", - 0, - 0, - "", - "0000000000000000000000000000000000000000000000000000000000000000", - 0, - None, - ); + for height in checkpoints_heights(network) { + let checkpoint_header = + storage_manager.get_header(*height).await?.expect("Should find checkpoint"); - assert_eq!(checkpoint_no_version.protocol_version(), None); - assert!(!checkpoint_no_version.has_masternode_list()); + let checkpoint = Checkpoint::new(*height, checkpoint_header.into()); + checkpoint.consensus_encode(&mut writer).expect("Error writing checkpoint to file"); + } + + writer.flush()?; + Ok(()) + } + } + + #[test] + #[should_panic(expected = "height 0")] + fn test_checkpoint_must_start_at_zero() { + CheckpointManager::dummy(&[1, 4, 5, 9, 90, 9000]); } #[test] - #[ignore] // Test depends on specific mainnet checkpoint data - fn test_fork_rejection() { - let checkpoints = mainnet_checkpoints(); - let manager = CheckpointManager::new(checkpoints); + #[should_panic(expected = "ascending")] + fn test_checkpoints_must_be_ascending() { + CheckpointManager::dummy(&[0, 1, 2, 3, 2, 1]); + } - // Should reject fork at checkpoint height - assert!(manager.should_reject_fork(1500)); - assert!(manager.should_reject_fork(750000)); + #[test] + fn test_last_checkpoint_before() { + let manager = CheckpointManager::dummy(&MAINNET_CHECKPOINTS_HEIGHTS); - // Should not reject fork after last checkpoint - assert!(!manager.should_reject_fork(2000000)); + // Test finding checkpoint before various heights + assert_eq!(manager.last_checkpoint_before_height(0).0, 0); + assert_eq!(manager.last_checkpoint_before_height(1000).0, 0); + assert_eq!(manager.last_checkpoint_before_height(5000).0, 4991); + assert_eq!(manager.last_checkpoint_before_height(200000).0, 107996); } #[test] fn test_checkpoint_by_timestamp() { - let checkpoints = mainnet_checkpoints(); - let manager = CheckpointManager::new(checkpoints); + let manager = CheckpointManager::dummy(&MAINNET_CHECKPOINTS_HEIGHTS); // Test finding checkpoint by timestamp let checkpoint = manager.last_checkpoint_before_timestamp(1500000000); - assert!(checkpoint.is_some()); - assert!(checkpoint.expect("Should find checkpoint by timestamp").timestamp <= 1500000000); + assert!(checkpoint.1.header().time <= 1500000000); } } diff --git a/dash-spv/src/chain/mod.rs b/dash-spv/src/chain/mod.rs index 1c5e2630f..a10dbd407 100644 --- a/dash-spv/src/chain/mod.rs +++ b/dash-spv/src/chain/mod.rs @@ -9,12 +9,11 @@ pub mod chain_tip; pub mod chain_work; pub mod chainlock_manager; -pub mod checkpoints; - -#[cfg(test)] -mod checkpoint_test; +mod checkpoints; pub use chain_tip::{ChainTip, ChainTipManager}; pub use chain_work::ChainWork; pub use chainlock_manager::{ChainLockEntry, ChainLockManager}; -pub use checkpoints::{Checkpoint, CheckpointManager}; +pub use checkpoints::CheckpointManager; + +pub(crate) use checkpoints::Checkpoint; diff --git a/dash-spv/src/client/lifecycle.rs b/dash-spv/src/client/lifecycle.rs index d9223b82e..e099a14e4 100644 --- a/dash-spv/src/client/lifecycle.rs +++ b/dash-spv/src/client/lifecycle.rs @@ -12,7 +12,7 @@ use std::collections::HashSet; use std::sync::Arc; use tokio::sync::{mpsc, Mutex, RwLock}; -use crate::chain::ChainLockManager; +use crate::chain::{ChainLockManager, CheckpointManager}; use crate::error::{Result, SpvError}; use crate::mempool_filter::MempoolFilter; use crate::network::NetworkManager; @@ -20,7 +20,6 @@ use crate::storage::StorageManager; use crate::sync::legacy::SyncManager; use crate::types::{ChainState, MempoolState, SpvStats}; use dashcore::network::constants::NetworkExt; -use dashcore_hashes::Hash; use key_wallet_manager::wallet_interface::WalletInterface; use super::{ClientConfig, DashSpvClient}; @@ -227,101 +226,45 @@ impl DashSpvClient crate::chain::checkpoints::mainnet_checkpoints(), - dashcore::Network::Testnet => crate::chain::checkpoints::testnet_checkpoints(), - _ => vec![], - }; - - // Create checkpoint manager - let checkpoint_manager = crate::chain::checkpoints::CheckpointManager::new(checkpoints); + let checkpoint_manager = CheckpointManager::new(self.network()); // Find the best checkpoint at or before the requested height - if let Some(checkpoint) = checkpoint_manager.last_checkpoint_before_height(start_height) + let (height, checkpoint) = + checkpoint_manager.last_checkpoint_before_height(start_height); + + tracing::info!("🚀 Starting sync from checkpoint"); + + let mut chain_state = self.state.write().await; + chain_state.init_from_checkpoint(height, *checkpoint.header(), self.network()); + + // Clone the chain state for storage + let chain_state_for_storage = (*chain_state).clone(); + drop(chain_state); + + // Update storage with chain state including sync_base_height { - if checkpoint.height > 0 { - tracing::info!( - "🚀 Starting sync from checkpoint at height {} instead of genesis (requested start height: {})", - checkpoint.height, - start_height - ); - - // Initialize chain state with checkpoint - let mut chain_state = self.state.write().await; - - // Build header from checkpoint - use dashcore::{ - block::{Header as BlockHeader, Version}, - pow::CompactTarget, - }; - - let checkpoint_header = BlockHeader { - version: Version::from_consensus(536870912), // Version 0x20000000 is common for modern blocks - prev_blockhash: checkpoint.prev_blockhash, - merkle_root: checkpoint - .merkle_root - .map(|h| dashcore::TxMerkleNode::from_byte_array(*h.as_byte_array())) - .unwrap_or_else(dashcore::TxMerkleNode::all_zeros), - time: checkpoint.timestamp, - bits: CompactTarget::from_consensus( - checkpoint.target.to_compact_lossy().to_consensus(), - ), - nonce: checkpoint.nonce, - }; - - // Verify hash matches - let calculated_hash = checkpoint_header.block_hash(); - if calculated_hash != checkpoint.block_hash { - tracing::warn!( - "Checkpoint header hash mismatch at height {}: expected {}, calculated {}", - checkpoint.height, - checkpoint.block_hash, - calculated_hash - ); - } else { - // Initialize chain state from checkpoint - chain_state.init_from_checkpoint( - checkpoint.height, - checkpoint_header, - self.config.network, - ); - - // Clone the chain state for storage - let chain_state_for_storage = (*chain_state).clone(); - drop(chain_state); - - // Update storage with chain state including sync_base_height - { - let mut storage = self.storage.lock().await; - storage - .store_headers_at_height(&[checkpoint_header], checkpoint.height) - .await?; - storage - .store_chain_state(&chain_state_for_storage) - .await - .map_err(SpvError::Storage)?; - } - - // Don't store the checkpoint header itself - we'll request headers from peers - // starting from this checkpoint - - tracing::info!( - "✅ Initialized from checkpoint at height {}, skipping {} headers", - checkpoint.height, - checkpoint.height - ); - - // Update the sync manager's cached flags from the checkpoint-initialized state - self.sync_manager.update_chain_state_cache(checkpoint.height); - tracing::info!( - "Updated sync manager with checkpoint-initialized chain state" - ); - - return Ok(()); - } - } + let mut storage = self.storage.lock().await; + storage.store_headers_at_height(&[*checkpoint.header()], height).await?; + storage + .store_chain_state(&chain_state_for_storage) + .await + .map_err(SpvError::Storage)?; } + + // Don't store the checkpoint header itself - we'll request headers from peers + // starting from this checkpoint + + tracing::info!( + "✅ Initialized from checkpoint at height {}, skipping {} headers", + height, + height + ); + + // Update the sync manager's cached flags from the checkpoint-initialized state + self.sync_manager.update_chain_state_cache(height); + tracing::info!("Updated sync manager with checkpoint-initialized chain state"); + + return Ok(()); } // Get the genesis block hash for this network diff --git a/dash-spv/src/sync/legacy/headers/manager.rs b/dash-spv/src/sync/legacy/headers/manager.rs index 4498c16d9..e9e111f81 100644 --- a/dash-spv/src/sync/legacy/headers/manager.rs +++ b/dash-spv/src/sync/legacy/headers/manager.rs @@ -6,7 +6,7 @@ use dashcore::{ }; use dashcore_hashes::Hash; -use crate::chain::checkpoints::{mainnet_checkpoints, testnet_checkpoints, CheckpointManager}; +use crate::chain::CheckpointManager; use crate::chain::{ChainTip, ChainTipManager, ChainWork}; use crate::client::ClientConfig; use crate::error::{SyncError, SyncResult}; @@ -48,7 +48,6 @@ pub struct HeaderSyncManager { config: ClientConfig, tip_manager: ChainTipManager, checkpoint_manager: CheckpointManager, - reorg_config: ReorgConfig, chain_state: Arc>, syncing_headers: bool, last_sync_progress: std::time::Instant, @@ -66,18 +65,12 @@ impl HeaderSyncManager { // WalletState removed - wallet functionality is now handled externally // Create checkpoint manager based on network - let checkpoints = match config.network { - dashcore::Network::Dash => mainnet_checkpoints(), - dashcore::Network::Testnet => testnet_checkpoints(), - _ => Vec::new(), // No checkpoints for other networks - }; - let checkpoint_manager = CheckpointManager::new(checkpoints); + let checkpoint_manager = CheckpointManager::new(config.network); Ok(Self { config: config.clone(), tip_manager: ChainTipManager::new(reorg_config.max_forks), checkpoint_manager, - reorg_config, chain_state, syncing_headers: false, last_sync_progress: std::time::Instant::now(), @@ -204,23 +197,7 @@ impl HeaderSyncManager { ); } - // Step 3: Process the Entire Validated Batch - - // Checkpoint Validation: Perform in-memory security check against checkpoints - for (index, cached_header) in cached_headers.iter().enumerate() { - let prospective_height = tip_height + (index as u32) + 1; - - if self.reorg_config.enforce_checkpoints { - // Use cached hash to avoid redundant X11 computation in loop - let header_hash = cached_header.hash(); - if !self.checkpoint_manager.validate_block(prospective_height, header_hash) { - return Err(SyncError::Validation(format!( - "Block at height {} does not match checkpoint", - prospective_height - ))); - } - } - } + // Step 3: Process the Entire Batch storage .store_headers(headers) @@ -599,48 +576,10 @@ impl HeaderSyncManager { // No headers in storage, use checkpoint based on wallet creation time // TODO: Pass wallet creation time from client config - if let Some(checkpoint) = self.checkpoint_manager.get_sync_checkpoint(None) { - // Return checkpoint as starting point - // Note: We'll need to prepopulate headers from checkpoints for this to work properly - return Some((checkpoint.height, checkpoint.block_hash)); - } - - // No suitable checkpoint, start from genesis - None - } - - /// Check if we can skip ahead to a checkpoint during sync - pub fn can_skip_to_checkpoint( - &self, - current_height: u32, - peer_height: u32, - ) -> Option<(u32, BlockHash)> { - // Don't skip if we're already close to the peer's tip - if peer_height.saturating_sub(current_height) < 1000 { - return None; - } - - // Find next checkpoint after current height - let checkpoint_heights = self.checkpoint_manager.checkpoint_heights(); - - for height in checkpoint_heights { - // Skip if checkpoint is: - // 1. After our current position - // 2. Before or at peer's height (peer has it) - // 3. Far enough ahead to be worth skipping (at least 500 blocks) - if *height > current_height && *height <= peer_height && *height > current_height + 500 - { - if let Some(checkpoint) = self.checkpoint_manager.get_checkpoint(*height) { - tracing::info!( - "Can skip from height {} to checkpoint at height {}", - current_height, - checkpoint.height - ); - return Some((checkpoint.height, checkpoint.block_hash)); - } - } - } - None + let (height, checkpoint) = self.checkpoint_manager.last_checkpoint_before_timestamp(0); + // Return checkpoint as starting point + // Note: We'll need to prepopulate headers from checkpoints for this to work properly + Some((height, *checkpoint.hash())) } /// Check if header sync is currently in progress diff --git a/dash-spv/src/test_utils/checkpoint.rs b/dash-spv/src/test_utils/checkpoint.rs index c9d11b433..53a2eb4bc 100644 --- a/dash-spv/src/test_utils/checkpoint.rs +++ b/dash-spv/src/test_utils/checkpoint.rs @@ -1,26 +1,20 @@ -use dashcore::{CompactTarget, Header, Target}; +use dashcore::Header; -use crate::chain::Checkpoint; +use crate::chain::{Checkpoint, CheckpointManager}; impl Checkpoint { pub fn dummy(height: u32) -> Checkpoint { - let block_header = Header::dummy(height); + let header = Header::dummy(height); - Checkpoint { - height, - block_hash: block_header.block_hash(), - prev_blockhash: block_header.prev_blockhash, - timestamp: block_header.time, - target: Target::from_compact(CompactTarget::from_consensus(0x1d00ffff)), - merkle_root: Some(block_header.block_hash()), - chain_work: format!("0x{:064x}", height.wrapping_mul(1000)), - masternode_list_name: if height.is_multiple_of(100000) && height > 0 { - Some(format!("ML{}__70230", height)) - } else { - None - }, - protocol_version: None, - nonce: block_header.nonce, - } + Checkpoint::new(height, header.into()) + } +} + +impl CheckpointManager { + pub fn dummy(heights: &[u32]) -> CheckpointManager { + let checkpoints = + heights.iter().map(|height| Checkpoint::dummy(*height)).collect::>(); + + CheckpointManager::new_with_checkpoints(checkpoints) } }