diff --git a/contrib/fetch_checkpoints.sh b/contrib/fetch_checkpoints.sh new file mode 100755 index 000000000..d7c4ca8df --- /dev/null +++ b/contrib/fetch_checkpoints.sh @@ -0,0 +1,200 @@ +#!/bin/sh +# Fetch checkpoint data from Dash network +# +# This script fetches block information at specific heights to create checkpoints +# for the rust-dashcore SPV client. +# +# Usage: +# # Fetch specific heights using block explorer +# ./fetch_checkpoints.sh mainnet 2400000 2450000 +# +# # Fetch using RPC (requires running Dash Core node) +# RPC_URL=http://localhost:9998 RPC_USER=user RPC_PASS=pass ./fetch_checkpoints.sh mainnet 2400000 + +set -e + +NETWORK=${1:-mainnet} +shift || true + +# Configuration +if [ "$NETWORK" = "mainnet" ]; then + EXPLORER_URL="https://insight.dash.org/insight-api" +elif [ "$NETWORK" = "testnet" ]; then + EXPLORER_URL="https://testnet-insight.dashevo.org/insight-api" +else + echo "Error: Invalid network. Use 'mainnet' or 'testnet'" + exit 1 +fi + +# Check for required tools +if ! command -v curl >/dev/null 2>&1; then + echo "Error: curl is required but not installed" + exit 1 +fi + +if ! command -v jq >/dev/null 2>&1; then + echo "Error: jq is required but not installed. Install with: sudo apt install jq" + exit 1 +fi + +# Function to get current blockchain height +get_current_height() { + if [ -n "$RPC_URL" ] && [ -n "$RPC_USER" ] && [ -n "$RPC_PASS" ]; then + # Use RPC + curl -s -u "$RPC_USER:$RPC_PASS" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":"checkpoint","method":"getblockchaininfo","params":[]}' \ + "$RPC_URL" | jq -r '.result.blocks' + else + # Use explorer + curl -s "$EXPLORER_URL/status?q=getInfo" | jq -r '.info.blocks' + fi +} + +# Function to get block by height using explorer +get_block_explorer() { + height=$1 + + # Get block hash at height + block_hash=$(curl -s "$EXPLORER_URL/block-index/$height" | jq -r '.blockHash') + + if [ -z "$block_hash" ] || [ "$block_hash" = "null" ]; then + echo "Error: Failed to get block hash for height $height" >&2 + return 1 + fi + + # Get full block data + curl -s "$EXPLORER_URL/block/$block_hash" +} + +# Function to get block by height using RPC +get_block_rpc() { + height=$1 + + # Get block hash + block_hash=$(curl -s -u "$RPC_USER:$RPC_PASS" \ + -H "Content-Type: application/json" \ + -d "{\"jsonrpc\":\"2.0\",\"id\":\"checkpoint\",\"method\":\"getblockhash\",\"params\":[$height]}" \ + "$RPC_URL" | jq -r '.result') + + if [ -z "$block_hash" ] || [ "$block_hash" = "null" ]; then + echo "Error: Failed to get block hash for height $height" >&2 + return 1 + fi + + # Get block data + curl -s -u "$RPC_USER:$RPC_PASS" \ + -H "Content-Type: application/json" \ + -d "{\"jsonrpc\":\"2.0\",\"id\":\"checkpoint\",\"method\":\"getblock\",\"params\":[\"$block_hash\",2]}" \ + "$RPC_URL" | jq -r '.result' +} + +# Function to format checkpoint as Rust code +format_checkpoint() { + block_json=$1 + + height=$(echo "$block_json" | jq -r '.height') + hash=$(echo "$block_json" | jq -r '.hash') + prev_hash=$(echo "$block_json" | jq -r '.previousblockhash // "0000000000000000000000000000000000000000000000000000000000000000"') + timestamp=$(echo "$block_json" | jq -r '.time') + bits=$(echo "$block_json" | jq -r '.bits') + merkle_root=$(echo "$block_json" | jq -r '.merkleroot') + nonce=$(echo "$block_json" | jq -r '.nonce') + chain_work=$(echo "$block_json" | jq -r '.chainwork // "0x0000000000000000000000000000000000000000000000000000000000000000"') + + # Ensure chainwork has 0x prefix + if ! echo "$chain_work" | grep -q "^0x"; then + chain_work="0x$chain_work" + fi + + # Convert bits to hex format + if echo "$bits" | grep -q "^[0-9a-fA-F]\+$"; then + bits_hex="0x$bits" + else + bits_hex="$bits" + fi + + # Format as Rust code + cat <")) + ), +EOF +} + +# Main script +echo "Fetching checkpoints for $NETWORK..." >&2 +echo "" >&2 + +# Get current height +current_height=$(get_current_height) +if [ -z "$current_height" ]; then + echo "Error: Failed to get current blockchain height" >&2 + exit 1 +fi +echo "Current blockchain height: $current_height" >&2 +echo "" >&2 + +# If no heights specified, show usage +if [ $# -eq 0 ]; then + echo "Usage: $0 [height2] [height3] ..." >&2 + echo "" >&2 + echo "Current height: $current_height" >&2 + echo "Suggested checkpoint heights:" >&2 + if [ "$NETWORK" = "mainnet" ]; then + echo " 2400000 - Block 2.4M" >&2 + echo " 2450000 - Block 2.45M" >&2 + echo " 2500000 - Block 2.5M (if available)" >&2 + else + echo " 1200000 - Block 1.2M" >&2 + echo " 1300000 - Block 1.3M" >&2 + fi + echo "" >&2 + echo "Examples:" >&2 + echo " $0 mainnet 2400000" >&2 + echo " RPC_URL=http://localhost:9998 RPC_USER=user RPC_PASS=pass $0 mainnet 2400000" >&2 + exit 1 +fi + +# Output header +echo "// Generated checkpoints for $NETWORK" +echo "// Add these to dash-spv/src/chain/checkpoints.rs" +echo "" + +# Fetch each requested height +for height in "$@"; do + if [ "$height" -gt "$current_height" ]; then + echo "// Skipping height $height (exceeds current height $current_height)" + continue + fi + + echo "Fetching block at height $height..." >&2 + + # Get block data + if [ -n "$RPC_URL" ] && [ -n "$RPC_USER" ] && [ -n "$RPC_PASS" ]; then + block_data=$(get_block_rpc "$height") + else + block_data=$(get_block_explorer "$height") + fi + + if [ $? -ne 0 ] || [ -z "$block_data" ]; then + echo "// Error: Failed to fetch block at height $height" + continue + fi + + # Format and output + format_checkpoint "$block_data" +done + +echo "" +echo "// NOTE: Update masternode_list_name values if the blocks have DML (Deterministic Masternode List)" +echo "// Check coinbase transaction for masternode list information" diff --git a/dash-spv/src/chain/checkpoints.rs b/dash-spv/src/chain/checkpoints.rs index 32d12c946..b143ede79 100644 --- a/dash-spv/src/chain/checkpoints.rs +++ b/dash-spv/src/chain/checkpoints.rs @@ -344,6 +344,18 @@ pub fn mainnet_checkpoints() -> Vec { 972444458, Some("ML2300000__70232"), ), + // Block 2350000 (2025) - additional recent checkpoint + create_checkpoint( + 2350000, + "00000000000000258216a62e8c7170be1207335474ddfa667092a71e3d4162d2", + "000000000000002702e07f9f3402026e4cd5707961ce54af22c873331a329708", + 1759648416, + 0x192f2bfb, + "0x00000000000000000000000000000000000000000000ada31a5dd056c969c842", + "3e77d2ed0c24aab79096d700cdc4fe3ea9502fcec38ee114c81c9063842a6725", + 3635235496, + Some("ML2350000__70232"), + ), ] } diff --git a/dash-spv/src/client/lifecycle.rs b/dash-spv/src/client/lifecycle.rs index c7c0ae43a..aa6708128 100644 --- a/dash-spv/src/client/lifecycle.rs +++ b/dash-spv/src/client/lifecycle.rs @@ -316,104 +316,110 @@ impl< return Ok(()); } - // Check if we should use a checkpoint instead of genesis - if let Some(start_height) = self.config.start_from_height { - // Get checkpoints for this network - let checkpoints = match self.config.network { - dashcore::Network::Dash => crate::chain::checkpoints::mainnet_checkpoints(), - dashcore::Network::Testnet => crate::chain::checkpoints::testnet_checkpoints(), - _ => vec![], - }; + // ALWAYS try to use checkpoints for faster sync (issue #165) + // Get checkpoints for this network + let checkpoints = match self.config.network { + dashcore::Network::Dash => 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); + // Create checkpoint manager + let checkpoint_manager = crate::chain::checkpoints::CheckpointManager::new(checkpoints); - // Find the best checkpoint at or before the requested height - if let Some(checkpoint) = - checkpoint_manager.best_checkpoint_at_or_before_height(start_height) - { - if checkpoint.height > 0 { - tracing::info!( - "🚀 Starting sync from checkpoint at height {} instead of genesis (requested start height: {})", + // Determine which checkpoint to use + let checkpoint_to_use = if let Some(start_height) = self.config.start_from_height { + // If user specified a start height, find the best checkpoint at or before it + checkpoint_manager.best_checkpoint_at_or_before_height(start_height) + } else { + // Otherwise, use the last (most recent) checkpoint + checkpoint_manager.last_checkpoint() + }; + + // Try to initialize from checkpoint + if let Some(checkpoint) = checkpoint_to_use { + if checkpoint.height > 0 { + tracing::info!( + "🚀 Starting sync from checkpoint at height {} instead of genesis{}", + checkpoint.height, + self.config + .start_from_height + .map(|h| format!(" (requested start height: {})", h)) + .unwrap_or_default() + ); + + // 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, - start_height + checkpoint.block_hash, + calculated_hash + ); + } else { + // Initialize chain state from checkpoint + chain_state.init_from_checkpoint( + checkpoint.height, + checkpoint_header, + self.config.network, ); - // 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(); - 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_chain_state(&chain_state_for_storage) - .await - .map_err(SpvError::Storage)?; - } + // 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_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 + // 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 - ); + 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( - true, - checkpoint.height, - headers_len, - ); - tracing::info!( - "Updated sync manager with checkpoint-initialized chain state" - ); + // Update the sync manager's cached flags from the checkpoint-initialized state + self.sync_manager.update_chain_state_cache( + true, + checkpoint.height, + headers_len, + ); + tracing::info!("Updated sync manager with checkpoint-initialized chain state"); - return Ok(()); - } + return Ok(()); } } } diff --git a/dash-spv/src/sync/headers_with_reorg.rs b/dash-spv/src/sync/headers_with_reorg.rs index 26dc1dd8a..9f5dab49c 100644 --- a/dash-spv/src/sync/headers_with_reorg.rs +++ b/dash-spv/src/sync/headers_with_reorg.rs @@ -314,6 +314,18 @@ impl { - // m/9'/coin_type'/account' + // m/9'/coin_type'/4'/account' (DIP9 Feature 4 = CoinJoin) Ok(DerivationPath::from(vec![ ChildNumber::from_hardened_idx(9).map_err(crate::error::Error::Bip32)?, ChildNumber::from_hardened_idx(coin_type) .map_err(crate::error::Error::Bip32)?, + ChildNumber::from_hardened_idx(4).map_err(crate::error::Error::Bip32)?, // Feature index for CoinJoin ChildNumber::from_hardened_idx(*index).map_err(crate::error::Error::Bip32)?, ])) } diff --git a/key-wallet/src/tests/account_tests.rs b/key-wallet/src/tests/account_tests.rs index baa754996..6aa4b103b 100644 --- a/key-wallet/src/tests/account_tests.rs +++ b/key-wallet/src/tests/account_tests.rs @@ -129,8 +129,8 @@ fn test_coinjoin_account_creation() { _ => panic!("Expected CoinJoin account type"), } - // Verify derivation path for CoinJoin: m/9'/1'/index' (testnet coin type) - assert_eq!(derivation_path.to_string(), format!("m/9'/1'/{}'", index)); + // Verify derivation path for CoinJoin: m/9'/1'/4'/index' (testnet coin type, feature 4 = CoinJoin) + assert_eq!(derivation_path.to_string(), format!("m/9'/1'/4'/{}'", index)); } } @@ -501,7 +501,7 @@ fn test_account_derivation_path_uniqueness() { AccountType::CoinJoin { index: 0, }, - "m/9'/1'/0'".to_string(), + "m/9'/1'/4'/0'".to_string(), // DIP9 Feature 4 = CoinJoin ), (AccountType::IdentityRegistration, "m/9'/1'/5'/1'".to_string()), (