From f59f1ac8895f71bab33b6af28303d4e758c31a4f Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 27 May 2026 21:21:37 +0200 Subject: [PATCH] feat(rs-platform-wallet-ffi): expose devnet name and LLMQ_DEVNET override in spv_start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Devnet SPV sync needs two knobs the FFI didn't expose: - Dash Core devnet peers gate the inbound handshake on a `devnet.devnet-` substring in the user agent (`net_processing.cpp:3957`). Without it, the SPV client cannot connect to any devnet peer. - `LLMQ_DEVNET` size/threshold must match the devnet's `-llmqdevnetparams=:` so the SPV client reconstructs the right signing sets for ChainLock / legacy InstantSend verification. Both are already library fields on `dash-spv`'s `ClientConfig` after rust-dashcore PR dashpay/rust-dashcore#784. This change wires them through the FFI and the Swift wrapper as pure plumbing — no decisions on either side. What changed: - `platform_wallet_manager_spv_start` gains three trailing params: `devnet_name: *const c_char`, `llmq_devnet_size: u32`, `llmq_devnet_threshold: u32`. `0`/`NULL` mean "leave default". Validates `(devnet_name set) <=> (network == Devnet)` and `(size > 0) <=> (threshold > 0)` and rejects size/threshold on non-devnet networks. When `devnet_name` is set, rebuilds the user agent as `/{base}(devnet.devnet-{name})/`, falling back to a `platform-wallet-ffi:` base when the caller didn't supply one — mirrors the format the `dash-spv` binary uses. - `PlatformSpvStartConfig` gains `devnetName: String?`, `llmqDevnetSize: UInt32`, `llmqDevnetThreshold: UInt32` (all defaulted, so existing call sites compile unchanged). `startSpv(config:)` marshals them to the new trailing FFI params. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/rs-platform-wallet-ffi/src/spv.rs | 66 ++++++++++++++++++- .../PlatformWalletManagerSPV.swift | 30 ++++++++- 2 files changed, 93 insertions(+), 3 deletions(-) diff --git a/packages/rs-platform-wallet-ffi/src/spv.rs b/packages/rs-platform-wallet-ffi/src/spv.rs index 04cc61b2b8..0c0b259a36 100644 --- a/packages/rs-platform-wallet-ffi/src/spv.rs +++ b/packages/rs-platform-wallet-ffi/src/spv.rs @@ -3,6 +3,7 @@ use std::ffi::CStr; use std::os::raw::c_char; +use dashcore::sml::llmq_type::LlmqDevnetParams; use platform_wallet::spv::{ClientConfig, ProgressPercentage, SyncProgress, SyncState}; use crate::error::*; @@ -197,6 +198,17 @@ pub unsafe extern "C" fn platform_wallet_manager_spv_tip_unix_seconds( /// Hardcoded to `true` here; if a future caller has a real reason to /// run SPV without masternode sync, it can construct the wallet /// manager without the asset-lock path instead. +/// +/// Devnet support: +/// - `devnet_name` must be set (non-null) when `network == Devnet` and +/// must be null on every other network. When set, the SPV user agent +/// is rewritten to embed the `devnet.devnet-` substring Dash +/// Core peers gate inbound devnet handshakes on (matches the format +/// the `dash-spv` binary uses). +/// - `llmq_devnet_size` / `llmq_devnet_threshold` mirror Dash Core's +/// `-llmqdevnetparams=:`. Both zero means "no +/// override". Setting one without the other, or setting them on a +/// non-devnet network, is rejected. #[no_mangle] #[allow(clippy::field_reassign_with_default)] pub unsafe extern "C" fn platform_wallet_manager_spv_start( @@ -208,10 +220,13 @@ pub unsafe extern "C" fn platform_wallet_manager_spv_start( peer_count: usize, restrict_to_configured_peers: bool, start_from_height: u32, + devnet_name: *const c_char, + llmq_devnet_size: u32, + llmq_devnet_threshold: u32, ) -> PlatformWalletFFIResult { check_ptr!(data_dir); let data_dir_str = unwrap_result_or_return!(CStr::from_ptr(data_dir).to_str()).to_string(); - let user_agent_str = if user_agent.is_null() { + let mut user_agent_str = if user_agent.is_null() { None } else { Some(unwrap_result_or_return!(CStr::from_ptr(user_agent).to_str()).to_string()) @@ -219,6 +234,49 @@ pub unsafe extern "C" fn platform_wallet_manager_spv_start( let net: crate::types::Network = network.into(); + let devnet_name_str = if devnet_name.is_null() { + None + } else { + Some(unwrap_result_or_return!(CStr::from_ptr(devnet_name).to_str()).to_string()) + }; + + if net == crate::types::Network::Devnet && devnet_name_str.is_none() { + return PlatformWalletFFIResult::err( + PlatformWalletFFIResultCode::ErrorInvalidParameter, + "devnet_name is required when network=Devnet", + ); + } + if net != crate::types::Network::Devnet && devnet_name_str.is_some() { + return PlatformWalletFFIResult::err( + PlatformWalletFFIResultCode::ErrorInvalidParameter, + "devnet_name is only valid on devnet", + ); + } + if (llmq_devnet_size > 0) ^ (llmq_devnet_threshold > 0) { + return PlatformWalletFFIResult::err( + PlatformWalletFFIResultCode::ErrorInvalidParameter, + "llmq_devnet_size and llmq_devnet_threshold must both be set or both zero", + ); + } + if llmq_devnet_size > 0 && net != crate::types::Network::Devnet { + return PlatformWalletFFIResult::err( + PlatformWalletFFIResultCode::ErrorInvalidParameter, + "llmq_devnet_params is only valid on devnet", + ); + } + + // Dash Core devnet peers disconnect any inbound connection whose + // user agent doesn't contain `devnet.devnet-`. Rebuild the + // user agent to embed the substring while keeping whatever base + // the caller (or our default) provided. Mirrors the dash-spv + // binary's `/rust-dash-spv:VERSION(devnet.devnet-NAME)/` format. + if let Some(name) = devnet_name_str.as_deref() { + let base = user_agent_str + .clone() + .unwrap_or_else(|| format!("platform-wallet-ffi:{}", env!("CARGO_PKG_VERSION"))); + user_agent_str = Some(format!("/{base}(devnet.devnet-{name})/")); + } + let mut peer_list: Vec = Vec::new(); if !peers.is_null() && peer_count > 0 { for i in 0..peer_count { @@ -256,6 +314,12 @@ pub unsafe extern "C" fn platform_wallet_manager_spv_start( config.peers.push(addr); } } + if llmq_devnet_size > 0 { + config.llmq_devnet_params = Some(LlmqDevnetParams { + size: llmq_devnet_size, + threshold: llmq_devnet_threshold, + }); + } let _guard = runtime().enter(); manager.spv_arc().spawn_in_background(config); diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManagerSPV.swift b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManagerSPV.swift index 88f17da998..b80a77deff 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManagerSPV.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManagerSPV.swift @@ -112,6 +112,20 @@ public struct PlatformSpvStartConfig { public var peers: [String] public var restrictToConfiguredPeers: Bool public var startFromHeight: UInt32 + /// Devnet name (e.g. `"333"` for `devnet-333`). Required when + /// `network == .devnet`, must be `nil` on every other network. + /// The FFI rebuilds the user agent to embed + /// `devnet.devnet-` so Dash Core devnet peers accept the + /// handshake. + public var devnetName: String? + /// LLMQ_DEVNET quorum size override (matches Dash Core's + /// `-llmqdevnetparams=:`). `0` means "no + /// override". Must be paired with `llmqDevnetThreshold` and only + /// valid on devnet. + public var llmqDevnetSize: UInt32 + /// LLMQ_DEVNET signing threshold override. See + /// `llmqDevnetSize`. + public var llmqDevnetThreshold: UInt32 public init( dataDir: String, @@ -119,7 +133,10 @@ public struct PlatformSpvStartConfig { userAgent: String? = nil, peers: [String] = [], restrictToConfiguredPeers: Bool = false, - startFromHeight: UInt32 = 0 + startFromHeight: UInt32 = 0, + devnetName: String? = nil, + llmqDevnetSize: UInt32 = 0, + llmqDevnetThreshold: UInt32 = 0 ) { self.dataDir = dataDir self.network = network @@ -127,6 +144,9 @@ public struct PlatformSpvStartConfig { self.peers = peers self.restrictToConfiguredPeers = restrictToConfiguredPeers self.startFromHeight = startFromHeight + self.devnetName = devnetName + self.llmqDevnetSize = llmqDevnetSize + self.llmqDevnetThreshold = llmqDevnetThreshold } } @@ -194,6 +214,9 @@ extension PlatformWalletManager { let userAgentPtr = config.userAgent.flatMap { strdup($0) } defer { if let p = userAgentPtr { free(p) } } + let devnetNamePtr = config.devnetName.flatMap { strdup($0) } + defer { if let p = devnetNamePtr { free(p) } } + try peerCStrings.withUnsafeBufferPointer { peersBuf in let peersPtr: UnsafePointer?>? = peersBuf.baseAddress.map { UnsafeRawPointer($0).assumingMemoryBound(to: UnsafePointer?.self) @@ -206,7 +229,10 @@ extension PlatformWalletManager { peersPtr, UInt(peerCStrings.count), config.restrictToConfiguredPeers, - config.startFromHeight + config.startFromHeight, + devnetNamePtr, + config.llmqDevnetSize, + config.llmqDevnetThreshold ).check() } }