From 9b1e709fd8e0bfab0dd69f7c86fd6b7d2e585fbe Mon Sep 17 00:00:00 2001 From: xdustinface Date: Mon, 6 Jul 2026 18:54:06 +1000 Subject: [PATCH] feat(dash-spv): add `--birth-height` CLI flag Let the SPV CLI set the wallet birth height so header, filter-header and filter sync anchor at the nearest checkpoint at or before it instead of syncing the whole chain from genesis. `now`/`latest` resolves to the highest bundled checkpoint, the right anchor for a brand-new wallet with no prior history. An unspecified value keeps the previous full-scan default of genesis. --- dash-spv/src/main.rs | 64 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/dash-spv/src/main.rs b/dash-spv/src/main.rs index eca7d7a6a..572d1813c 100644 --- a/dash-spv/src/main.rs +++ b/dash-spv/src/main.rs @@ -5,6 +5,7 @@ use std::process; use std::sync::Arc; use clap::{Parser, ValueEnum}; +use dash_spv::chain::CheckpointManager; use dash_spv::{ ClientConfig, DashSpvClient, DevnetConfig, LevelFilter, LlmqDevnetParams, MempoolStrategy, Network, ValidationMode, @@ -119,6 +120,13 @@ struct Args { #[arg(short, long, value_name = "HEIGHT")] start_height: Option, + /// Wallet birth height. Header, filter-header and filter sync anchor at the + /// nearest checkpoint at or before this height instead of genesis. Use 'now' + /// for the latest checkpoint (a brand-new wallet with no prior history). + /// Defaults to genesis (0) so an unspecified wallet still does a full scan. + #[arg(long, value_name = "HEIGHT")] + birth_height: Option, + /// Disable log file output (enables console logging as fallback) #[arg(long)] no_log_file: bool, @@ -249,11 +257,14 @@ async fn run() -> Result<(), Box> { config = config.with_user_agent(user_agent); } + // Resolve the wallet birth height, mapping 'now' to the latest checkpoint. + let birth_height = resolve_birth_height(&args.birth_height, config.network)?; + // Create the wallet manager let mut wallet_manager = WalletManager::::new(config.network); wallet_manager.create_wallet_from_mnemonic( mnemonic_phrase.as_str(), - 0, + birth_height, key_wallet::wallet::initialization::WalletAccountCreationOptions::default(), )?; let wallet = Arc::new(tokio::sync::RwLock::new(wallet_manager)); @@ -279,6 +290,27 @@ async fn run() -> Result<(), Box> { Ok(()) } +/// Resolve the `--birth-height` argument into a concrete block height. +/// +/// `None` means genesis (0). `now`/`latest` resolves to the highest bundled +/// checkpoint for the network, which is the right anchor for a brand-new wallet +/// with no prior on-chain history. A numeric value is taken verbatim and gets +/// snapped to the nearest checkpoint at or before it during sync anchoring. +fn resolve_birth_height(arg: &Option, network: Network) -> Result { + let Some(value) = arg else { + return Ok(0); + }; + + if value == "now" || value == "latest" { + return Ok(CheckpointManager::for_network(network) + .last_checkpoint() + .map(|checkpoint| checkpoint.height) + .unwrap_or(0)); + } + + value.parse::().map_err(|e| format!("Invalid birth height '{}': {}", value, e)) +} + fn build_client_config(args: &Args, data_dir: PathBuf) -> Result { let network: Network = args.network.into(); let validation_mode: ValidationMode = args.validation_mode.into(); @@ -437,6 +469,36 @@ mod tests { parse(&argv).expect("parse") } + #[test] + fn birth_height_resolves_none_now_and_numeric() { + // Unspecified means genesis, preserving the full-scan default. + assert_eq!(resolve_birth_height(&None, Network::Mainnet).unwrap(), 0); + + // A numeric value is taken verbatim (nearest-checkpoint snapping happens later). + assert_eq!( + resolve_birth_height(&Some("123456".to_string()), Network::Mainnet).unwrap(), + 123456 + ); + + // 'now'/'latest' resolve to the highest bundled checkpoint for the network. + let latest = CheckpointManager::for_network(Network::Mainnet) + .last_checkpoint() + .expect("mainnet has checkpoints") + .height; + assert!(latest > 0); + assert_eq!( + resolve_birth_height(&Some("now".to_string()), Network::Mainnet).unwrap(), + latest + ); + assert_eq!( + resolve_birth_height(&Some("latest".to_string()), Network::Mainnet).unwrap(), + latest + ); + + // Non-numeric junk is rejected. + assert!(resolve_birth_height(&Some("abc".to_string()), Network::Mainnet).is_err()); + } + #[test] fn devnet_requires_name() { let args = args(&["--network", "devnet"]);