Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 13 additions & 1 deletion dash-spv/src/chain/checkpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Checkpoint>) -> Self {
let mut checkpoint_map = HashMap::new();
Expand Down
98 changes: 45 additions & 53 deletions dash-spv/src/client/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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;
Expand All @@ -42,9 +46,20 @@ impl<W: WalletInterface, N: NetworkManager, S: StorageManager> DashSpvClient<W,
config.validate().map_err(SpvError::Config)?;
config.apply_global_overrides().map_err(SpvError::Config)?;

// Resolve where to anchor the chain. An explicit `start_from_height` always
// wins. Otherwise fall back to the wallet birth height so we don't sync headers
// and filter headers from genesis when the wallet only cares about recent blocks.
let start_from_height = match config.start_from_height {
Some(height) => 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 {
Expand All @@ -65,12 +80,7 @@ impl<W: WalletInterface, N: NetworkManager, S: StorageManager> DashSpvClient<W,
W,
> = 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(),
Expand Down Expand Up @@ -228,7 +238,11 @@ impl<W: WalletInterface, N: NetworkManager, S: StorageManager> DashSpvClient<W,
/// Initialize genesis block or checkpoint in storage.
///
/// Called before creating managers so they can read the tip during construction.
async fn initialize_genesis_block(config: &ClientConfig, storage: &mut S) -> Result<()> {
async fn initialize_genesis_block(
config: &ClientConfig,
start_from_height: Option<u32>,
storage: &mut S,
) -> Result<()> {
// Check if we already have any headers in storage
let current_tip = storage.get_tip_height().await;

Expand All @@ -239,16 +253,8 @@ impl<W: WalletInterface, N: NetworkManager, S: StorageManager> DashSpvClient<W,
}

// Check if we should use a checkpoint instead of genesis
if let Some(start_height) = config.start_from_height {
// Get checkpoints for this network
let checkpoints = match config.network {
dashcore::Network::Mainnet => 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)
Expand All @@ -260,51 +266,37 @@ impl<W: WalletInterface, N: NetworkManager, S: StorageManager> DashSpvClient<W,
start_height
);

// Build header from checkpoint
use dashcore::{
block::{Header as BlockHeader, Version},
pow::CompactTarget,
};

// The checkpoint stores the trusted block hash but not the block version,
// so a reconstructed header cannot be hashed back to that value. Anchor on
// the trusted hash directly: chain linkage compares against the stored hash,
// and `time`/`bits` (used for difficulty checks of later headers) come from
// the checkpoint. The version is irrelevant since the hash is never recomputed.
let checkpoint_header = BlockHeader {
version: Version::from_consensus(536870912), // Version 0x20000000 is common for modern blocks
version: Version::from_consensus(0),
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),
.map(|h| TxMerkleNode::from_byte_array(*h.as_byte_array()))
.unwrap_or_else(TxMerkleNode::all_zeros),
time: checkpoint.timestamp,
bits: CompactTarget::from_consensus(
checkpoint.target.to_compact_lossy().to_consensus(),
),
nonce: checkpoint.nonce,
};
let anchor = HashedBlockHeader::with_trusted_hash(
checkpoint_header,
checkpoint.block_hash,
);
storage.store_headers_at_height(&[anchor], checkpoint.height).await?;

tracing::info!(
"✅ Initialized from checkpoint at height {}, skipping {} headers",
checkpoint.height,
checkpoint.height
);

// 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 {
storage
.store_headers_at_height(
&[crate::types::HashedBlockHeader::from(checkpoint_header)],
checkpoint.height,
)
.await?;

tracing::info!(
"✅ Initialized from checkpoint at height {}, skipping {} headers",
checkpoint.height,
checkpoint.height
);

return Ok(());
}
return Ok(());
}
}
}
Expand Down
61 changes: 61 additions & 0 deletions dash-spv/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,73 @@ mod tests {
use crate::client::config::MempoolStrategy;
use crate::storage::DiskStorageManager;
use crate::test_utils::MockNetworkManager;
use crate::Network;
use key_wallet::wallet::initialization::WalletAccountCreationOptions;
use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo;
use key_wallet_manager::WalletManager;
use std::sync::Arc;
use tempfile::TempDir;
use tokio::sync::RwLock;

const TEST_MNEMONIC: &str =
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";

/// Construct a mainnet client with the given wallet birth height and optional
/// explicit `start_from_height`, then return the height and hash its chain is
/// anchored at.
async fn anchored_tip(
birth_height: u32,
start_from_height: Option<u32>,
) -> (u32, Option<dashcore::BlockHash>) {
let mut wallet_manager = WalletManager::<ManagedWalletInfo>::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()
Expand Down
12 changes: 12 additions & 0 deletions dash-spv/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading