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
35 changes: 22 additions & 13 deletions dash-spv/src/client/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +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::Network;
// Serialization removed due to complex Address types

use crate::client::devnet::DevnetConfig;
use crate::types::ValidationMode;

/// Strategy for handling mempool (unconfirmed) transactions.
Expand Down Expand Up @@ -72,9 +72,8 @@ pub struct ClientConfig {
/// The client will use the nearest checkpoint at or before this height.
pub start_from_height: Option<u32>,

/// 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>,
/// Devnet-only configuration. Must be `Some` iff `network == Network::Devnet`.
pub devnet: Option<DevnetConfig>,
}

impl Default for ClientConfig {
Expand All @@ -95,7 +94,7 @@ impl Default for ClientConfig {
max_mempool_transactions: 1000,
fetch_mempool_transactions: true,
start_from_height: None,
llmq_devnet_params: None,
devnet: None,
}
}
}
Expand Down Expand Up @@ -187,10 +186,10 @@ impl ClientConfig {
self
}

/// Override `LLMQ_DEVNET` quorum size and threshold for a devnet.
/// Mirrors Dash Core's `-llmqdevnetparams=<size>:<threshold>`.
pub fn with_llmq_devnet_params(mut self, params: LlmqDevnetParams) -> Self {
self.llmq_devnet_params = Some(params);
/// Attach a [`DevnetConfig`]. The network must be `Network::Devnet`.
/// [`validate`](Self::validate) enforces the biconditional.
pub fn with_devnet(mut self, devnet: DevnetConfig) -> Self {
self.devnet = Some(devnet);
self
}

Expand All @@ -209,8 +208,18 @@ impl ClientConfig {
);
}

