From 1de715f7edc1481ab62ca948684598d2e5372c51 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 20 Nov 2025 18:55:07 +0700 Subject: [PATCH 1/8] feat(spv): add QuorumLookup component for thread-safe quorum queries --- dash-spv/src/client/chainlock.rs | 9 +- dash-spv/src/client/core.rs | 42 +++- dash-spv/src/client/lifecycle.rs | 6 +- dash-spv/src/client/mod.rs | 2 + dash-spv/src/client/queries.rs | 84 +++---- dash-spv/src/client/quorum_lookup.rs | 314 +++++++++++++++++++++++++++ 6 files changed, 396 insertions(+), 61 deletions(-) create mode 100644 dash-spv/src/client/quorum_lookup.rs diff --git a/dash-spv/src/client/chainlock.rs b/dash-spv/src/client/chainlock.rs index 75df8a2ec..3d57c01f2 100644 --- a/dash-spv/src/client/chainlock.rs +++ b/dash-spv/src/client/chainlock.rs @@ -137,12 +137,17 @@ impl< pub fn update_chainlock_validation(&self) -> Result { // Check if masternode sync has an engine available if let Some(engine) = self.sync_manager.get_masternode_engine() { - // Clone the engine for the ChainLockManager + // Clone the engine for the ChainLockManager and QuorumLookup let engine_arc = Arc::new(engine.clone()); - self.chainlock_manager.set_masternode_engine(engine_arc); + // Update ChainLockManager for ChainLock validation + self.chainlock_manager.set_masternode_engine(engine_arc.clone()); tracing::info!("Updated ChainLockManager with masternode engine for full validation"); + // Update QuorumLookup for quorum queries + self.quorum_lookup.set_engine(engine_arc); + tracing::info!("Updated QuorumLookup with masternode engine for quorum queries"); + // Note: Pending ChainLocks will be validated when they are next processed // or can be triggered by calling validate_pending_chainlocks separately // when mutable access to storage is available diff --git a/dash-spv/src/client/core.rs b/dash-spv/src/client/core.rs index 30b5fcd63..a0a96faed 100644 --- a/dash-spv/src/client/core.rs +++ b/dash-spv/src/client/core.rs @@ -25,7 +25,7 @@ use crate::types::{ChainState, DetailedSyncProgress, MempoolState, SpvEvent, Spv use crate::validation::ValidationManager; use key_wallet_manager::wallet_interface::WalletInterface; -use super::{BlockProcessingTask, ClientConfig, StatusDisplay}; +use super::{BlockProcessingTask, ClientConfig, QuorumLookup, StatusDisplay}; /// Main Dash SPV client with generic trait-based architecture. /// @@ -135,6 +135,14 @@ pub struct DashSpvClient, pub(super) validation: ValidationManager, pub(super) chainlock_manager: Arc, + /// Thread-safe quorum and masternode lookup interface. + /// + /// This component provides shared access to masternode lists and quorum data + /// without requiring exclusive access to the sync_manager. Applications can + /// clone the Arc and perform queries from multiple threads. + /// + /// See `QuorumLookup` documentation for usage examples. + pub(super) quorum_lookup: Arc, pub(super) running: Arc>, #[cfg(feature = "terminal-ui")] pub(super) terminal_ui: Option>, @@ -177,6 +185,38 @@ impl< &self.chainlock_manager } + /// Get reference to quorum lookup component. + /// + /// This component provides thread-safe access to masternode lists and quorum data. + /// The returned `Arc` can be cheaply cloned and shared across threads. + /// + /// # Example + /// + /// ```rust,no_run + /// # use dash_spv::client::DashSpvClient; + /// # async fn example(client: &DashSpvClient< + /// # key_wallet_manager::wallet_manager::WalletManager, + /// # dash_spv::network::manager::PeerNetworkManager, + /// # dash_spv::storage::DiskStorageManager + /// # >) { + /// // Get the quorum lookup component + /// let quorum_lookup = client.quorum_lookup(); + /// + /// // Clone for use in another thread/task + /// let lookup_clone = quorum_lookup.clone(); + /// + /// // Use in async task + /// tokio::spawn(async move { + /// if let Some(quorum) = lookup_clone.get_quorum_at_height(100000, 1, &[0u8; 32]).await { + /// println!("Found quorum!"); + /// } + /// }); + /// # } + /// ``` + pub fn quorum_lookup(&self) -> &Arc { + &self.quorum_lookup + } + /// Get mutable reference to sync manager (for testing). #[cfg(test)] pub fn sync_manager_mut(&mut self) -> &mut SequentialSyncManager { diff --git a/dash-spv/src/client/lifecycle.rs b/dash-spv/src/client/lifecycle.rs index c7c0ae43a..dcaf8fe07 100644 --- a/dash-spv/src/client/lifecycle.rs +++ b/dash-spv/src/client/lifecycle.rs @@ -23,7 +23,7 @@ use crate::validation::ValidationManager; use dashcore::network::constants::NetworkExt; use key_wallet_manager::wallet_interface::WalletInterface; -use super::{BlockProcessor, ClientConfig, DashSpvClient}; +use super::{BlockProcessor, ClientConfig, DashSpvClient, QuorumLookup}; impl< W: WalletInterface + Send + Sync + 'static, @@ -66,6 +66,9 @@ impl< // Create ChainLock manager let chainlock_manager = Arc::new(ChainLockManager::new(true)); + // Create quorum lookup component + let quorum_lookup = Arc::new(QuorumLookup::new()); + // Create block processing channel let (block_processor_tx, _block_processor_rx) = mpsc::unbounded_channel(); @@ -88,6 +91,7 @@ impl< sync_manager, validation, chainlock_manager, + quorum_lookup, running: Arc::new(RwLock::new(false)), #[cfg(feature = "terminal-ui")] terminal_ui: None, diff --git a/dash-spv/src/client/mod.rs b/dash-spv/src/client/mod.rs index 1e8c2c0a9..6a60bebda 100644 --- a/dash-spv/src/client/mod.rs +++ b/dash-spv/src/client/mod.rs @@ -48,6 +48,7 @@ mod lifecycle; mod mempool; mod progress; mod queries; +mod quorum_lookup; mod sync_coordinator; mod transactions; @@ -56,6 +57,7 @@ pub use block_processor::{BlockProcessingTask, BlockProcessor}; pub use config::ClientConfig; pub use filter_sync::FilterSyncCoordinator; pub use message_handler::MessageHandler; +pub use quorum_lookup::QuorumLookup; pub use status_display::StatusDisplay; // Re-export the main client struct diff --git a/dash-spv/src/client/queries.rs b/dash-spv/src/client/queries.rs index 8e21f796c..22f1c204a 100644 --- a/dash-spv/src/client/queries.rs +++ b/dash-spv/src/client/queries.rs @@ -70,68 +70,38 @@ impl< /// Get a quorum entry by type and hash at a specific block height. /// Returns None if the quorum is not found. + /// + /// # Deprecated + /// + /// This method is now a synchronous wrapper that blocks on the async QuorumLookup. + /// For better performance and to avoid blocking, prefer using the async version: + /// + /// ```rust,no_run + /// # use dash_spv::client::DashSpvClient; + /// # async fn example(client: &DashSpvClient< + /// # key_wallet_manager::wallet_manager::WalletManager, + /// # dash_spv::network::manager::PeerNetworkManager, + /// # dash_spv::storage::DiskStorageManager + /// # >) { + /// let quorum_lookup = client.quorum_lookup(); + /// if let Some(quorum) = quorum_lookup.get_quorum_at_height(100000, 1, &[0u8; 32]).await { + /// println!("Found quorum!"); + /// } + /// # } + /// ``` pub fn get_quorum_at_height( &self, height: u32, quorum_type: u8, quorum_hash: &[u8; 32], - ) -> Option<&QualifiedQuorumEntry> { - use dashcore::sml::llmq_type::LLMQType; - use dashcore::QuorumHash; - use dashcore_hashes::Hash; - - let llmq_type: LLMQType = LLMQType::from(quorum_type); - if llmq_type == LLMQType::LlmqtypeUnknown { - tracing::warn!("Invalid quorum type {} requested at height {}", quorum_type, height); - return None; - }; - - let qhash = QuorumHash::from_byte_array(*quorum_hash); - - // First check if we have the masternode list at this height - match self.get_masternode_list_at_height(height) { - Some(ml) => { - // We have the masternode list, now look for the quorum - match ml.quorums.get(&llmq_type) { - Some(quorums) => match quorums.get(&qhash) { - Some(quorum) => { - tracing::debug!( - "Found quorum type {} at height {} with hash {}", - quorum_type, - height, - hex::encode(quorum_hash) - ); - Some(quorum) - } - None => { - tracing::warn!( - "Quorum not found: type {} at height {} with hash {} (masternode list exists with {} quorums of this type)", - quorum_type, - height, - hex::encode(quorum_hash), - quorums.len() - ); - None - } - }, - None => { - tracing::warn!( - "No quorums of type {} found at height {} (masternode list exists)", - quorum_type, - height - ); - None - } - } - } - None => { - tracing::warn!( - "No masternode list found at height {} - cannot retrieve quorum", - height - ); - None - } - } + ) -> Option { + // Delegate to the QuorumLookup component + // This requires blocking on the async call + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + self.quorum_lookup.get_quorum_at_height(height, quorum_type, quorum_hash).await + }) + }) } // ============ Balance Queries ============ diff --git a/dash-spv/src/client/quorum_lookup.rs b/dash-spv/src/client/quorum_lookup.rs new file mode 100644 index 000000000..15bde5a15 --- /dev/null +++ b/dash-spv/src/client/quorum_lookup.rs @@ -0,0 +1,314 @@ +//! Thread-safe quorum and masternode lookup component. +//! +//! This module provides a shareable interface for querying masternode lists and quorums +//! without requiring exclusive access to the DashSpvClient. This solves the architectural +//! challenge where the sync_manager (which owns the masternode engine) is not shareable, +//! but applications need to perform quorum lookups from multiple threads. +//! +//! ## Design Philosophy +//! +//! The `QuorumLookup` component provides read-only access to masternode and quorum data +//! through a thread-safe `Arc`-wrapped interface. This allows: +//! +//! 1. **Shared Access**: Multiple parts of an application can clone `Arc` +//! and perform concurrent queries without blocking each other. +//! +//! 2. **Separation of Concerns**: Query operations are separated from sync operations, +//! maintaining the single-owner pattern for the sync manager. +//! +//! 3. **Non-blocking Reads**: Uses `RwLock` internally to allow multiple concurrent readers +//! while ensuring consistency during masternode list updates. +//! +//! ## Usage Example +//! +//! ```rust,no_run +//! # use dash_spv::client::DashSpvClient; +//! # use std::sync::Arc; +//! # async fn example(client: &DashSpvClient< +//! # key_wallet_manager::wallet_manager::WalletManager, +//! # dash_spv::network::manager::PeerNetworkManager, +//! # dash_spv::storage::DiskStorageManager +//! # >) { +//! // Get the shared quorum lookup component +//! let quorum_lookup = client.quorum_lookup(); +//! +//! // Clone it for use in another thread/task +//! let lookup_clone = quorum_lookup.clone(); +//! +//! // Perform queries +//! if let Some(quorum) = quorum_lookup.get_quorum_at_height( +//! 100000, +//! 1, // LLMQ_TYPE_50_60 +//! &[0u8; 32] +//! ).await { +//! println!("Found quorum with {} members", quorum.quorum_public_key.len()); +//! } +//! # } +//! ``` + +use dashcore::sml::llmq_type::LLMQType; +use dashcore::sml::masternode_list::MasternodeList; +use dashcore::sml::masternode_list_engine::MasternodeListEngine; +use dashcore::sml::quorum_entry::qualified_quorum_entry::QualifiedQuorumEntry; +use dashcore::QuorumHash; +use dashcore_hashes::Hash; +use std::sync::{Arc, RwLock}; +use tracing::{debug, warn}; + +/// Thread-safe component for querying masternode lists and quorums. +/// +/// This struct wraps the masternode list engine in a thread-safe manner, +/// allowing multiple threads to perform read-only queries concurrently. +/// +/// ## Thread Safety +/// +/// Uses `Arc>>>` to provide: +/// - **Multiple concurrent readers**: Many threads can query simultaneously +/// - **Exclusive writer**: Only the sync manager updates the engine +/// - **Interior mutability**: The outer `Arc` allows cheap cloning for sharing +/// +/// ## Performance Considerations +/// +/// - Cloning `QuorumLookup` is cheap (just clones the `Arc`) +/// - Read queries acquire a read lock (non-blocking for other readers) +/// - The engine itself is wrapped in an inner `Arc` to avoid deep copying +pub struct QuorumLookup { + /// The masternode list engine, wrapped for thread-safe access. + /// + /// - Outer `Arc`: Allows cheap cloning of QuorumLookup + /// - `RwLock`: Provides concurrent read access, exclusive write access + /// - Inner `Option>`: Engine may not be available before masternode sync + engine: Arc>>>, +} + +impl QuorumLookup { + /// Create a new QuorumLookup with no engine (before masternode sync). + pub fn new() -> Self { + Self { + engine: Arc::new(RwLock::new(None)), + } + } + + /// Create a QuorumLookup with an existing engine. + pub fn with_engine(engine: Arc) -> Self { + Self { + engine: Arc::new(RwLock::new(Some(engine))), + } + } + + /// Update the masternode list engine. + /// + /// This method is called by the sync manager when masternode data becomes available. + /// It uses a write lock to ensure exclusive access during updates. + /// + /// ## Panics + /// + /// Panics if the RwLock is poisoned (which should never happen in normal operation). + pub fn set_engine(&self, engine: Arc) { + let mut guard = self.engine.write().expect("QuorumLookup RwLock should not be poisoned"); + *guard = Some(engine); + debug!("Masternode engine updated in QuorumLookup"); + } + + /// Get a reference to the masternode list engine if available. + /// + /// Returns `None` if masternode sync hasn't completed yet. + /// + /// ## Panics + /// + /// Panics if the RwLock is poisoned. + pub fn engine(&self) -> Option> { + let guard = self.engine.read().expect("QuorumLookup RwLock should not be poisoned"); + guard.clone() + } + + /// Get the masternode list at a specific block height. + /// + /// Returns `None` if: + /// - Masternode sync is not yet complete + /// - The masternode list for that height is not available + /// + /// ## Example + /// + /// ```rust,no_run + /// # use dash_spv::client::QuorumLookup; + /// # async fn example(lookup: &QuorumLookup) { + /// if let Some(ml) = lookup.get_masternode_list_at_height(100000).await { + /// println!("Masternode list has {} masternodes", ml.masternodes.len()); + /// } + /// # } + /// ``` + pub async fn get_masternode_list_at_height(&self, height: u32) -> Option { + let engine = self.engine()?; + + // Clone the masternode list to avoid holding the lock + engine.masternode_lists.get(&height).cloned() + } + + /// Get a quorum entry by type and hash at a specific block height. + /// + /// This is the core method for quorum lookups, used by applications to retrieve + /// quorum public keys and other quorum information needed for validation. + /// + /// ## Parameters + /// + /// - `height`: Block height at which to query the masternode list + /// - `quorum_type`: LLMQ type (e.g., 1 for LLMQ_TYPE_50_60, 4 for LLMQ_TYPE_400_60) + /// - `quorum_hash`: 32-byte hash identifying the specific quorum + /// + /// ## Returns + /// + /// - `Some(quorum)`: If the quorum is found + /// - `None`: If masternode sync incomplete, no list at height, or quorum not found + /// + /// ## Example + /// + /// ```rust,no_run + /// # use dash_spv::client::QuorumLookup; + /// # async fn example(lookup: &QuorumLookup) { + /// let quorum_hash = [0u8; 32]; // Your quorum hash here + /// if let Some(quorum) = lookup.get_quorum_at_height( + /// 100000, + /// 1, // LLMQ_TYPE_50_60 + /// &quorum_hash + /// ).await { + /// println!("Found quorum with public key: {:?}", quorum.quorum_public_key); + /// } else { + /// println!("Quorum not found"); + /// } + /// # } + /// ``` + pub async fn get_quorum_at_height( + &self, + height: u32, + quorum_type: u8, + quorum_hash: &[u8; 32], + ) -> Option { + // Convert quorum type to LLMQType + let llmq_type: LLMQType = LLMQType::from(quorum_type); + if llmq_type == LLMQType::LlmqtypeUnknown { + warn!("Invalid quorum type {} requested at height {}", quorum_type, height); + return None; + } + + // Convert hash + let qhash = QuorumHash::from_byte_array(*quorum_hash); + + // Get the masternode list at this height + let ml = self.get_masternode_list_at_height(height).await?; + + // Look for the quorum in the masternode list + match ml.quorums.get(&llmq_type) { + Some(quorums) => match quorums.get(&qhash) { + Some(quorum) => { + debug!( + "Found quorum type {} at height {} with hash {}", + quorum_type, + height, + hex::encode(quorum_hash) + ); + Some(quorum.clone()) + } + None => { + warn!( + "Quorum not found: type {} at height {} with hash {} (masternode list exists with {} quorums of this type)", + quorum_type, + height, + hex::encode(quorum_hash), + quorums.len() + ); + None + } + }, + None => { + warn!( + "No quorums of type {} found at height {} (masternode list exists)", + quorum_type, + height + ); + None + } + } + } + + /// Check if the masternode engine is available. + /// + /// Returns `true` if masternode sync has completed and the engine is ready for queries. + pub fn is_available(&self) -> bool { + self.engine().is_some() + } + + /// Get the number of masternode lists currently stored in the engine. + /// + /// Returns `0` if the engine is not yet available. + /// + /// This can be useful for monitoring sync progress or debugging. + pub fn masternode_list_count(&self) -> usize { + self.engine() + .map(|engine| engine.masternode_lists.len()) + .unwrap_or(0) + } + + /// Get the height range of available masternode lists. + /// + /// Returns `None` if no masternode lists are available yet. + /// Returns `Some((min_height, max_height))` if lists are available. + pub fn masternode_list_height_range(&self) -> Option<(u32, u32)> { + let engine = self.engine()?; + + let heights: Vec = engine.masternode_lists.keys().copied().collect(); + if heights.is_empty() { + return None; + } + + let min = heights.iter().min().copied()?; + let max = heights.iter().max().copied()?; + Some((min, max)) + } +} + +impl Clone for QuorumLookup { + fn clone(&self) -> Self { + Self { + engine: Arc::clone(&self.engine), + } + } +} + +impl Default for QuorumLookup { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_quorum_lookup_new() { + let lookup = QuorumLookup::new(); + assert!(!lookup.is_available()); + assert_eq!(lookup.masternode_list_count(), 0); + assert_eq!(lookup.masternode_list_height_range(), None); + } + + #[tokio::test] + async fn test_quorum_lookup_clone() { + let lookup1 = QuorumLookup::new(); + let lookup2 = lookup1.clone(); + + // Both should see the same state + assert!(!lookup1.is_available()); + assert!(!lookup2.is_available()); + } + + #[tokio::test] + async fn test_quorum_lookup_before_sync() { + let lookup = QuorumLookup::new(); + + // Queries should return None before engine is set + assert!(lookup.get_masternode_list_at_height(100).await.is_none()); + assert!(lookup.get_quorum_at_height(100, 1, &[0u8; 32]).await.is_none()); + } +} From 3babcb112e9c185515bae75619d80f75045fef1c Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 20 Nov 2025 20:55:48 +0700 Subject: [PATCH 2/8] fmt --- dash-spv/src/client/quorum_lookup.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/dash-spv/src/client/quorum_lookup.rs b/dash-spv/src/client/quorum_lookup.rs index 15bde5a15..0c90aeeeb 100644 --- a/dash-spv/src/client/quorum_lookup.rs +++ b/dash-spv/src/client/quorum_lookup.rs @@ -223,8 +223,7 @@ impl QuorumLookup { None => { warn!( "No quorums of type {} found at height {} (masternode list exists)", - quorum_type, - height + quorum_type, height ); None } @@ -244,9 +243,7 @@ impl QuorumLookup { /// /// This can be useful for monitoring sync progress or debugging. pub fn masternode_list_count(&self) -> usize { - self.engine() - .map(|engine| engine.masternode_lists.len()) - .unwrap_or(0) + self.engine().map(|engine| engine.masternode_lists.len()).unwrap_or(0) } /// Get the height range of available masternode lists. From cabc0287dbad7540ea2a77f8179aaff9f6d874b0 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 20 Nov 2025 21:12:12 +0700 Subject: [PATCH 3/8] fix --- dash-spv/src/client/quorum_lookup.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash-spv/src/client/quorum_lookup.rs b/dash-spv/src/client/quorum_lookup.rs index 0c90aeeeb..056d1a0c8 100644 --- a/dash-spv/src/client/quorum_lookup.rs +++ b/dash-spv/src/client/quorum_lookup.rs @@ -41,7 +41,7 @@ //! 1, // LLMQ_TYPE_50_60 //! &[0u8; 32] //! ).await { -//! println!("Found quorum with {} members", quorum.quorum_public_key.len()); +//! println!("Found quorum with {} members", quorum.quorum_entry.quorum_public_key.len()); //! } //! # } //! ``` @@ -172,7 +172,7 @@ impl QuorumLookup { /// 1, // LLMQ_TYPE_50_60 /// &quorum_hash /// ).await { - /// println!("Found quorum with public key: {:?}", quorum.quorum_public_key); + /// println!("Found quorum with public key: {:?}", quorum.quorum_entry.quorum_public_key); /// } else { /// println!("Quorum not found"); /// } From 739b68be47e484935bbcfb388002dadc11564094 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Fri, 21 Nov 2025 17:01:51 +0700 Subject: [PATCH 4/8] fixes --- dash-spv/src/client/queries.rs | 3 +- dash-spv/src/client/quorum_lookup.rs | 129 ++++++++++++++++++++++----- 2 files changed, 109 insertions(+), 23 deletions(-) diff --git a/dash-spv/src/client/queries.rs b/dash-spv/src/client/queries.rs index 22f1c204a..bd9f60170 100644 --- a/dash-spv/src/client/queries.rs +++ b/dash-spv/src/client/queries.rs @@ -84,7 +84,8 @@ impl< /// # dash_spv::storage::DiskStorageManager /// # >) { /// let quorum_lookup = client.quorum_lookup(); - /// if let Some(quorum) = quorum_lookup.get_quorum_at_height(100000, 1, &[0u8; 32]).await { + /// let quorum_hash = [0u8; 32]; // Placeholder - use actual hash + /// if let Some(quorum) = quorum_lookup.get_quorum_at_height(100000, 1, &quorum_hash).await { /// println!("Found quorum!"); /// } /// # } diff --git a/dash-spv/src/client/quorum_lookup.rs b/dash-spv/src/client/quorum_lookup.rs index 056d1a0c8..8115e959c 100644 --- a/dash-spv/src/client/quorum_lookup.rs +++ b/dash-spv/src/client/quorum_lookup.rs @@ -194,13 +194,16 @@ impl QuorumLookup { // Convert hash let qhash = QuorumHash::from_byte_array(*quorum_hash); - // Get the masternode list at this height - let ml = self.get_masternode_list_at_height(height).await?; + // Get the engine + let engine = self.engine()?; - // Look for the quorum in the masternode list - match ml.quorums.get(&llmq_type) { - Some(quorums) => match quorums.get(&qhash) { - Some(quorum) => { + // Look up the masternode list and quorum + let quorum = engine + .masternode_lists + .get(&height) + .and_then(|ml| ml.quorums.get(&llmq_type)) + .and_then(|quorums| { + if let Some(quorum) = quorums.get(&qhash) { debug!( "Found quorum type {} at height {} with hash {}", quorum_type, @@ -208,8 +211,7 @@ impl QuorumLookup { hex::encode(quorum_hash) ); Some(quorum.clone()) - } - None => { + } else { warn!( "Quorum not found: type {} at height {} with hash {} (masternode list exists with {} quorums of this type)", quorum_type, @@ -219,15 +221,19 @@ impl QuorumLookup { ); None } - }, - None => { - warn!( - "No quorums of type {} found at height {} (masternode list exists)", - quorum_type, height - ); - None - } + }); + + if quorum.is_none() && engine.masternode_lists.contains_key(&height) { + // We have the masternode list but not this quorum type + warn!( + "No quorums of type {} found at height {} (masternode list exists)", + quorum_type, height + ); + } else if quorum.is_none() { + warn!("No masternode list found at height {} - cannot retrieve quorum", height); } + + quorum } /// Check if the masternode engine is available. @@ -253,14 +259,19 @@ impl QuorumLookup { pub fn masternode_list_height_range(&self) -> Option<(u32, u32)> { let engine = self.engine()?; - let heights: Vec = engine.masternode_lists.keys().copied().collect(); - if heights.is_empty() { - return None; + // Compute min/max + let mut min: Option = None; + let mut max: Option = None; + + for &height in engine.masternode_lists.keys() { + min = Some(min.map_or(height, |m| m.min(height))); + max = Some(max.map_or(height, |m| m.max(height))); } - let min = heights.iter().min().copied()?; - let max = heights.iter().max().copied()?; - Some((min, max)) + match (min, max) { + (Some(min_height), Some(max_height)) => Some((min_height, max_height)), + _ => None, + } } } @@ -308,4 +319,78 @@ mod tests { assert!(lookup.get_masternode_list_at_height(100).await.is_none()); assert!(lookup.get_quorum_at_height(100, 1, &[0u8; 32]).await.is_none()); } + + #[tokio::test] + async fn test_quorum_lookup_with_engine() { + use dashcore::sml::masternode_list_engine::MasternodeListEngine; + use dashcore::Network; + + // Create a test engine using the proper constructor + let engine = MasternodeListEngine::default_for_network(Network::Dash); + + // Create QuorumLookup and set the engine + let lookup = QuorumLookup::with_engine(Arc::new(engine)); + + // Verify engine is available (even though it has no data yet) + assert!(lookup.is_available()); + assert_eq!(lookup.masternode_list_count(), 0); + assert_eq!(lookup.masternode_list_height_range(), None); + + // Query for a non-existent masternode list - should return None + let ml_result = lookup.get_masternode_list_at_height(1000).await; + assert!(ml_result.is_none()); + + // Query for a non-existent quorum - should return None + let quorum_result = lookup.get_quorum_at_height(1000, 1, &[1u8; 32]).await; + assert!(quorum_result.is_none()); + + // Verify that even with an empty engine, the lookup works without panicking + assert!(lookup.is_available()); + } + + #[tokio::test] + async fn test_quorum_lookup_concurrent_access() { + use dashcore::sml::masternode_list_engine::MasternodeListEngine; + use dashcore::Network; + + // Create a QuorumLookup with an engine + let engine = MasternodeListEngine::default_for_network(Network::Dash); + let lookup = Arc::new(QuorumLookup::with_engine(Arc::new(engine))); + + // Spawn multiple tasks that query concurrently + let mut handles = vec![]; + for _ in 0..10 { + let lookup_clone = lookup.clone(); + let handle = tokio::spawn(async move { + // All queries should return None (no data in engine) + // but shouldn't panic or deadlock + let _ = lookup_clone.get_quorum_at_height(100, 1, &[0u8; 32]).await; + let _ = lookup_clone.is_available(); + let _ = lookup_clone.masternode_list_count(); + }); + handles.push(handle); + } + + // Wait for all tasks to complete + for handle in handles { + handle.await.expect("Task should not panic"); + } + } + + #[tokio::test] + async fn test_quorum_lookup_height_range_optimization() { + // This test verifies that the height range optimization works correctly + // without allocating a Vec + use dashcore::sml::masternode_list_engine::MasternodeListEngine; + use dashcore::Network; + + let engine = MasternodeListEngine::default_for_network(Network::Dash); + let lookup = QuorumLookup::with_engine(Arc::new(engine)); + + // With an empty engine, should return None + assert_eq!(lookup.masternode_list_height_range(), None); + + // The optimization ensures we don't allocate a Vec for the keys + // If there's a panic or incorrect behavior, this test will catch it + } } From 4ad5e45c7b5ec3b4e52ed83a2817202883ace75e Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Fri, 21 Nov 2025 17:13:44 +0700 Subject: [PATCH 5/8] update doc --- dash-spv/src/client/queries.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dash-spv/src/client/queries.rs b/dash-spv/src/client/queries.rs index bd9f60170..36da1d2bc 100644 --- a/dash-spv/src/client/queries.rs +++ b/dash-spv/src/client/queries.rs @@ -71,10 +71,10 @@ impl< /// Get a quorum entry by type and hash at a specific block height. /// Returns None if the quorum is not found. /// - /// # Deprecated + /// # Note /// - /// This method is now a synchronous wrapper that blocks on the async QuorumLookup. - /// For better performance and to avoid blocking, prefer using the async version: + /// This is a synchronous convenience wrapper that blocks on the async QuorumLookup. + /// For better performance and to avoid blocking, prefer using the async version directly: /// /// ```rust,no_run /// # use dash_spv::client::DashSpvClient; From c334ac2a6b0091ac5d69af6e6a5ab36a973ac8fc Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Fri, 21 Nov 2025 18:00:16 +0700 Subject: [PATCH 6/8] fix --- dash-spv/src/client/queries.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/dash-spv/src/client/queries.rs b/dash-spv/src/client/queries.rs index 36da1d2bc..5f83613df 100644 --- a/dash-spv/src/client/queries.rs +++ b/dash-spv/src/client/queries.rs @@ -76,6 +76,11 @@ impl< /// This is a synchronous convenience wrapper that blocks on the async QuorumLookup. /// For better performance and to avoid blocking, prefer using the async version directly: /// + /// Returns `None` if: + /// - No Tokio runtime is available (fails gracefully instead of panicking) + /// - The quorum is not found + /// - Masternode sync is not yet complete + /// /// ```rust,no_run /// # use dash_spv::client::DashSpvClient; /// # async fn example(client: &DashSpvClient< @@ -98,8 +103,11 @@ impl< ) -> Option { // Delegate to the QuorumLookup component // This requires blocking on the async call - tokio::task::block_in_place(|| { - tokio::runtime::Handle::current().block_on(async { + // Return None gracefully if no Tokio runtime is available + let handle = tokio::runtime::Handle::try_current().ok()?; + + tokio::task::block_in_place(move || { + handle.block_on(async { self.quorum_lookup.get_quorum_at_height(height, quorum_type, quorum_hash).await }) }) From f633d3ad481663269ec54a589ed8f2c5eab36542 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Wed, 26 Nov 2025 16:21:58 +0700 Subject: [PATCH 7/8] cleanup doc --- dash-spv/src/client/core.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/dash-spv/src/client/core.rs b/dash-spv/src/client/core.rs index a0a96faed..b19904ebe 100644 --- a/dash-spv/src/client/core.rs +++ b/dash-spv/src/client/core.rs @@ -140,8 +140,6 @@ pub struct DashSpvClient, pub(super) running: Arc>, #[cfg(feature = "terminal-ui")] From aa52a0c3d7880f95d99a9528db9bb1683158ef67 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Wed, 26 Nov 2025 18:57:41 +0700 Subject: [PATCH 8/8] cleanup --- dash-spv/src/client/quorum_lookup.rs | 68 +++++++++++++--------------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/dash-spv/src/client/quorum_lookup.rs b/dash-spv/src/client/quorum_lookup.rs index 8115e959c..f08692f08 100644 --- a/dash-spv/src/client/quorum_lookup.rs +++ b/dash-spv/src/client/quorum_lookup.rs @@ -197,43 +197,39 @@ impl QuorumLookup { // Get the engine let engine = self.engine()?; - // Look up the masternode list and quorum - let quorum = engine - .masternode_lists - .get(&height) - .and_then(|ml| ml.quorums.get(&llmq_type)) - .and_then(|quorums| { - if let Some(quorum) = quorums.get(&qhash) { - debug!( - "Found quorum type {} at height {} with hash {}", - quorum_type, - height, - hex::encode(quorum_hash) - ); - Some(quorum.clone()) - } else { - warn!( - "Quorum not found: type {} at height {} with hash {} (masternode list exists with {} quorums of this type)", - quorum_type, - height, - hex::encode(quorum_hash), - quorums.len() - ); - None - } - }); - - if quorum.is_none() && engine.masternode_lists.contains_key(&height) { - // We have the masternode list but not this quorum type - warn!( - "No quorums of type {} found at height {} (masternode list exists)", - quorum_type, height - ); - } else if quorum.is_none() { - warn!("No masternode list found at height {} - cannot retrieve quorum", height); + let masternode_list = match engine.masternode_lists.get(&height) { + Some(list) => list, + None => { + debug!( + "No masternode list at height {} when looking up quorum type {} with hash {}", + height, + quorum_type, + hex::encode(quorum_hash) + ); + return None; + } + }; + + match masternode_list.quorum_entry_of_type_for_quorum_hash(llmq_type, qhash).cloned() { + Some(q) => { + debug!( + "Found verified quorum type {} at height {} with hash {}", + quorum_type, + height, + hex::encode(quorum_hash) + ); + Some(q) + } + None => { + debug!( + "Missing quorum type {} at height {} with hash {}", + quorum_type, + height, + hex::encode(quorum_hash) + ); + None + } } - - quorum } /// Check if the masternode engine is available.