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
6 changes: 5 additions & 1 deletion dash-spv/src/client/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,15 @@ impl<W: WalletInterface, N: NetworkManager, S: StorageManager> DashSpvClient<W,
// 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.
// The wallet-derived height is floored at the network minimum: no HD/BIP39 wallet
// can predate mainnet's activation height, so a low or zero birth height must never
// drag mainnet sync below it.
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)
let start = birth_height.max(config.network.hd_wallet_sync_floor());
(start > 0).then_some(start)
}
};

Expand Down
24 changes: 15 additions & 9 deletions dash-spv/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
31 changes: 30 additions & 1 deletion dash/src/network/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<BlockHash>;

/// 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 {
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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);
Expand Down
Loading