Skip to content

Commit 3728bec

Browse files
committed
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.
1 parent cd6c04e commit 3728bec

1 file changed

Lines changed: 63 additions & 1 deletion

File tree

dash-spv/src/main.rs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::process;
55
use std::sync::Arc;
66

77
use clap::{Parser, ValueEnum};
8+
use dash_spv::chain::CheckpointManager;
89
use dash_spv::{
910
ClientConfig, DashSpvClient, DevnetConfig, LevelFilter, LlmqDevnetParams, MempoolStrategy,
1011
Network, ValidationMode,
@@ -119,6 +120,13 @@ struct Args {
119120
#[arg(short, long, value_name = "HEIGHT")]
120121
start_height: Option<String>,
121122

123+
/// Wallet birth height. Header, filter-header and filter sync anchor at the
124+
/// nearest checkpoint at or before this height instead of genesis. Use 'now'
125+
/// for the latest checkpoint (a brand-new wallet with no prior history).
126+
/// Defaults to genesis (0) so an unspecified wallet still does a full scan.
127+
#[arg(long, value_name = "HEIGHT")]
128+
birth_height: Option<String>,
129+
122130
/// Disable log file output (enables console logging as fallback)
123131
#[arg(long)]
124132
no_log_file: bool,
@@ -249,11 +257,14 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
249257
config = config.with_user_agent(user_agent);
250258
}
251259

260+
// Resolve the wallet birth height, mapping 'now' to the latest checkpoint.
261+
let birth_height = resolve_birth_height(&args.birth_height, config.network)?;
262+
252263
// Create the wallet manager
253264
let mut wallet_manager = WalletManager::<ManagedWalletInfo>::new(config.network);
254265
wallet_manager.create_wallet_from_mnemonic(
255266
mnemonic_phrase.as_str(),
256-
0,
267+
birth_height,
257268
key_wallet::wallet::initialization::WalletAccountCreationOptions::default(),
258269
)?;
259270
let wallet = Arc::new(tokio::sync::RwLock::new(wallet_manager));
@@ -279,6 +290,27 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
279290
Ok(())
280291
}
281292

293+
/// Resolve the `--birth-height` argument into a concrete block height.
294+
///
295+
/// `None` means genesis (0). `now`/`latest` resolves to the highest bundled
296+
/// checkpoint for the network, which is the right anchor for a brand-new wallet
297+
/// with no prior on-chain history. A numeric value is taken verbatim and gets
298+
/// snapped to the nearest checkpoint at or before it during sync anchoring.
299+
fn resolve_birth_height(arg: &Option<String>, network: Network) -> Result<u32, String> {
300+
let Some(value) = arg else {
301+
return Ok(0);
302+
};
303+
304+
if value == "now" || value == "latest" {
305+
return Ok(CheckpointManager::for_network(network)
306+
.last_checkpoint()
307+
.map(|checkpoint| checkpoint.height)
308+
.unwrap_or(0));
309+
}
310+
311+
value.parse::<u32>().map_err(|e| format!("Invalid birth height '{}': {}", value, e))
312+
}
313+
282314
fn build_client_config(args: &Args, data_dir: PathBuf) -> Result<ClientConfig, String> {
283315
let network: Network = args.network.into();
284316
let validation_mode: ValidationMode = args.validation_mode.into();
@@ -437,6 +469,36 @@ mod tests {
437469
parse(&argv).expect("parse")
438470
}
439471

472+
#[test]
473+
fn birth_height_resolves_none_now_and_numeric() {
474+
// Unspecified means genesis, preserving the full-scan default.
475+
assert_eq!(resolve_birth_height(&None, Network::Mainnet).unwrap(), 0);
476+
477+
// A numeric value is taken verbatim (nearest-checkpoint snapping happens later).
478+
assert_eq!(
479+
resolve_birth_height(&Some("123456".to_string()), Network::Mainnet).unwrap(),
480+
123456
481+
);
482+
483+
// 'now'/'latest' resolve to the highest bundled checkpoint for the network.
484+
let latest = CheckpointManager::for_network(Network::Mainnet)
485+
.last_checkpoint()
486+
.expect("mainnet has checkpoints")
487+
.height;
488+
assert!(latest > 0);
489+
assert_eq!(
490+
resolve_birth_height(&Some("now".to_string()), Network::Mainnet).unwrap(),
491+
latest
492+
);
493+
assert_eq!(
494+
resolve_birth_height(&Some("latest".to_string()), Network::Mainnet).unwrap(),
495+
latest
496+
);
497+
498+
// Non-numeric junk is rejected.
499+
assert!(resolve_birth_height(&Some("abc".to_string()), Network::Mainnet).is_err());
500+
}
501+
440502
#[test]
441503
fn devnet_requires_name() {
442504
let args = args(&["--network", "devnet"]);

0 commit comments

Comments
 (0)