Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1ca5da0
fix(dash-spv): correct NetworkMessage enum variant name from GetMnLis…
Aug 15, 2025
0382ae9
fix(dash-spv): add explicit error handling to find_chain_lock_quorum …
Aug 15, 2025
7a2a46d
fix(dash-spv): align block message expectations with runtime behavior…
Aug 15, 2025
54c224b
fix(dash-spv): resolve storage handle shadowing in chainlock validati…
Aug 15, 2025
767918c
fix(dash-spv): use actual block height in MnListDiff validation errors
Aug 15, 2025
080d620
fix(dash-spv): replace MemoryStorage with MemoryStorageManager in val…
Aug 15, 2025
e6e1396
fix(dash-spv): correct test assertion for qr_info_extra_share default
Aug 15, 2025
b9c8a3c
fix(dash-spv): add bounds checking to sync phase completion methods
Aug 15, 2025
f49b3f7
fix(sml): use per-window mining_start for LLMQ type skip check
Aug 15, 2025
ec8fa83
fix(docs): update API documentation to match actual dash-spv implemen…
Aug 15, 2025
f20b8f4
fix(dash-spv): replace hardcoded quorum size with dynamic LLMQ size
Aug 15, 2025
b52df40
fix(dash-spv): update MockStorageManager to match current StorageMana…
Aug 15, 2025
6d9c734
Replace todo!() with real QRInfo response correlation implementation
Aug 15, 2025
6d80d0c
fix(phase-3): correct processing_time to store elapsed duration inste…
Aug 15, 2025
19499bf
Replace todo!() with real QRInfo response correlation implementation
Aug 15, 2025
e285a40
fix(qr_info_spv_plan): replace send_error with proper Result type han…
Aug 15, 2025
c9d9240
fix(qr_info_spv_plan): replace trait object Vec with enum-based recov…
Aug 15, 2025
640465f
Fix QRInfoCorrelationManager pending_requests field synchronization
Aug 15, 2025
daea8ee
fix: integrate Phase 3 with existing dash-spv interfaces instead of d…
Aug 15, 2025
99cb5cd
fix(dash-spv): correct NetworkMessage enum variant to GetMnListD
Aug 16, 2025
4293bf5
fix(build): resolve compilation errors in key-wallet-ffi and dash-spv
Aug 16, 2025
6b699ae
revert: undo extra changes to dash-spv/src/sync/sequential/mod.rs fro…
Aug 16, 2025
3bb7cfb
revert: restore KeyWalletFFI targets in Package.swift from commit 578…
Aug 16, 2025
a32c854
chore: remove qr_info_spv_plan folder
Aug 16, 2025
63ba730
revert: uncomment AddressGenerator interface in key_wallet.udl
Aug 16, 2025
9ec2793
revert: restore original key-wallet-ffi/src/lib.rs implementation
Aug 16, 2025
dea2559
fix: restore working versions from v0.40-dev to fix build and test co…
Aug 16, 2025
0ecb5ad
fix: add missing UnconfirmedTransaction import in error_handling_test.rs
Aug 16, 2025
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
6 changes: 3 additions & 3 deletions dash-spv-ffi/src/platform_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,9 @@ pub unsafe extern "C" fn ffi_dash_spv_get_quorum_public_key(
);
}

// Copy the public key directly from the global index
let pubkey_ptr = public_key as *const _ as *const u8;
std::ptr::copy_nonoverlapping(pubkey_ptr, out_pubkey, QUORUM_PUBKEY_SIZE);
// Get the public key's canonical 48-byte representation safely
let pubkey_bytes: &[u8; 48] = public_key.as_ref();
std::ptr::copy_nonoverlapping(pubkey_bytes.as_ptr(), out_pubkey, QUORUM_PUBKEY_SIZE);

