Skip to content
Draft
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
132 changes: 131 additions & 1 deletion dash-spv/src/client/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use clap::ValueEnum;
use std::net::SocketAddr;
use std::path::PathBuf;

use dashcore::sml::llmq_type::{set_llmq_devnet_params, LlmqDevnetParams};
use dashcore::sml::llmq_type::{
set_devnet_chain_locks_type, set_devnet_isd_type, set_devnet_platform_type,
set_llmq_devnet_params, LLMQType, LlmqDevnetParams,
};
use dashcore::Network;
// Serialization removed due to complex Address types

Expand Down Expand Up @@ -75,6 +78,18 @@ pub struct ClientConfig {
/// Override for `LLMQ_DEVNET` quorum size and threshold, applied at startup.
/// Mirrors Dash Core's `-llmqdevnetparams=<size>:<threshold>`. Only meaningful on devnet.
pub llmq_devnet_params: Option<LlmqDevnetParams>,

/// Override the LLMQ type used for ChainLocks, applied at startup.
/// Mirrors Dash Core's `-llmqchainlocks=<quorum name>`. Only valid on devnet.
pub llmq_chainlocks_type: Option<LLMQType>,

/// Override the LLMQ type used for InstantSend DIP24 locks, applied at startup.
/// Mirrors Dash Core's `-llmqinstantsenddip0024=<quorum name>`. Only valid on devnet.
pub llmq_instantsend_dip0024_type: Option<LLMQType>,

/// Override the LLMQ type used for Platform quorums, applied at startup.
/// Mirrors Dash Core's `-llmqplatform=<quorum name>`. Only valid on devnet.
pub llmq_platform_type: Option<LLMQType>,
}

impl Default for ClientConfig {
Expand All @@ -96,6 +111,9 @@ impl Default for ClientConfig {
fetch_mempool_transactions: true,
start_from_height: None,
llmq_devnet_params: None,
llmq_chainlocks_type: None,
llmq_instantsend_dip0024_type: None,
llmq_platform_type: None,
}
}
}
Expand Down Expand Up @@ -194,6 +212,27 @@ impl ClientConfig {
self
}

/// Reroute ChainLocks onto a different devnet LLMQ type.
/// Mirrors Dash Core's `-llmqchainlocks=<quorum name>`.
pub fn with_llmq_chainlocks_type(mut self, llmq_type: LLMQType) -> Self {
self.llmq_chainlocks_type = Some(llmq_type);
self
}

/// Reroute InstantSend DIP24 locks onto a different devnet LLMQ type.
/// Mirrors Dash Core's `-llmqinstantsenddip0024=<quorum name>`.
pub fn with_llmq_instantsend_dip0024_type(mut self, llmq_type: LLMQType) -> Self {
self.llmq_instantsend_dip0024_type = Some(llmq_type);
self
}

/// Reroute Platform quorums onto a different devnet LLMQ type.
/// Mirrors Dash Core's `-llmqplatform=<quorum name>`.
pub fn with_llmq_platform_type(mut self, llmq_type: LLMQType) -> Self {
self.llmq_platform_type = Some(llmq_type);
self
}

