From 202b0a1406d07cac5ae8b0722576482c80473589 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Mon, 13 Jul 2026 20:24:29 +1000 Subject: [PATCH] feat(dash-spv): floor mainnet sync at HD/BIP39 activation height Floor mainnet sync at block 227121, the height from which HD/BIP39 wallets could first exist on chain. No address any HD wallet derives can appear before it, so syncing the pre-HD-wallet chain is wasted work. This 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. Add `MAINNET_HD_ACTIVATION_HEIGHT` and a `NetworkExt::hd_wallet_sync_floor()` method to the `dash` crate, alongside `known_genesis_block_hash` where the other per-network chain constants live (mainnet returns the floor, other networks return `0`, meaning sync from genesis). When the caller sets no explicit `start_from_height`, `DashSpvClient::new` floors the wallet-derived birth height via `birth_height.max(config.network.hd_wallet_sync_floor())`, so a low or zero birth height can no longer drag mainnet sync back to genesis. Anchoring reuses the existing checkpoint machinery and snaps to the nearest checkpoint at or before the floor (the 200000 checkpoint), a safe undershoot that never skips a block that could hold an HD wallet transaction. An explicit `start_from_height` is still honored downward, and testnet/regtest/devnet are unaffected. --- dash-spv/src/client/lifecycle.rs | 6 +++++- dash-spv/src/client/mod.rs | 24 +++++++++++++++--------- dash/src/network/constants.rs | 31 ++++++++++++++++++++++++++++++- 3 files changed, 50 insertions(+), 11 deletions(-) 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);