From efef0e95f14efe8839488fc2fba3ceb3c4fbb53d Mon Sep 17 00:00:00 2001 From: xdustinface Date: Thu, 18 Jun 2026 00:36:24 +1000 Subject: [PATCH] feat(dash-spv): anchor sync at wallet birth height checkpoint When no explicit `start_from_height` is configured, `DashSpvClient::new` now derives the sync anchor from the wallet's `earliest_required_height` and starts from the nearest checkpoint at or before it. Previously the wallet birth height only short-circuited the compact-filter download, while block headers and filter headers always synced from genesis. Explicit `start_from_height` still takes precedence. This also fixes the checkpoint anchoring itself, which never worked. `initialize_genesis_block` rebuilt the checkpoint header with a hardcoded version `0x20000000` and verified the recomputed hash against the stored `block_hash`. Old blocks use version `2`, so the hash never matched, the code logged a warning and silently fell back to a full genesis sync, defeating `start_from_height` entirely. The `Checkpoint` does not store the block version, so the exact header bytes cannot be reconstructed. Since the checkpoint `block_hash` is a trusted constant and chain linkage only ever compares against the stored hash (never a recomputed one), we now store the anchor via the new `HashedBlockHeader::with_trusted_hash`, pairing the reconstructed header with the trusted hash. `time` and `bits` still come from the checkpoint for difficulty checks of later headers. --- dash-spv/src/chain/checkpoints.rs | 14 ++++- dash-spv/src/client/lifecycle.rs | 98 ++++++++++++++----------------- dash-spv/src/client/mod.rs | 61 +++++++++++++++++++ dash-spv/src/types.rs | 12 ++++ 4 files changed, 131 insertions(+), 54 deletions(-) diff --git a/dash-spv/src/chain/checkpoints.rs b/dash-spv/src/chain/checkpoints.rs index f78621b54..6bb53ba5c 100644 --- a/dash-spv/src/chain/checkpoints.rs +++ b/dash-spv/src/chain/checkpoints.rs @@ -6,7 +6,7 @@ //! - Protect against deep reorganizations //! - Bootstrap masternode lists at specific heights -use dashcore::{BlockHash, CompactTarget, Target}; +use dashcore::{BlockHash, CompactTarget, Network, Target}; use dashcore_hashes::{hex, Hash}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -66,6 +66,18 @@ pub struct CheckpointManager { } impl CheckpointManager { + /// Create a checkpoint manager seeded with the hardcoded checkpoints for `network`. + /// + /// Networks without bundled checkpoints (devnet, regtest) yield an empty manager. + pub fn for_network(network: Network) -> Self { + let checkpoints = match network { + Network::Mainnet => mainnet_checkpoints(), + Network::Testnet => testnet_checkpoints(), + _ => vec![], + }; + Self::new(checkpoints) + } + /// Create a new checkpoint manager from a list of checkpoints pub fn new(checkpoints: Vec) -> Self { let mut checkpoint_map = HashMap::new(); diff --git a/dash-spv/src/client/lifecycle.rs b/dash-spv/src/client/lifecycle.rs index d844ceec7..2f789bdf8 100644 --- a/dash-spv/src/client/lifecycle.rs +++ b/dash-spv/src/client/lifecycle.rs @@ -9,7 +9,7 @@ //! - Wallet data loading use super::{ClientConfig, DashSpvClient, EventHandler}; -use crate::chain::checkpoints::{mainnet_checkpoints, testnet_checkpoints, CheckpointManager}; +use crate::chain::checkpoints::CheckpointManager; use crate::error::{Result, SpvError}; use crate::network::NetworkManager; use crate::storage::{ @@ -20,8 +20,12 @@ use crate::sync::{ BlockHeadersManager, BlocksManager, ChainLockManager, FilterHeadersManager, FiltersManager, InstantSendManager, Managers, MasternodesManager, MempoolManager, SyncCoordinator, }; +use crate::types::HashedBlockHeader; +use dashcore::block::{Header as BlockHeader, Version}; use dashcore::network::constants::NetworkExt; +use dashcore::pow::CompactTarget; use dashcore::sml::masternode_list_engine::MasternodeListEngine; +use dashcore::TxMerkleNode; use dashcore_hashes::Hash; use key_wallet_manager::WalletInterface; use std::sync::Arc; @@ -42,9 +46,20 @@ impl DashSpvClient Some(height), + None => { + let birth_height = wallet.read().await.earliest_required_height().await; + (birth_height > 0).then_some(birth_height) + } + }; + // Initialize genesis block or checkpoint before creating managers, // so they can read the tip from storage during construction. - Self::initialize_genesis_block(&config, &mut storage).await?; + Self::initialize_genesis_block(&config, start_from_height, &mut storage).await?; let masternode_engine = { if config.enable_masternodes { @@ -65,12 +80,7 @@ impl DashSpvClient = Managers::default(); - let checkpoints = match config.network { - dashcore::Network::Mainnet => mainnet_checkpoints(), - dashcore::Network::Testnet => testnet_checkpoints(), - _ => Vec::new(), - }; - let checkpoint_manager = Arc::new(CheckpointManager::new(checkpoints)); + let checkpoint_manager = Arc::new(CheckpointManager::for_network(config.network)); managers.block_headers = Some( BlockHeadersManager::new( storage.block_headers(), @@ -228,7 +238,11 @@ impl DashSpvClient Result<()> { + async fn initialize_genesis_block( + config: &ClientConfig, + start_from_height: Option, + storage: &mut S, + ) -> Result<()> { // Check if we already have any headers in storage let current_tip = storage.get_tip_height().await; @@ -239,16 +253,8 @@ 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); + if let Some(start_height) = start_from_height { + let checkpoint_manager = CheckpointManager::for_network(config.network); // Find the best checkpoint at or before the requested height if let Some(checkpoint) = checkpoint_manager.last_checkpoint_before_height(start_height) @@ -260,51 +266,37 @@ impl DashSpvClient, + ) -> (u32, Option) { + let mut wallet_manager = WalletManager::::new(Network::Mainnet); + wallet_manager + .create_wallet_from_mnemonic( + TEST_MNEMONIC, + birth_height, + WalletAccountCreationOptions::Default, + ) + .expect("wallet creation must succeed"); + let wallet = Arc::new(RwLock::new(wallet_manager)); + + let temp_dir = TempDir::new().unwrap(); + let mut config = ClientConfig::mainnet() + .without_filters() + .without_masternodes() + .with_storage_path(temp_dir.path()); + config.start_from_height = start_from_height; + + let storage = DiskStorageManager::new(&config).await.expect("Failed to create storage"); + let client = DashSpvClient::new( + config, + MockNetworkManager::new(), + storage, + wallet, + vec![Arc::new(())], + ) + .await + .expect("client construction must succeed"); + (client.tip_height().await, client.tip_hash().await) + } + + #[tokio::test] + async fn birth_height_anchors_chain_to_nearest_checkpoint() { + // A wallet born at 120_000 anchors at the 100_000 checkpoint, not genesis, and + // the stored tip carries the trusted checkpoint hash (what the next header's + // `prev_blockhash` is validated against), not a hash recomputed from a header. + let (height, hash) = anchored_tip(120_000, None).await; + assert_eq!(height, 100_000); + let expected: dashcore::BlockHash = + "00000000000fd08c2fb661d2fcb0d49abb3a91e5f27082ce64feed3b4dede2e2".parse().unwrap(); + assert_eq!(hash, Some(expected)); + + // Birth height 0 keeps syncing from genesis. + assert_eq!(anchored_tip(0, None).await.0, 0); + + // Explicit `start_from_height` wins over the wallet birth height, even when + // the birth height would resolve to a higher checkpoint. + assert_eq!(anchored_tip(120_000, Some(60_000)).await.0, 50_000); + } + #[tokio::test] async fn client_exposes_shared_wallet_manager() { let config = ClientConfig::mainnet() diff --git a/dash-spv/src/types.rs b/dash-spv/src/types.rs index dc21b4732..ff14a9449 100644 --- a/dash-spv/src/types.rs +++ b/dash-spv/src/types.rs @@ -27,6 +27,18 @@ pub struct HashedBlockHeader { } impl HashedBlockHeader { + /// Pair a header with a known, trusted hash without recomputing it from the header. + /// + /// The caller guarantees the hash is correct. This is used to anchor the chain at a + /// checkpoint whose hash is trusted but whose exact header bytes (the block version) + /// are not stored, so hashing the reconstructed header would not reproduce it. + pub(crate) fn with_trusted_hash(header: BlockHeader, hash: BlockHash) -> Self { + Self { + header, + hash, + } + } + pub fn header(&self) -> &BlockHeader { &self.header }