/// Validate the configuration.
pub fn validate(&self) -> Result<(), String> {
// Note: Empty peers list is now valid - DNS discovery will be used automatically
Expand All @@ -213,6 +252,18 @@ impl ClientConfig {
return Err("llmq_devnet_params is only valid on devnet".to_string());
}

if self.network != Network::Devnet {
if self.llmq_chainlocks_type.is_some() {
return Err("llmq_chainlocks_type is only valid on devnet".to_string());
}
if self.llmq_instantsend_dip0024_type.is_some() {
return Err("llmq_instantsend_dip0024_type is only valid on devnet".to_string());
}
if self.llmq_platform_type.is_some() {
return Err("llmq_platform_type is only valid on devnet".to_string());
}
}

std::fs::create_dir_all(&self.storage_path).map_err(|e| {
format!(
"A valid storage path must be provided to the ClientConfig {:?}: {e}",
Expand All @@ -229,6 +280,85 @@ impl ClientConfig {
if let Some(params) = self.llmq_devnet_params {
set_llmq_devnet_params(params).map_err(|e| e.to_string())?;
}
if let Some(t) = self.llmq_chainlocks_type {
set_devnet_chain_locks_type(t)?;
}
if let Some(t) = self.llmq_instantsend_dip0024_type {
set_devnet_isd_type(t)?;
}
if let Some(t) = self.llmq_platform_type {
set_devnet_platform_type(t)?;
}
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;

fn devnet_config() -> ClientConfig {
let mut cfg = ClientConfig::new(Network::Devnet);
// Validation creates the storage dir; point it somewhere disposable.
let unique = format!(
"dash-spv-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or_default()
);
cfg.storage_path = std::env::temp_dir().join(unique);
cfg
}

#[test]
fn rejects_chainlocks_override_outside_devnet() {
for net in [Network::Mainnet, Network::Testnet, Network::Regtest] {
let mut cfg = ClientConfig::new(net);
cfg.llmq_chainlocks_type = Some(LLMQType::LlmqtypeDevnet);
let err = cfg.validate().expect_err("non-devnet must reject chainlocks override");
assert!(err.contains("llmq_chainlocks_type"), "{err}");
}
}

#[test]
fn rejects_isd_override_outside_devnet() {
for net in [Network::Mainnet, Network::Testnet, Network::Regtest] {
let mut cfg = ClientConfig::new(net);
cfg.llmq_instantsend_dip0024_type = Some(LLMQType::LlmqtypeDevnet);
let err = cfg.validate().expect_err("non-devnet must reject isd override");
assert!(err.contains("llmq_instantsend_dip0024_type"), "{err}");
}
}

#[test]
fn rejects_platform_override_outside_devnet() {
for net in [Network::Mainnet, Network::Testnet, Network::Regtest] {
let mut cfg = ClientConfig::new(net);
cfg.llmq_platform_type = Some(LLMQType::LlmqtypeDevnet);
let err = cfg.validate().expect_err("non-devnet must reject platform override");
assert!(err.contains("llmq_platform_type"), "{err}");
}
}

#[test]
fn accepts_routing_overrides_on_devnet() {
let mut cfg = devnet_config();
cfg.llmq_chainlocks_type = Some(LLMQType::LlmqtypeDevnetDIP0024);
cfg.llmq_instantsend_dip0024_type = Some(LLMQType::LlmqtypeDevnet);
cfg.llmq_platform_type = Some(LLMQType::LlmqtypeDevnet);
cfg.validate().expect("devnet routing overrides should validate");
}

#[test]
fn builder_methods_set_routing_overrides() {
let cfg = ClientConfig::new(Network::Devnet)
.with_llmq_chainlocks_type(LLMQType::LlmqtypeDevnetDIP0024)
.with_llmq_instantsend_dip0024_type(LLMQType::LlmqtypeDevnet)
.with_llmq_platform_type(LLMQType::LlmqtypeDevnet);
assert_eq!(cfg.llmq_chainlocks_type, Some(LLMQType::LlmqtypeDevnetDIP0024));
assert_eq!(cfg.llmq_instantsend_dip0024_type, Some(LLMQType::LlmqtypeDevnet));
assert_eq!(cfg.llmq_platform_type, Some(LLMQType::LlmqtypeDevnet));
}
}
45 changes: 44 additions & 1 deletion dash-spv/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::sync::Arc;

use clap::{Parser, ValueEnum};
use dash_spv::{ClientConfig, DashSpvClient, LevelFilter, MempoolStrategy, Network};
use dashcore::sml::llmq_type::LlmqDevnetParams;
use dashcore::sml::llmq_type::{devnet_llmq_type_from_name, LlmqDevnetParams};
use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo;
use key_wallet_manager::WalletManager;

Expand Down Expand Up @@ -143,6 +143,21 @@ struct Args {
/// Override `LLMQ_DEVNET` size and threshold (matches Dash Core's `-llmqdevnetparams=<size>:<threshold>`).
#[arg(long, value_name = "SIZE:THRESHOLD")]
llmq_devnet_params: Option<String>,

/// Reroute ChainLocks onto the given devnet quorum (matches Dash Core's
/// `-llmqchainlocks=<quorum name>`). Accepts: llmq_devnet, llmq_devnet_dip0024, llmq_devnet_platform.
#[arg(long, value_name = "QUORUM_NAME")]
llmq_chainlocks: Option<String>,

/// Reroute InstantSend DIP24 onto the given devnet quorum (matches Dash Core's
/// `-llmqinstantsenddip0024=<quorum name>`). Accepts: llmq_devnet, llmq_devnet_dip0024, llmq_devnet_platform.
#[arg(long, value_name = "QUORUM_NAME")]
llmq_instantsend_dip0024: Option<String>,

/// Reroute Platform quorums onto the given devnet quorum (matches Dash Core's
/// `-llmqplatform=<quorum name>`). Accepts: llmq_devnet, llmq_devnet_dip0024, llmq_devnet_platform.
#[arg(long, value_name = "QUORUM_NAME")]
llmq_platform: Option<String>,
}

#[tokio::main]
Expand Down Expand Up @@ -252,13 +267,41 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
params.threshold
);
}

if let Some(name) = args.llmq_chainlocks.as_deref() {
let t = devnet_llmq_type_from_name(name)
.map_err(|e| format!("--llmq-chainlocks: {}", e))?;
config = config.with_llmq_chainlocks_type(t);
tracing::info!("ChainLocks LLMQ type overridden: {:?}", t);
}
if let Some(name) = args.llmq_instantsend_dip0024.as_deref() {
let t = devnet_llmq_type_from_name(name)
.map_err(|e| format!("--llmq-instantsend-dip0024: {}", e))?;
config = config.with_llmq_instantsend_dip0024_type(t);
tracing::info!("InstantSend DIP24 LLMQ type overridden: {:?}", t);
}
if let Some(name) = args.llmq_platform.as_deref() {
let t =
devnet_llmq_type_from_name(name).map_err(|e| format!("--llmq-platform: {}", e))?;
config = config.with_llmq_platform_type(t);
tracing::info!("Platform LLMQ type overridden: {:?}", t);
}
} else {
if args.devnet_name.is_some() {
return Err("--devnet-name is only valid with --network=devnet".into());
}
if args.llmq_devnet_params.is_some() {
return Err("--llmq-devnet-params is only valid with --network=devnet".into());
}
if args.llmq_chainlocks.is_some() {
return Err("--llmq-chainlocks is only valid with --network=devnet".into());
}
if args.llmq_instantsend_dip0024.is_some() {
return Err("--llmq-instantsend-dip0024 is only valid with --network=devnet".into());
}
if args.llmq_platform.is_some() {
return Err("--llmq-platform is only valid with --network=devnet".into());
}
}

// Add custom peers if specified
Expand Down
Loading
Loading