diff --git a/dash-spv/src/client/lifecycle.rs b/dash-spv/src/client/lifecycle.rs index 2f789bdf8..4ce3d0c47 100644 --- a/dash-spv/src/client/lifecycle.rs +++ b/dash-spv/src/client/lifecycle.rs @@ -49,11 +49,15 @@ impl DashSpvClient Some(height), None => { let birth_height = wallet.read().await.earliest_required_height().await; - (birth_height > 0).then_some(birth_height) + let start = birth_height.max(config.network.hd_wallet_sync_floor()); + (start > 0).then_some(start) } }; diff --git a/dash-spv/src/client/mod.rs b/dash-spv/src/client/mod.rs index 541a71794..6a7c8bda5 100644 --- a/dash-spv/src/client/mod.rs +++ b/dash-spv/src/client/mod.rs @@ -92,20 +92,26 @@ mod tests { #[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. + // A wallet birth height below mainnet's HD/BIP39 activation floor is clamped up to + // the floor, which anchors at the nearest checkpoint at or before it: the 200_000 + // checkpoint. 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. + // 120_000 is below mainnet's HD/BIP39 sync floor, so it is clamped up. let (height, hash) = anchored_tip(120_000, None).await; - assert_eq!(height, 100_000); + assert_eq!(height, 200_000); let expected: dashcore::BlockHash = - "00000000000fd08c2fb661d2fcb0d49abb3a91e5f27082ce64feed3b4dede2e2".parse().unwrap(); + "000000000004d0615ff622ec78457ca211dc63fc9c62cca9d9d9af7206be721b".parse().unwrap(); assert_eq!(hash, Some(expected)); - // Birth height 0 keeps syncing from genesis. - assert_eq!(anchored_tip(0, None).await.0, 0); + // Birth height 0 no longer drags mainnet sync to genesis; it floors at HD activation. + assert_eq!(anchored_tip(0, None).await.0, 200_000); - // Explicit `start_from_height` wins over the wallet birth height, even when - // the birth height would resolve to a higher checkpoint. + // A birth height above the floor is unaffected and anchors at the nearest + // checkpoint at or below it. + assert_eq!(anchored_tip(560_000, None).await.0, 550_000); + + // Explicit `start_from_height` wins over the wallet birth height and is honored + // even below the floor. assert_eq!(anchored_tip(120_000, Some(60_000)).await.0, 50_000); } diff --git a/dash/src/network/constants.rs b/dash/src/network/constants.rs index 6fc4268bc..57c8cddec 100644 --- a/dash/src/network/constants.rs +++ b/dash/src/network/constants.rs @@ -50,9 +50,22 @@ pub const NODE_HEADERS_COMPRESSED: ServiceFlags = ServiceFlags::NODE_HEADERS_COM /// 60001 - Support `pong` message and nonce in `ping` message pub const PROTOCOL_VERSION: u32 = 70237; +/// Mainnet height from which Dash HD/BIP39 wallets could first exist on chain. +/// +/// No address any HD wallet derives can appear earlier, so an SPV client scanning for HD +/// wallet activity has no reason to sync mainnet below this height. Matches the entry in +/// DashSync's `mainnet_checkpoint_array` commented "first sync time (aka BIP39 creation +/// time)", the value the official Dash mobile SPV client uses as its floor. +pub const MAINNET_HD_ACTIVATION_HEIGHT: u32 = 227121; + pub trait NetworkExt { /// Returns the known genesis block hash for `network`, if one is hardcoded. fn known_genesis_block_hash(&self) -> Option; + + /// Earliest block height worth syncing for HD/BIP39 wallets, or `0` when the network + /// has no floor (sync from genesis). Mainnet is floored at + /// [`MAINNET_HD_ACTIVATION_HEIGHT`]; other networks return `0`. + fn hd_wallet_sync_floor(&self) -> u32; } impl NetworkExt for Network { @@ -88,6 +101,13 @@ impl NetworkExt for Network { } } } + + fn hd_wallet_sync_floor(&self) -> u32 { + match self { + Network::Mainnet => MAINNET_HD_ACTIVATION_HEIGHT, + _ => 0, + } + } } /// Flags to indicate which network services a node supports. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -265,10 +285,19 @@ impl Decodable for ServiceFlags { #[cfg(test)] mod tests { - use super::ServiceFlags; + use super::{MAINNET_HD_ACTIVATION_HEIGHT, NetworkExt, ServiceFlags}; use crate::Network; use crate::consensus::encode::{deserialize, serialize}; + #[test] + fn test_hd_wallet_sync_floor() { + // Only mainnet is floored; every other network syncs from genesis (0). + assert_eq!(Network::Mainnet.hd_wallet_sync_floor(), MAINNET_HD_ACTIVATION_HEIGHT); + assert_eq!(Network::Testnet.hd_wallet_sync_floor(), 0); + assert_eq!(Network::Devnet.hd_wallet_sync_floor(), 0); + assert_eq!(Network::Regtest.hd_wallet_sync_floor(), 0); + } + #[test] fn test_network_magic() { assert_eq!(Network::Mainnet.magic(), 0xBD6B0CBF);