From 38d265696ace082842c9dbf7b294ff501a5f9c49 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Wed, 27 May 2026 23:35:58 +1000 Subject: [PATCH 1/8] fix(dash): return known genesis hash for `Network::Devnet` The static height-0 devnet genesis block has a deterministic hash (verified by the existing `devnet_genesis_full_block` test). Returning `None` here caused `dash-spv` to fail with `"No known genesis hash for network"` when initializing storage for a devnet. Only the height-1 "devnet genesis" varies per devnet name, height 0 does not. --- dash/src/network/constants.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/dash/src/network/constants.rs b/dash/src/network/constants.rs index 324789cf6..6fc4268bc 100644 --- a/dash/src/network/constants.rs +++ b/dash/src/network/constants.rs @@ -52,9 +52,6 @@ pub const PROTOCOL_VERSION: u32 = 70237; pub trait NetworkExt { /// Returns the known genesis block hash for `network`, if one is hardcoded. - /// - /// `Network::Devnet` returns `None` because devnets use dynamically-generated - /// genesis blocks. fn known_genesis_block_hash(&self) -> Option; } @@ -75,7 +72,13 @@ impl NetworkExt for Network { block_hash.reverse(); Some(BlockHash::from_byte_array(block_hash.try_into().expect("expected 32 bytes"))) } - Network::Devnet => None, + Network::Devnet => { + let mut block_hash = + hex::decode("000008ca1832a4baf228eb1553c03d3a2c8e02399550dd6ea8d65cec3ef23d2e") + .expect("expected valid hex"); + block_hash.reverse(); + Some(BlockHash::from_byte_array(block_hash.try_into().expect("expected 32 bytes"))) + } Network::Regtest => { let mut block_hash = hex::decode("000008ca1832a4baf228eb1553c03d3a2c8e02399550dd6ea8d65cec3ef23d2e") From 4cf58b77811e55660f9f8c51e8b836089d5781e0 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Wed, 27 May 2026 23:36:09 +1000 Subject: [PATCH 2/8] fix(dash): use `LlmqtypeDevnetPlatform` for `Network::Devnet` platform quorum `platform_type()` returned `LlmqtypeDevnet`, but Dash Core's devnet defaults use `LLMQ_DEVNET_PLATFORM` for `llmqTypePlatform` (see `chainparams.cpp:651`). This matches the standard devnet config `llmqplatform=llmq_devnet_platform`. --- dash/src/sml/llmq_type/network.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/src/sml/llmq_type/network.rs b/dash/src/sml/llmq_type/network.rs index a0fc6c9ab..65a6b7a00 100644 --- a/dash/src/sml/llmq_type/network.rs +++ b/dash/src/sml/llmq_type/network.rs @@ -45,7 +45,7 @@ impl NetworkLLMQExt for Network { match self { Network::Mainnet => LLMQType::Llmqtype100_67, Network::Testnet => LLMQType::Llmqtype25_67, - Network::Devnet => LLMQType::LlmqtypeDevnet, + Network::Devnet => LLMQType::LlmqtypeDevnetPlatform, Network::Regtest => LLMQType::LlmqtypeTestnetPlatform, } } From 4c2dbc9baa510e04339407d68de08cc636512800 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Wed, 27 May 2026 23:37:40 +1000 Subject: [PATCH 3/8] feat(dash-spv): add `devnet` to `--network` CLI Allows targeting `Network::Devnet` from the `dash-spv` binary. Required for SPV-syncing a devnet, since previously only mainnet, testnet, and regtest were exposed. --- dash-spv/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dash-spv/src/main.rs b/dash-spv/src/main.rs index 677ff274f..42689bb56 100644 --- a/dash-spv/src/main.rs +++ b/dash-spv/src/main.rs @@ -14,6 +14,7 @@ use key_wallet_manager::WalletManager; enum NetworkArg { Mainnet, Testnet, + Devnet, Regtest, } @@ -22,6 +23,7 @@ impl From for Network { match arg { NetworkArg::Mainnet => Network::Mainnet, NetworkArg::Testnet => Network::Testnet, + NetworkArg::Devnet => Network::Devnet, NetworkArg::Regtest => Network::Regtest, } } From a8a537ec66f64c71fb47c60887e6ebe4d2d4231b Mon Sep 17 00:00:00 2001 From: xdustinface Date: Wed, 27 May 2026 23:39:12 +1000 Subject: [PATCH 4/8] feat(dash-spv): add `--devnet-name` for devnet handshake compatibility Dash Core nodes on devnet check that incoming peers carry `devnet.` in their user agent and disconnect otherwise (see `net_processing.cpp:3957-3967`). The `network-id` is `devnet-` per `ArgsManager::GetDevNetName`, so the substring required for the `paloma` devnet is `devnet.devnet-paloma`. The new flag is required whenever `--network=devnet`, and the user agent is rebuilt as `/rust-dash-spv:(devnet.devnet-)/` so devnet peers accept the handshake. --- dash-spv/src/main.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/dash-spv/src/main.rs b/dash-spv/src/main.rs index 42689bb56..841df0baa 100644 --- a/dash-spv/src/main.rs +++ b/dash-spv/src/main.rs @@ -134,6 +134,10 @@ struct Args { /// Path to file containing BIP39 mnemonic phrase #[arg(long, value_name = "PATH")] mnemonic_file: String, + + /// Devnet name (required when --network=devnet). Embedded in user agent so devnet peers accept the connection. + #[arg(long, value_name = "NAME")] + devnet_name: Option, } #[tokio::main] @@ -214,6 +218,19 @@ async fn run() -> Result<(), Box> { .with_storage_path(data_dir.clone()) .with_validation_mode(validation_mode); + if network == Network::Devnet { + let devnet_name = args + .devnet_name + .as_deref() + .ok_or("--devnet-name is required when --network=devnet")?; + let user_agent = + format!("/rust-dash-spv:{}(devnet.devnet-{})/", dash_spv::VERSION, devnet_name); + tracing::info!("Devnet user agent: {}", user_agent); + config = config.with_user_agent(user_agent); + } else if args.devnet_name.is_some() { + return Err("--devnet-name is only valid with --network=devnet".into()); + } + // Add custom peers if specified if !args.peer.is_empty() { config.peers.clear(); From f59de5ffcc8975e8489de704f1293658f3bffbf2 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Wed, 27 May 2026 23:40:24 +1000 Subject: [PATCH 5/8] feat: support `LLMQ_DEVNET` params override via `ClientConfig` Dash Core's `-llmqdevnetparams=:` lets a devnet adjust the `LLMQ_DEVNET` quorum size and threshold (`chainparams.cpp:UpdateLLMQDevnetParameters`). Without matching params, the SPV client picks the wrong masternodes when reconstructing the signing set, so ChainLock and legacy InstantSend signatures cannot be verified on devnets that use the override. `ClientConfig::llmq_devnet_params: Option<(u32, u32)>` carries the override and `DashSpvClient::new` applies it after `validate`. Internally this writes to a `OnceLock` in `dashcore::sml::llmq_type` that `LLMQType::params()` consults for `LlmqtypeDevnet`, mirroring Dash Core's chainparams-as-global model without exposing the setter beyond the lib. Only `LLMQ_DEVNET` is affected (matches Dash Core, the DIP0024 and platform devnet quorums are not adjusted by this flag). The `dash-spv` binary exposes the override as `--llmq-devnet-params=:`, valid only with `--network=devnet`. --- dash-spv/src/client/config.rs | 26 +++++++++++++++++++++++++ dash-spv/src/client/lifecycle.rs | 1 + dash-spv/src/main.rs | 33 ++++++++++++++++++++++++++------ dash/src/sml/llmq_type/mod.rs | 26 +++++++++++++++++++++++-- 4 files changed, 78 insertions(+), 8 deletions(-) diff --git a/dash-spv/src/client/config.rs b/dash-spv/src/client/config.rs index 8d5dc7ef6..d0e27b6e5 100644 --- a/dash-spv/src/client/config.rs +++ b/dash-spv/src/client/config.rs @@ -4,6 +4,7 @@ use clap::ValueEnum; use std::net::SocketAddr; use std::path::PathBuf; +use dashcore::sml::llmq_type::set_llmq_devnet_params; use dashcore::Network; // Serialization removed due to complex Address types @@ -70,6 +71,10 @@ pub struct ClientConfig { /// Start syncing from a specific block height. /// The client will use the nearest checkpoint at or before this height. pub start_from_height: Option, + + /// Override for `LLMQ_DEVNET` quorum size and threshold, applied at startup. + /// Mirrors Dash Core's `-llmqdevnetparams=:`. Only meaningful on devnet. + pub llmq_devnet_params: Option<(u32, u32)>, } impl Default for ClientConfig { @@ -90,6 +95,7 @@ impl Default for ClientConfig { max_mempool_transactions: 1000, fetch_mempool_transactions: true, start_from_height: None, + llmq_devnet_params: None, } } } @@ -181,6 +187,13 @@ impl ClientConfig { self } + /// Override `LLMQ_DEVNET` quorum size and threshold for a devnet. + /// Mirrors Dash Core's `-llmqdevnetparams=:`. + pub fn with_llmq_devnet_params(mut self, size: u32, threshold: u32) -> Self { + self.llmq_devnet_params = Some((size, threshold)); + self + } + /// Validate the configuration. pub fn validate(&self) -> Result<(), String> { // Note: Empty peers list is now valid - DNS discovery will be used automatically @@ -196,6 +209,10 @@ 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()); + } + std::fs::create_dir_all(&self.storage_path).map_err(|e| { format!( "A valid storage path must be provided to the ClientConfig {:?}: {e}", @@ -205,4 +222,13 @@ impl ClientConfig { Ok(()) } + + /// 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((size, threshold)) = self.llmq_devnet_params { + set_llmq_devnet_params(size, threshold).map_err(|e| e.to_string())?; + } + Ok(()) + } } diff --git a/dash-spv/src/client/lifecycle.rs b/dash-spv/src/client/lifecycle.rs index fc2b282d8..8c7d4f29f 100644 --- a/dash-spv/src/client/lifecycle.rs +++ b/dash-spv/src/client/lifecycle.rs @@ -40,6 +40,7 @@ impl DashSpvClient, + + /// Override `LLMQ_DEVNET` size and threshold (matches Dash Core's `-llmqdevnetparams=:`). + #[arg(long, value_name = "SIZE:THRESHOLD")] + llmq_devnet_params: Option, } #[tokio::main] @@ -219,16 +223,33 @@ async fn run() -> Result<(), Box> { .with_validation_mode(validation_mode); if network == Network::Devnet { - let devnet_name = args - .devnet_name - .as_deref() - .ok_or("--devnet-name is required when --network=devnet")?; + let devnet_name = + args.devnet_name.as_deref().ok_or("--devnet-name is required when --network=devnet")?; let user_agent = format!("/rust-dash-spv:{}(devnet.devnet-{})/", dash_spv::VERSION, devnet_name); tracing::info!("Devnet user agent: {}", user_agent); config = config.with_user_agent(user_agent); - } else if args.devnet_name.is_some() { - return Err("--devnet-name is only valid with --network=devnet".into()); + + if let Some(raw) = args.llmq_devnet_params.as_deref() { + let (size_str, threshold_str) = raw.split_once(':').ok_or_else(|| { + format!("--llmq-devnet-params expects SIZE:THRESHOLD, got '{}'", raw) + })?; + let size: u32 = size_str + .parse() + .map_err(|e| format!("invalid LLMQ_DEVNET size '{}': {}", size_str, e))?; + let threshold: u32 = threshold_str + .parse() + .map_err(|e| format!("invalid LLMQ_DEVNET threshold '{}': {}", threshold_str, e))?; + config = config.with_llmq_devnet_params(size, threshold); + tracing::info!("LLMQ_DEVNET params overridden: size={} threshold={}", size, threshold); + } + } 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()); + } } // Add custom peers if specified diff --git a/dash/src/sml/llmq_type/mod.rs b/dash/src/sml/llmq_type/mod.rs index 9fb8b3334..32a8cefcf 100644 --- a/dash/src/sml/llmq_type/mod.rs +++ b/dash/src/sml/llmq_type/mod.rs @@ -3,6 +3,7 @@ pub mod rotation; use std::fmt::{Display, Formatter}; use std::io; +use std::sync::OnceLock; #[cfg(feature = "bincode")] use bincode::{Decode, Encode}; @@ -207,6 +208,27 @@ pub const LLMQ_DEVNET: LLMQParams = LLMQParams { recovery_members: 6, }; +/// Runtime override for `LLMQ_DEVNET` params, matching Dash Core's `-llmqdevnetparams`. +static LLMQ_DEVNET_OVERRIDE: OnceLock<(u32, u32)> = OnceLock::new(); + +/// Override the `LLMQ_DEVNET` quorum size and threshold (matches Dash Core's +/// `-llmqdevnetparams=:`). May only be called once per process. +pub fn set_llmq_devnet_params(size: u32, threshold: u32) -> Result<(), &'static str> { + LLMQ_DEVNET_OVERRIDE.set((size, threshold)).map_err(|_| "LLMQ_DEVNET params already set") +} + +/// Get the effective `LLMQ_DEVNET` params, applying any runtime override. +pub fn llmq_devnet_params() -> LLMQParams { + let mut params = LLMQ_DEVNET; + if let Some(&(size, threshold)) = LLMQ_DEVNET_OVERRIDE.get() { + params.size = size; + params.min_size = threshold; + params.threshold = threshold; + params.dkg_params.bad_votes_threshold = threshold; + } + params +} + pub const LLMQ_50_60: LLMQParams = LLMQParams { quorum_type: LLMQType::Llmqtype50_60, name: "llmq_50_60", @@ -358,14 +380,14 @@ impl LLMQType { LLMQType::Llmqtype60_75 => LLMQ_60_75, LLMQType::Llmqtype25_67 => LLMQ_25_67, LLMQType::LlmqtypeTest => LLMQ_TEST, - LLMQType::LlmqtypeDevnet => LLMQ_DEVNET, + LLMQType::LlmqtypeDevnet => llmq_devnet_params(), LLMQType::LlmqtypeTestV17 => LLMQ_V017, LLMQType::LlmqtypeTestDIP0024 => LLMQ_TEST_DIP00024, LLMQType::LlmqtypeTestInstantSend => LLMQ_TEST_INSTANT_SEND, LLMQType::LlmqtypeDevnetDIP0024 => LLMQ_0024, LLMQType::LlmqtypeTestnetPlatform => LLMQ_TEST_PLATFORM, LLMQType::LlmqtypeDevnetPlatform => LLMQ_DEV_PLATFORM, - LLMQType::LlmqtypeUnknown => LLMQ_DEVNET, + LLMQType::LlmqtypeUnknown => llmq_devnet_params(), } } pub fn size(&self) -> u32 { From 4d5e0f66fd7eea6533bd2b8a53775f97b4636f37 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Thu, 28 May 2026 00:25:14 +1000 Subject: [PATCH 6/8] fix(dash): make `set_llmq_devnet_params` idempotent for identical values The `apply_global_overrides` doc claimed idempotent behavior, but `OnceLock::set` rejected every second call regardless of value. Constructing multiple `DashSpvClient` instances with the same devnet config in one process therefore failed on the second call. Now the setter compares against the existing value and returns `Ok(())` when it matches, only erroring on a genuine conflict. Addresses CodeRabbit review comment on PR #784 https://github.com/dashpay/rust-dashcore/pull/784#discussion_r3311404218 --- dash/src/sml/llmq_type/mod.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/dash/src/sml/llmq_type/mod.rs b/dash/src/sml/llmq_type/mod.rs index 32a8cefcf..a54d7d3af 100644 --- a/dash/src/sml/llmq_type/mod.rs +++ b/dash/src/sml/llmq_type/mod.rs @@ -212,9 +212,16 @@ pub const LLMQ_DEVNET: LLMQParams = LLMQParams { static LLMQ_DEVNET_OVERRIDE: OnceLock<(u32, u32)> = OnceLock::new(); /// Override the `LLMQ_DEVNET` quorum size and threshold (matches Dash Core's -/// `-llmqdevnetparams=:`). May only be called once per process. +/// `-llmqdevnetparams=:`). Idempotent for identical values, +/// returns an error if a conflicting override was already set. pub fn set_llmq_devnet_params(size: u32, threshold: u32) -> Result<(), &'static str> { - LLMQ_DEVNET_OVERRIDE.set((size, threshold)).map_err(|_| "LLMQ_DEVNET params already set") + match LLMQ_DEVNET_OVERRIDE.get() { + Some(&existing) if existing == (size, threshold) => Ok(()), + Some(_) => Err("LLMQ_DEVNET params already set to a different value"), + None => LLMQ_DEVNET_OVERRIDE + .set((size, threshold)) + .map_err(|_| "LLMQ_DEVNET params already set to a different value"), + } } /// Get the effective `LLMQ_DEVNET` params, applying any runtime override. From e3c800211b48c8d2dd8f1cdb96dec32fd8f66bf3 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Thu, 28 May 2026 00:25:41 +1000 Subject: [PATCH 7/8] test(dash): cover `LLMQ_DEVNET` override contract Add a single test that walks through valid initial set, idempotent reapplication of the same values, and rejection of a conflicting reapplication. The three checks share one test because `LLMQ_DEVNET_OVERRIDE` is a process-global `OnceLock` that cannot be reset between tests. Addresses CodeRabbit review comment on PR #784 https://github.com/dashpay/rust-dashcore/pull/784#discussion_r3311404240 --- dash/src/sml/llmq_type/mod.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/dash/src/sml/llmq_type/mod.rs b/dash/src/sml/llmq_type/mod.rs index a54d7d3af..b40c25ce1 100644 --- a/dash/src/sml/llmq_type/mod.rs +++ b/dash/src/sml/llmq_type/mod.rs @@ -690,4 +690,25 @@ mod tests { assert_eq!(params.threshold, 67); assert_eq!(params.signing_active_quorum_count, 24); } + + #[test] + fn test_llmq_devnet_override_lifecycle() { + // LLMQ_DEVNET_OVERRIDE is a process-global OnceLock, so the three contract + // checks (initial set, idempotent re-set, conflicting re-set) all run in + // this single test to avoid races between tests sharing the same lock. + set_llmq_devnet_params(8, 5).expect("initial override should succeed"); + + let params = llmq_devnet_params(); + assert_eq!(params.size, 8); + assert_eq!(params.min_size, 5); + assert_eq!(params.threshold, 5); + assert_eq!(params.dkg_params.bad_votes_threshold, 5); + + set_llmq_devnet_params(8, 5).expect("re-setting identical values should be idempotent"); + assert!(set_llmq_devnet_params(12, 6).is_err(), "conflicting override must error"); + + let params_after = llmq_devnet_params(); + assert_eq!(params_after.size, 8); + assert_eq!(params_after.threshold, 5); + } } From a5b52046109f3092523ff0c167bcc8660f283e6a Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 27 May 2026 19:19:14 +0200 Subject: [PATCH 8/8] refactor(dash): use `LlmqDevnetParams` struct instead of `(u32, u32)` tuple MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Option<(u32, u32)>` was unreadable at call sites — `with_llmq_devnet_params(13, 9)` is one transposition away from a silent bug. Introduce a named struct in `dashcore::sml::llmq_type` so the size/threshold meaning is explicit everywhere it crosses an API boundary. Co-Authored-By: Claude Opus 4.7 (1M context) --- dash-spv/src/client/config.rs | 12 ++++----- dash-spv/src/main.rs | 13 ++++++++-- dash/src/sml/llmq_type/mod.rs | 46 +++++++++++++++++++++++++++++------ 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/dash-spv/src/client/config.rs b/dash-spv/src/client/config.rs index d0e27b6e5..23849f066 100644 --- a/dash-spv/src/client/config.rs +++ b/dash-spv/src/client/config.rs @@ -4,7 +4,7 @@ use clap::ValueEnum; use std::net::SocketAddr; use std::path::PathBuf; -use dashcore::sml::llmq_type::set_llmq_devnet_params; +use dashcore::sml::llmq_type::{set_llmq_devnet_params, LlmqDevnetParams}; use dashcore::Network; // Serialization removed due to complex Address types @@ -74,7 +74,7 @@ pub struct ClientConfig { /// Override for `LLMQ_DEVNET` quorum size and threshold, applied at startup. /// Mirrors Dash Core's `-llmqdevnetparams=:`. Only meaningful on devnet. - pub llmq_devnet_params: Option<(u32, u32)>, + pub llmq_devnet_params: Option, } impl Default for ClientConfig { @@ -189,8 +189,8 @@ impl ClientConfig { /// Override `LLMQ_DEVNET` quorum size and threshold for a devnet. /// Mirrors Dash Core's `-llmqdevnetparams=:`. - pub fn with_llmq_devnet_params(mut self, size: u32, threshold: u32) -> Self { - self.llmq_devnet_params = Some((size, threshold)); + pub fn with_llmq_devnet_params(mut self, params: LlmqDevnetParams) -> Self { + self.llmq_devnet_params = Some(params); self } @@ -226,8 +226,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((size, threshold)) = self.llmq_devnet_params { - set_llmq_devnet_params(size, threshold).map_err(|e| e.to_string())?; + if let Some(params) = self.llmq_devnet_params { + set_llmq_devnet_params(params).map_err(|e| e.to_string())?; } Ok(()) } diff --git a/dash-spv/src/main.rs b/dash-spv/src/main.rs index a9e3f0da8..22e64f236 100644 --- a/dash-spv/src/main.rs +++ b/dash-spv/src/main.rs @@ -6,6 +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 key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; use key_wallet_manager::WalletManager; @@ -240,8 +241,16 @@ async fn run() -> Result<(), Box> { let threshold: u32 = threshold_str .parse() .map_err(|e| format!("invalid LLMQ_DEVNET threshold '{}': {}", threshold_str, e))?; - config = config.with_llmq_devnet_params(size, threshold); - tracing::info!("LLMQ_DEVNET params overridden: size={} threshold={}", size, threshold); + let params = LlmqDevnetParams { + size, + threshold, + }; + config = config.with_llmq_devnet_params(params); + tracing::info!( + "LLMQ_DEVNET params overridden: size={} threshold={}", + params.size, + params.threshold + ); } } else { if args.devnet_name.is_some() { diff --git a/dash/src/sml/llmq_type/mod.rs b/dash/src/sml/llmq_type/mod.rs index b40c25ce1..573804e14 100644 --- a/dash/src/sml/llmq_type/mod.rs +++ b/dash/src/sml/llmq_type/mod.rs @@ -208,18 +208,29 @@ pub const LLMQ_DEVNET: LLMQParams = LLMQParams { recovery_members: 6, }; +/// Runtime override values for `LLMQ_DEVNET`, matching Dash Core's +/// `-llmqdevnetparams=:`. +#[repr(C)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct LlmqDevnetParams { + /// Quorum size (total members). + pub size: u32, + /// Signing threshold (also used as min_size and bad_votes_threshold). + pub threshold: u32, +} + /// Runtime override for `LLMQ_DEVNET` params, matching Dash Core's `-llmqdevnetparams`. -static LLMQ_DEVNET_OVERRIDE: OnceLock<(u32, u32)> = OnceLock::new(); +static LLMQ_DEVNET_OVERRIDE: OnceLock = OnceLock::new(); /// Override the `LLMQ_DEVNET` quorum size and threshold (matches Dash Core's /// `-llmqdevnetparams=:`). Idempotent for identical values, /// returns an error if a conflicting override was already set. -pub fn set_llmq_devnet_params(size: u32, threshold: u32) -> Result<(), &'static str> { +pub fn set_llmq_devnet_params(params: LlmqDevnetParams) -> Result<(), &'static str> { match LLMQ_DEVNET_OVERRIDE.get() { - Some(&existing) if existing == (size, threshold) => Ok(()), + Some(&existing) if existing == params => Ok(()), Some(_) => Err("LLMQ_DEVNET params already set to a different value"), None => LLMQ_DEVNET_OVERRIDE - .set((size, threshold)) + .set(params) .map_err(|_| "LLMQ_DEVNET params already set to a different value"), } } @@ -227,7 +238,11 @@ pub fn set_llmq_devnet_params(size: u32, threshold: u32) -> Result<(), &'static /// Get the effective `LLMQ_DEVNET` params, applying any runtime override. pub fn llmq_devnet_params() -> LLMQParams { let mut params = LLMQ_DEVNET; - if let Some(&(size, threshold)) = LLMQ_DEVNET_OVERRIDE.get() { + if let Some(&LlmqDevnetParams { + size, + threshold, + }) = LLMQ_DEVNET_OVERRIDE.get() + { params.size = size; params.min_size = threshold; params.threshold = threshold; @@ -696,7 +711,11 @@ mod tests { // LLMQ_DEVNET_OVERRIDE is a process-global OnceLock, so the three contract // checks (initial set, idempotent re-set, conflicting re-set) all run in // this single test to avoid races between tests sharing the same lock. - set_llmq_devnet_params(8, 5).expect("initial override should succeed"); + set_llmq_devnet_params(LlmqDevnetParams { + size: 8, + threshold: 5, + }) + .expect("initial override should succeed"); let params = llmq_devnet_params(); assert_eq!(params.size, 8); @@ -704,8 +723,19 @@ mod tests { assert_eq!(params.threshold, 5); assert_eq!(params.dkg_params.bad_votes_threshold, 5); - set_llmq_devnet_params(8, 5).expect("re-setting identical values should be idempotent"); - assert!(set_llmq_devnet_params(12, 6).is_err(), "conflicting override must error"); + set_llmq_devnet_params(LlmqDevnetParams { + size: 8, + threshold: 5, + }) + .expect("re-setting identical values should be idempotent"); + assert!( + set_llmq_devnet_params(LlmqDevnetParams { + size: 12, + threshold: 6, + }) + .is_err(), + "conflicting override must error" + ); let params_after = llmq_devnet_params(); assert_eq!(params_after.size, 8);