if self.llmq_devnet_params.is_some() && self.network != Network::Devnet {
return Err("llmq_devnet_params is only valid on devnet".to_string());
match (self.network == Network::Devnet, &self.devnet) {
(true, Some(devnet)) => devnet.validate()?,
(true, None) => {
return Err("network is Devnet but no DevnetConfig was provided".to_string());
}
(false, Some(_)) => {
return Err(format!(
"DevnetConfig is only valid on Devnet, but network is {:?}",
self.network
));
}
(false, None) => {}
}

std::fs::create_dir_all(&self.storage_path).map_err(|e| {
Expand All @@ -226,8 +235,8 @@ impl ClientConfig {
/// Apply process-wide settings derived from this config. Idempotent for the
/// same values, returns an error if a conflicting setting was already applied.
pub(crate) fn apply_global_overrides(&self) -> Result<(), String> {
if let Some(params) = self.llmq_devnet_params {
set_llmq_devnet_params(params).map_err(|e| e.to_string())?;
if let Some(devnet) = &self.devnet {
devnet.apply_global_overrides()?;
}
Ok(())
}
Expand Down
124 changes: 123 additions & 1 deletion dash-spv/src/client/config_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@
#[cfg(test)]
mod tests {
use crate::client::config::{ClientConfig, MempoolStrategy};
use crate::client::devnet::DevnetConfig;
use crate::types::ValidationMode;
use dashcore::sml::llmq_type::{
devnet_chain_locks_type_override, devnet_isd_type_override, devnet_platform_type_override,
llmq_devnet_params, LLMQType, LlmqDevnetParams,
};
use dashcore::Network;
use std::net::SocketAddr;
use std::path::PathBuf;
use tempfile::TempDir;

#[test]
fn test_default_config() {
Expand All @@ -24,21 +30,26 @@ mod tests {
assert_eq!(config.mempool_strategy, MempoolStrategy::FetchAll);
assert_eq!(config.max_mempool_transactions, 1000);
assert!(config.fetch_mempool_transactions);

assert!(config.devnet.is_none());
}

#[test]
fn test_network_specific_configs() {
let mainnet = ClientConfig::mainnet();
assert_eq!(mainnet.network, Network::Mainnet);
assert!(mainnet.peers.is_empty()); // Should use DNS discovery
assert!(mainnet.devnet.is_none());

let testnet = ClientConfig::testnet();
assert_eq!(testnet.network, Network::Testnet);
assert!(testnet.peers.is_empty()); // Should use DNS discovery
assert!(testnet.devnet.is_none());

let regtest = ClientConfig::regtest();
assert_eq!(regtest.network, Network::Regtest);
assert!(regtest.peers.is_empty());
assert!(regtest.devnet.is_none());
}

#[test]
Expand All @@ -60,6 +71,83 @@ mod tests {
assert_eq!(config.mempool_strategy, MempoolStrategy::BloomFilter);
assert_eq!(config.max_mempool_transactions, 500);
assert_eq!(config.start_from_height, Some(100000));
assert!(config.devnet.is_none());
}

#[test]
fn test_with_devnet_round_trip() {
let devnet = DevnetConfig::new("alpha")
.with_llmq_params(LlmqDevnetParams {
size: 6,
threshold: 4,
})
.with_chainlocks_type(LLMQType::Llmqtype50_60)
.with_instantsend_dip0024_type(LLMQType::LlmqtypeDevnetDIP0024)
.with_platform_type(LLMQType::LlmqtypeDevnetPlatform);

let config = ClientConfig::new(Network::Devnet).with_devnet(devnet);

let devnet = config.devnet.as_ref().expect("devnet must be set");
assert_eq!(devnet.name, "alpha");
assert_eq!(
devnet.llmq_params,
Some(LlmqDevnetParams {
size: 6,
threshold: 4
})
);
assert_eq!(devnet.llmq_chainlocks_type, Some(LLMQType::Llmqtype50_60));
assert_eq!(devnet.llmq_instantsend_dip0024_type, Some(LLMQType::LlmqtypeDevnetDIP0024));
assert_eq!(devnet.llmq_platform_type, Some(LLMQType::LlmqtypeDevnetPlatform));
}

#[test]
fn test_user_agent_format_matches_dash_core() {
let devnet = DevnetConfig::new("alpha");
assert_eq!(devnet.user_agent("0.43.0"), "/rust-dash-spv:0.43.0(devnet.devnet-alpha)/");
}

#[test]
fn test_validate_devnet_matrix() {
let tmp = TempDir::new().unwrap();
let networks = [Network::Mainnet, Network::Testnet, Network::Regtest, Network::Devnet];
for network in networks {
let want_devnet = network == Network::Devnet;
for has_devnet in [false, true] {
let mut config =
ClientConfig::new(network).with_storage_path(tmp.path().join("storage"));
if has_devnet {
config = config.with_devnet(DevnetConfig::new("alpha"));
}
let result = config.validate();
if has_devnet == want_devnet {
assert!(
result.is_ok(),
"network={:?} has_devnet={} should be OK, got {:?}",
network,
has_devnet,
result
);
} else {
assert!(
result.is_err(),
"network={:?} has_devnet={} must error",
network,
has_devnet
);
}
}
}
}

#[test]
fn test_validate_rejects_empty_devnet_name() {
let tmp = TempDir::new().unwrap();
let config = ClientConfig::new(Network::Devnet)
.with_storage_path(tmp.path().join("storage"))
.with_devnet(DevnetConfig::new(""));
let err = config.validate().expect_err("empty name must be rejected");
assert!(err.contains("must not be empty"), "got: {}", err);
}

#[test]
Expand Down Expand Up @@ -109,5 +197,39 @@ mod tests {
assert!(result.unwrap_err().contains("max_mempool_transactions must be > 0"));
}

// Removed selective strategy validation test; Selective variant no longer exists
#[test]
fn test_apply_global_overrides_no_devnet_is_noop() {
let tmp = TempDir::new().unwrap();
let config = ClientConfig::new(Network::Mainnet).with_storage_path(tmp.path());
assert!(config.apply_global_overrides().is_ok());
}

// Each `dashcore` `OnceLock` accepts only one value per process; all four
// slots are exercised here in one shot.
#[test]
fn test_apply_global_overrides_forwards_all_slots() {
let tmp = TempDir::new().unwrap();
let devnet = DevnetConfig::new("alpha")
.with_llmq_params(LlmqDevnetParams {
size: 11,
threshold: 7,
})
.with_chainlocks_type(LLMQType::Llmqtype100_67)
.with_instantsend_dip0024_type(LLMQType::Llmqtype60_75)
.with_platform_type(LLMQType::LlmqtypeDevnet);
let config =
ClientConfig::new(Network::Devnet).with_storage_path(tmp.path()).with_devnet(devnet);

config.apply_global_overrides().expect("forwarding all four overrides must succeed");

let params = llmq_devnet_params();
assert_eq!(params.size, 11);
assert_eq!(params.threshold, 7);
assert_eq!(devnet_chain_locks_type_override(), Some(LLMQType::Llmqtype100_67));
assert_eq!(devnet_isd_type_override(), Some(LLMQType::Llmqtype60_75));
assert_eq!(devnet_platform_type_override(), Some(LLMQType::LlmqtypeDevnet));

// Re-applying the same config must be idempotent.
config.apply_global_overrides().expect("idempotent re-apply");
}
}
106 changes: 106 additions & 0 deletions dash-spv/src/client/devnet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//! Devnet-only configuration knobs that mirror Dash Core's `-devnet=<name>`,
//! `-llmqdevnetparams`, and the three `-llmq{chainlocks,instantsenddip0024,platform}`
//! routing flags. Grouped into a single struct so the cross-field invariant
//! "presence iff `Network::Devnet`" is expressible at the `ClientConfig` level.

use dashcore::sml::llmq_type::{
set_devnet_chain_locks_type, set_devnet_isd_type, set_devnet_platform_type,
set_llmq_devnet_params, LLMQType, LlmqDevnetParams,
};

/// Configuration values that only apply on `Network::Devnet`.
///
/// The `name` field is required because Dash Core embeds the devnet name into
/// both the genesis-block discovery and the peer-handshake user agent. Without
/// a name the SPV client cannot complete a devnet handshake against `dashd`.
/// Dash Core itself technically accepts `-devnet` with no name (defaulting the
/// network name to `"devnet"`), but every real devnet is launched with one.
#[derive(Debug, Clone)]
pub struct DevnetConfig {
/// Devnet name. Embedded in the user agent suffix
/// (`devnet.devnet-<name>`) so peers gating on the name accept us.
pub name: String,
/// Override for `LLMQ_DEVNET` quorum size and threshold.
/// Mirrors Dash Core's `-llmqdevnetparams=<size>:<threshold>`.
pub llmq_params: Option<LlmqDevnetParams>,
/// Reroute ChainLocks onto a different devnet LLMQ type.
/// Mirrors Dash Core's `-llmqchainlocks=<quorum name>`.
pub llmq_chainlocks_type: Option<LLMQType>,
/// Reroute InstantSend DIP24 locks onto a different devnet LLMQ type.
/// Mirrors Dash Core's `-llmqinstantsenddip0024=<quorum name>`.
pub llmq_instantsend_dip0024_type: Option<LLMQType>,
/// Reroute Platform quorums onto a different devnet LLMQ type.
/// Mirrors Dash Core's `-llmqplatform=<quorum name>`.
pub llmq_platform_type: Option<LLMQType>,
}

impl DevnetConfig {
/// Create a new devnet config with no overrides.
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
llmq_params: None,
llmq_chainlocks_type: None,
llmq_instantsend_dip0024_type: None,
llmq_platform_type: None,
}
}

/// Set `LLMQ_DEVNET` size and threshold override.
pub fn with_llmq_params(mut self, params: LlmqDevnetParams) -> Self {
self.llmq_params = Some(params);
self
}

/// Set the ChainLocks LLMQ routing override.
pub fn with_chainlocks_type(mut self, llmq_type: LLMQType) -> Self {
self.llmq_chainlocks_type = Some(llmq_type);
self
}

/// Set the InstantSend DIP24 LLMQ routing override.
pub fn with_instantsend_dip0024_type(mut self, llmq_type: LLMQType) -> Self {
self.llmq_instantsend_dip0024_type = Some(llmq_type);
self
}

/// Set the Platform LLMQ routing override.
pub fn with_platform_type(mut self, llmq_type: LLMQType) -> Self {
self.llmq_platform_type = Some(llmq_type);
self
}

/// Render the user agent suffix that signals devnet identity to peers,
/// matching the format `dashd` itself uses: `/<base>(devnet.devnet-<name>)/`.
pub fn user_agent(&self, crate_version: &str) -> String {
format!("/rust-dash-spv:{}(devnet.devnet-{})/", crate_version, self.name)
}

pub(crate) fn validate(&self) -> Result<(), String> {
if self.name.is_empty() {
return Err("devnet name must not be empty".to_string());
}
if self.name.contains('/') {
return Err("devnet name must not contain '/'".to_string());
}
Ok(())
}

/// Apply the four `dashcore` process-global overrides. Idempotent for
/// identical values, errors on conflicting re-set or invalid type.
pub(crate) fn apply_global_overrides(&self) -> Result<(), String> {
if let Some(params) = self.llmq_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(())
}
Comment thread
xdustinface marked this conversation as resolved.
}
2 changes: 2 additions & 0 deletions dash-spv/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
//! - `sync_coordinator.rs` - Sync orchestration and network monitoring
pub mod config;
pub mod devnet;
pub mod event_handler;

mod core;
Expand All @@ -26,6 +27,7 @@ mod transactions;

// Re-export public types from extracted modules
pub use config::ClientConfig;
pub use devnet::DevnetConfig;
pub use event_handler::EventHandler;

// Re-export the main client struct
Expand Down
4 changes: 2 additions & 2 deletions dash-spv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ pub mod validation;

// Re-export main types for convenience
pub use client::config::MempoolStrategy;
pub use client::{ClientConfig, DashSpvClient, EventHandler};
pub use client::{ClientConfig, DashSpvClient, DevnetConfig, EventHandler};
pub use error::{
LoggingError, LoggingResult, NetworkError, SpvError, StorageError, SyncError, ValidationError,
};
Expand All @@ -95,7 +95,7 @@ pub use dashcore::sml::masternode_list_engine::{
};

// Re-export LLMQ types
pub use dashcore::sml::llmq_type::LLMQType;
pub use dashcore::sml::llmq_type::{LLMQType, LlmqDevnetParams};

/// Current version of the dash-spv library.
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
Expand Down
Loading
Loading