// Return success
FFIResult {
Expand Down
5 changes: 3 additions & 2 deletions dash-spv-ffi/tests/test_platform_integration_safety.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,10 +357,11 @@ fn test_error_string_lifecycle() {
fn test_handle_lifecycle() {
unsafe {
// Test null handle operations
let null_handle = ptr::null_mut();
let null_client: *mut FFIDashSpvClient = ptr::null_mut();
let null_handle: *mut CoreSDKHandle = ptr::null_mut();

// Getting core handle from null client
let handle = ffi_dash_spv_get_core_handle(null_handle);
let handle = ffi_dash_spv_get_core_handle(null_client);
assert!(handle.is_null());

// Releasing null handle should be safe
Expand Down
6 changes: 6 additions & 0 deletions dash-spv-ffi/tests/unit/test_type_conversions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ mod tests {
current_filter_tip: None,
masternode_engine: None,
last_masternode_diff_height: None,
sync_base_height: 0,
synced_from_checkpoint: false,
};

let ffi_state = FFIChainState::from(state);
Expand Down Expand Up @@ -213,6 +215,10 @@ mod tests {
pending_filter_requests: 0,
filter_request_timeouts: u64::MAX,
filter_requests_retried: u64::MAX,
connected_peers: 0,
total_peers: 0,
header_height: 0,
filter_height: 0,
};
Comment on lines +218 to 222
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

SpvStats new fields are not validated in FFISpvStats

connected_peers/total_peers/header_height/filter_height are set in the Rust struct but not asserted post-conversion. Either add FFISpvStats assertions (if supported) or note they’re intentionally ignored.

Try locating these fields in the FFI:


🏁 Script executed:

#!/bin/bash
# Search for new SpvStats fields in FFI conversion code
rg -n -C3 -P '\b(connected_peers|total_peers|header_height|filter_height)\b'

Length of output: 70575


🏁 Script executed:

#!/bin/bash
# Display the FFISpvStats conversion test and following assertions
sed -n '200,260p' dash-spv-ffi/tests/unit/test_type_conversions.rs

Length of output: 2442


Add missing FFISpvStats assertions for peer and height fields
The SpvStats conversion test in dash-spv-ffi/tests/unit/test_type_conversions.rs currently checks most metrics but omits the four new fields (connected_peers, total_peers, header_height, filter_height). Please update the test to include assertions for these fields:

diff --git a/dash-spv-ffi/tests/unit/test_type_conversions.rs b/dash-spv-ffi/tests/unit/test_type_conversions.rs
index abc1234..def5678 100644
--- a/dash-spv-ffi/tests/unit/test_type_conversions.rs
+++ b/dash-spv-ffi/tests/unit/test_type_conversions.rs
@@ fn test_spv_stats_conversion() {
     assert_eq!(ffi_stats.uptime, u64::MAX);
+    // Validate peer counts and heights
+    assert_eq!(ffi_stats.connected_peers, 0);
+    assert_eq!(ffi_stats.total_peers,     0);
+    assert_eq!(ffi_stats.header_height,    0);
+    assert_eq!(ffi_stats.filter_height,    0);
 }

This ensures all fields introduced on the Rust side are properly round-tripped to the FFI struct.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
connected_peers: 0,
total_peers: 0,
header_height: 0,
filter_height: 0,
};
assert_eq!(ffi_stats.uptime, u64::MAX);
// Validate peer counts and heights
assert_eq!(ffi_stats.connected_peers, 0);
assert_eq!(ffi_stats.total_peers, 0);
assert_eq!(ffi_stats.header_height, 0);
assert_eq!(ffi_stats.filter_height, 0);
}
🤖 Prompt for AI Agents
In dash-spv-ffi/tests/unit/test_type_conversions.rs around lines 218 to 222, the
SpvStats round-trip test is missing assertions for the four new fields
(connected_peers, total_peers, header_height, filter_height); update the
expected Rust SpvStats instance and/or the assertions on the converted FFI
struct to include checks that connected_peers and total_peers equal the original
values and that header_height and filter_height equal the original heights,
adding those four assertions alongside the existing assertions so the test
validates all fields introduced on the Rust side.


let ffi_stats = FFISpvStats::from(stats);
Expand Down
4 changes: 2 additions & 2 deletions dash-spv/src/network/message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,11 @@ impl MessageHandler {
self.stats.qrinfo_messages += 1;
MessageHandleResult::QRInfo(qr_info)
}
NetworkMessage::GetQRInfo(_) => {
NetworkMessage::GetQRInfo(req) => {
// We don't serve QRInfo requests, only make them
tracing::warn!("Received unexpected GetQRInfo request");
self.stats.other_messages += 1;
MessageHandleResult::Unhandled(message)
MessageHandleResult::Unhandled(NetworkMessage::GetQRInfo(req))
}
other => {
self.stats.other_messages += 1;
Expand Down
46 changes: 32 additions & 14 deletions dash-spv/src/sync/chainlock_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,20 +223,38 @@ impl ChainLockValidator {
// Get the masternode list at or before the height
let mn_list_height = engine.masternode_lists.range(..=height).rev().next().map(|(h, _)| *h);

if let Some(list_height) = mn_list_height {
if let Some(mn_list) = engine.masternode_lists.get(&list_height) {
// Find the chain lock quorum
if let Some(quorums) = mn_list.quorums.get(&self.config.required_llmq_type) {
// Get the most recent quorum
if let Some((quorum_hash, entry)) = quorums.iter().next() {
// Get public key from the quorum entry
return Ok(Some((*quorum_hash, entry.quorum_entry.quorum_public_key)));
}
}
}
}

Ok(None)
let list_height = mn_list_height.ok_or_else(|| {
SyncError::Validation(format!(
"No masternode list found at or before height {}",
height
))
})?;

let mn_list = engine.masternode_lists.get(&list_height).ok_or_else(|| {
SyncError::Validation(format!(
"Masternode list not found at height {}",
list_height
))
})?;

// Find the chain lock quorum
let quorums = mn_list.quorums.get(&self.config.required_llmq_type).ok_or_else(|| {
SyncError::Validation(format!(
"No quorums found for LLMQ type {:?} at masternode list height {}",
self.config.required_llmq_type, list_height
))
})?;

// Get the most recent quorum
let (quorum_hash, entry) = quorums.iter().next().ok_or_else(|| {
SyncError::Validation(format!(
"No quorum entries found for LLMQ type {:?}",
self.config.required_llmq_type
))
})?;

// Get public key from the quorum entry
Ok(Some((*quorum_hash, entry.quorum_entry.quorum_public_key)))
}

/// Verify chain lock signature using the engine's built-in verification
Expand Down
2 changes: 1 addition & 1 deletion dash-spv/src/sync/sequential/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1823,7 +1823,7 @@ impl SequentialSyncManager {
// If we have masternodes enabled, request masternode list updates for ChainLock validation
if self.config.enable_masternodes {
// For ChainLock validation, we need masternode lists at (block_height - CHAINLOCK_VALIDATION_MASTERNODE_OFFSET)
// So we request the masternode diff for this new block to maintain our rolling window
// We request the masternode diff for each new block (not just offset blocks) to maintain a complete rolling window
let base_block_hash = if height > 0 {
// Get the previous block hash
storage
Expand Down
24 changes: 17 additions & 7 deletions dash-spv/src/sync/sequential/phases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -546,17 +546,22 @@ impl SyncPhase {
if let SyncPhase::DownloadingMnList {
sync_strategy:
Some(HybridSyncStrategy::EngineDiscovery {
qr_info_requests,
qr_info_completed,
..
}),
requests_completed,
requests_total,
last_progress,
..
} = self
{
*qr_info_completed += 1;
*requests_completed += 1;
*last_progress = Instant::now();
// Only increment if we haven't reached the planned total
if *qr_info_completed < *qr_info_requests {
*qr_info_completed += 1;
*requests_completed = (*requests_completed + 1).min(*requests_total);
*last_progress = Instant::now();
}
}
}

Expand All @@ -565,19 +570,24 @@ impl SyncPhase {
if let SyncPhase::DownloadingMnList {
sync_strategy:
Some(HybridSyncStrategy::EngineDiscovery {
mn_diff_requests,
mn_diff_completed,
..
}),
requests_completed,
requests_total,
diffs_processed,
last_progress,
..
} = self
{
*mn_diff_completed += 1;
*requests_completed += 1;
*diffs_processed += 1; // Backward compatibility
*last_progress = Instant::now();
// Only increment if we haven't reached the planned total
if *mn_diff_completed < *mn_diff_requests {
*mn_diff_completed += 1;
*requests_completed = (*requests_completed + 1).min(*requests_total);
*diffs_processed += 1; // Backward compatibility
*last_progress = Instant::now();
}
}
}

Expand Down
14 changes: 8 additions & 6 deletions dash-spv/src/sync/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,14 +233,15 @@ impl ValidationEngine {

// Validate masternode list diffs
for diff in &qr_info.mn_list_diff_list {
let block_height = engine.block_container.get_height(&diff.block_hash).unwrap_or(0);
match self.validate_mn_list_diff(diff, engine) {
Ok(true) => items_validated += 1,
Ok(false) => errors.push(ValidationError::InvalidMnListDiff(
0, // We don't have block height in MnListDiff
block_height,
"Validation failed".to_string(),
)),
Err(e) => errors.push(ValidationError::InvalidMnListDiff(
0, // We don't have block height in MnListDiff
block_height,
e.to_string(),
)),
}
Expand Down Expand Up @@ -277,7 +278,8 @@ impl ValidationEngine {
diff: &MnListDiff,
engine: &MasternodeListEngine,
) -> SyncResult<bool> {
let cache_key = ValidationCacheKey::MasternodeList(0); // Use 0 as we don't have block height
let block_height = engine.block_container.get_height(&diff.block_hash).unwrap_or(0);
let cache_key = ValidationCacheKey::MasternodeList(block_height);

// Check cache
if let Some(cached) = self.get_cached_result(&cache_key) {
Expand All @@ -297,11 +299,11 @@ impl ValidationEngine {
fn perform_mn_list_diff_validation(
&self,
diff: &MnListDiff,
_engine: &MasternodeListEngine,
engine: &MasternodeListEngine,
) -> SyncResult<bool> {
// Check if we have the base list
// Note: We can't check by height as MnListDiff doesn't contain block height
// We would need to look up the height from the block hash
// We can resolve block height from the diff's block hash using the engine's block container
let block_height = engine.block_container.get_height(&diff.block_hash).unwrap_or(0);

// Validate merkle root matches
// TODO: Implement merkle root validation
Expand Down
4 changes: 2 additions & 2 deletions dash-spv/src/sync/validation_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#[cfg(test)]
mod tests {
use crate::client::ClientConfig;
use crate::storage::MemoryStorage;
use crate::storage::MemoryStorageManager;
use crate::sync::chainlock_validation::{ChainLockValidationConfig, ChainLockValidator};
use crate::sync::masternodes::MasternodeSyncManager;
use crate::sync::validation::{ValidationConfig, ValidationEngine};
Expand Down Expand Up @@ -136,7 +136,7 @@ mod tests {
async fn test_qr_info_validation() {
let config = create_test_config();
let _sync_manager = MasternodeSyncManager::new(&config);
let _storage = MemoryStorage::new();
let _storage = MemoryStorageManager::new().await.expect("Failed to create MemoryStorageManager");

// Create mock QRInfo
let _qr_info = create_mock_qr_info();
Expand Down
6 changes: 2 additions & 4 deletions dash-spv/tests/chainlock_validation_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ async fn test_chainlock_queue_and_process_flow() {
let storage_path = temp_dir.path().to_path_buf();

// Create storage
let storage = Box::new(DiskStorageManager::new(storage_path).await.unwrap());
let mut storage = Box::new(DiskStorageManager::new(storage_path).await.unwrap());
let network = Box::new(MockNetworkManager::new());

// Create client config
Expand Down Expand Up @@ -325,7 +325,6 @@ async fn test_chainlock_queue_and_process_flow() {

// Process pending (will fail validation but clear the queue)
let chain_state = ChainState::new();
let storage = client.storage();
let _ = chainlock_manager.validate_pending_chainlocks(&chain_state, &mut *storage).await;

// Verify queue is cleared
Expand Down Expand Up @@ -363,13 +362,12 @@ async fn test_chainlock_manager_cache_operations() {

// Add test headers
let genesis = genesis_block(Network::Dash).header;
let storage = client.storage();
let mut storage = client.storage();
// storage.store_header(&genesis, 0).await.unwrap();

// Create and process a ChainLock
let chain_lock = create_test_chainlock(0, genesis.block_hash());
let chain_state = ChainState::new();
let storage = client.storage();
let _ =
chainlock_manager.process_chain_lock(chain_lock.clone(), &chain_state, &mut *storage).await;

Expand Down
Loading
Loading