diff --git a/.gitignore b/.gitignore index 7eb2f1671..b07033a86 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ cobertura.xml # Build scripts artifacts *.log /dash-spv-ffi/peer_reputation.json +/dash-spv/peer_reputation.json diff --git a/dash-spv-ffi/FFI_API.md b/dash-spv-ffi/FFI_API.md index 03e4e18cb..009478074 100644 --- a/dash-spv-ffi/FFI_API.md +++ b/dash-spv-ffi/FFI_API.md @@ -4,7 +4,7 @@ This document provides a comprehensive reference for all FFI (Foreign Function I **Auto-generated**: This documentation is automatically generated from the source code. Do not edit manually. -**Total Functions**: 63 +**Total Functions**: 64 ## Table of Contents @@ -34,12 +34,12 @@ Functions: 4 ### Configuration -Functions: 25 +Functions: 26 | Function | Description | Module | |----------|-------------|--------| | `dash_spv_ffi_client_update_config` | Update the running client's configuration | client | -| `dash_spv_ffi_config_add_peer` | Adds a peer address to the configuration # Safety - `config` must be a valid... | config | +| `dash_spv_ffi_config_add_peer` | Adds a peer address to the configuration Accepts either a full socket addres... | config | | `dash_spv_ffi_config_destroy` | Destroys an FFIClientConfig and frees its memory # Safety - `config` must be... | config | | `dash_spv_ffi_config_get_data_dir` | Gets the data directory path from the configuration # Safety - `config` must... | config | | `dash_spv_ffi_config_get_mempool_strategy` | Gets the mempool synchronization strategy # Safety - `config` must be a vali... | config | @@ -58,6 +58,7 @@ Functions: 25 | `dash_spv_ffi_config_set_mempool_tracking` | Enables or disables mempool tracking # Safety - `config` must be a valid poi... | config | | `dash_spv_ffi_config_set_persist_mempool` | Sets whether to persist mempool state to disk # Safety - `config` must be a ... | config | | `dash_spv_ffi_config_set_relay_transactions` | Sets whether to relay transactions (currently a no-op) # Safety - `config` m... | config | +| `dash_spv_ffi_config_set_restrict_to_configured_peers` | Restrict connections strictly to configured peers (disable DNS discovery and ... | config | | `dash_spv_ffi_config_set_start_from_height` | Sets the starting block height for synchronization # Safety - `config` must ... | config | | `dash_spv_ffi_config_set_user_agent` | Sets the user agent string to advertise in the P2P handshake # Safety - `con... | config | | `dash_spv_ffi_config_set_validation_mode` | Sets the validation mode for the SPV client # Safety - `config` must be a va... | config | @@ -247,10 +248,10 @@ dash_spv_ffi_config_add_peer(config: *mut FFIClientConfig, addr: *const c_char,) ``` **Description:** -Adds a peer address to the configuration # Safety - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - `addr` must be a valid null-terminated C string containing a socket address (e.g., "192.168.1.1:9999") - The caller must ensure both pointers remain valid for the duration of this call +Adds a peer address to the configuration Accepts either a full socket address (e.g., "192.168.1.1:9999" or "[::1]:19999") or an IP-only string (e.g., "127.0.0.1" or "2001:db8::1"). When an IP-only string is given, the default P2P port for the configured network is used. # Safety - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - `addr` must be a valid null-terminated C string containing a socket address or IP-only string - The caller must ensure both pointers remain valid for the duration of this call **Safety:** -- `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - `addr` must be a valid null-terminated C string containing a socket address (e.g., "192.168.1.1:9999") - The caller must ensure both pointers remain valid for the duration of this call +- `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - `addr` must be a valid null-terminated C string containing a socket address or IP-only string - The caller must ensure both pointers remain valid for the duration of this call **Module:** `config` @@ -532,6 +533,22 @@ Sets whether to relay transactions (currently a no-op) # Safety - `config` must --- +#### `dash_spv_ffi_config_set_restrict_to_configured_peers` + +```c +dash_spv_ffi_config_set_restrict_to_configured_peers(config: *mut FFIClientConfig, restrict: bool,) -> i32 +``` + +**Description:** +Restrict connections strictly to configured peers (disable DNS discovery and peer store) # Safety - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet + +**Safety:** +- `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet + +**Module:** `config` + +--- + #### `dash_spv_ffi_config_set_start_from_height` ```c diff --git a/dash-spv-ffi/README.md b/dash-spv-ffi/README.md index 82e7a4e02..00eeea2c2 100644 --- a/dash-spv-ffi/README.md +++ b/dash-spv-ffi/README.md @@ -82,7 +82,7 @@ dash_spv_ffi_config_destroy(config); - `dash_spv_ffi_config_set_data_dir(config, path)` - Set data directory - `dash_spv_ffi_config_set_validation_mode(config, mode)` - Set validation mode - `dash_spv_ffi_config_set_max_peers(config, max)` - Set maximum peers -- `dash_spv_ffi_config_add_peer(config, addr)` - Add a peer address +- `dash_spv_ffi_config_add_peer(config, addr)` - Add a peer address. Accepts `"ip:port"`, `[ipv6]:port`, or IP-only (defaults to the network port). - `dash_spv_ffi_config_destroy(config)` - Free config memory ### Client Operations @@ -123,4 +123,4 @@ The FFI bindings are thread-safe. The client uses internal synchronization to en ## License -MIT \ No newline at end of file +MIT diff --git a/dash-spv-ffi/include/dash_spv_ffi.h b/dash-spv-ffi/include/dash_spv_ffi.h index f156d8b3e..ae4ca7397 100644 --- a/dash-spv-ffi/include/dash_spv_ffi.h +++ b/dash-spv-ffi/include/dash_spv_ffi.h @@ -509,9 +509,13 @@ int32_t dash_spv_ffi_config_set_max_peers(FFIClientConfig *config, /** * Adds a peer address to the configuration * + * Accepts either a full socket address (e.g., "192.168.1.1:9999" or "[::1]:19999") + * or an IP-only string (e.g., "127.0.0.1" or "2001:db8::1"). When an IP-only + * string is given, the default P2P port for the configured network is used. + * * # Safety * - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - * - `addr` must be a valid null-terminated C string containing a socket address (e.g., "192.168.1.1:9999") + * - `addr` must be a valid null-terminated C string containing a socket address or IP-only string * - The caller must ensure both pointers remain valid for the duration of this call */ int32_t dash_spv_ffi_config_add_peer(FFIClientConfig *config, @@ -548,6 +552,15 @@ int32_t dash_spv_ffi_config_set_relay_transactions(FFIClientConfig *config, int32_t dash_spv_ffi_config_set_filter_load(FFIClientConfig *config, bool load_filters); +/** + * Restrict connections strictly to configured peers (disable DNS discovery and peer store) + * + * # Safety + * - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet + */ +int32_t dash_spv_ffi_config_set_restrict_to_configured_peers(FFIClientConfig *config, + bool restrict); + /** * Enables or disables masternode synchronization * diff --git a/dash-spv-ffi/src/config.rs b/dash-spv-ffi/src/config.rs index 60318b65c..cffae6c68 100644 --- a/dash-spv-ffi/src/config.rs +++ b/dash-spv-ffi/src/config.rs @@ -2,6 +2,7 @@ use crate::{null_check, set_last_error, FFIErrorCode, FFIMempoolStrategy, FFIStr use dash_spv::{ClientConfig, ValidationMode}; use key_wallet_ffi::FFINetwork; use std::ffi::CStr; +use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; use std::os::raw::c_char; #[repr(C)] @@ -115,9 +116,13 @@ pub unsafe extern "C" fn dash_spv_ffi_config_set_max_peers( /// Adds a peer address to the configuration /// +/// Accepts either a full socket address (e.g., "192.168.1.1:9999" or "[::1]:19999") +/// or an IP-only string (e.g., "127.0.0.1" or "2001:db8::1"). When an IP-only +/// string is given, the default P2P port for the configured network is used. +/// /// # Safety /// - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet -/// - `addr` must be a valid null-terminated C string containing a socket address (e.g., "192.168.1.1:9999") +/// - `addr` must be a valid null-terminated C string containing a socket address or IP-only string /// - The caller must ensure both pointers remain valid for the duration of this call #[no_mangle] pub unsafe extern "C" fn dash_spv_ffi_config_add_peer( @@ -127,20 +132,55 @@ pub unsafe extern "C" fn dash_spv_ffi_config_add_peer( null_check!(config); null_check!(addr); - let config = &mut (*config).inner; - match CStr::from_ptr(addr).to_str() { - Ok(addr_str) => match addr_str.parse() { - Ok(socket_addr) => { - config.peers.push(socket_addr); + let cfg = &mut (*config).inner; + let default_port = match cfg.network { + dashcore::Network::Dash => 9999, + dashcore::Network::Testnet => 19999, + dashcore::Network::Regtest => 19899, + dashcore::Network::Devnet => 29999, + _ => 9999, + }; + + let addr_str = match CStr::from_ptr(addr).to_str() { + Ok(s) => s.trim(), + Err(e) => { + set_last_error(&format!("Invalid UTF-8 in address: {}", e)); + return FFIErrorCode::InvalidArgument as i32; + } + }; + + // 1) Try parsing as full SocketAddr first (handles IPv6 [::1]:port forms) + if let Ok(sock) = addr_str.parse::() { + cfg.peers.push(sock); + return FFIErrorCode::Success as i32; + } + + // 2) If that fails, try parsing as bare IP address and apply default port + if let Ok(ip) = addr_str.parse::() { + let sock = SocketAddr::new(ip, default_port); + cfg.peers.push(sock); + return FFIErrorCode::Success as i32; + } + + // 3) Optionally attempt DNS name with explicit port only; if no port, reject + if !addr_str.contains(':') { + set_last_error("Missing port for hostname; supply 'host:port' or IP only"); + return FFIErrorCode::InvalidArgument as i32; + } + + match addr_str.to_socket_addrs() { + Ok(mut iter) => match iter.next() { + Some(sock) => { + cfg.peers.push(sock); FFIErrorCode::Success as i32 } - Err(e) => { - set_last_error(&format!("Invalid socket address: {}", e)); + None => { + set_last_error(&format!("Failed to resolve address: {}", addr_str)); FFIErrorCode::InvalidArgument as i32 } }, Err(e) => { - set_last_error(&format!("Invalid UTF-8 in address: {}", e)); + set_last_error(&format!("Invalid address {} ({})", addr_str, e)); FFIErrorCode::InvalidArgument as i32 } } @@ -209,6 +249,22 @@ pub unsafe extern "C" fn dash_spv_ffi_config_set_filter_load( FFIErrorCode::Success as i32 } +/// Restrict connections strictly to configured peers (disable DNS discovery and peer store) +/// +/// # Safety +/// - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_config_set_restrict_to_configured_peers( + config: *mut FFIClientConfig, + restrict: bool, +) -> i32 { + null_check!(config); + + let config = &mut (*config).inner; + config.restrict_to_configured_peers = restrict; + FFIErrorCode::Success as i32 +} + /// Enables or disables masternode synchronization /// /// # Safety diff --git a/dash-spv-ffi/tests/unit/test_configuration.rs b/dash-spv-ffi/tests/unit/test_configuration.rs index 47b68349e..1ff9c8271 100644 --- a/dash-spv-ffi/tests/unit/test_configuration.rs +++ b/dash-spv-ffi/tests/unit/test_configuration.rs @@ -58,7 +58,7 @@ mod tests { "256.256.256.256:9999", "127.0.0.1:99999", // port too high "127.0.0.1:-1", // negative port - "127.0.0.1", // missing port + "localhost", // hostname without port should be rejected ":9999", // missing IP ":::", // invalid IPv6 "localhost:abc", // non-numeric port @@ -74,9 +74,15 @@ mod tests { assert!(!error_ptr.is_null()); } - // Test valid addresses - let valid_addrs = - ["127.0.0.1:9999", "192.168.1.1:8333", "[::1]:9999", "[2001:db8::1]:8333"]; + // Test valid addresses including IP-only forms (port inferred from network) + let valid_addrs = [ + "127.0.0.1:9999", + "192.168.1.1:8333", + "[::1]:9999", + "[2001:db8::1]:8333", + "127.0.0.1", // IP-only v4 + "2001:db8::1", // IP-only v6 + ]; for addr in &valid_addrs { let c_addr = CString::new(*addr).unwrap(); @@ -198,6 +204,11 @@ mod tests { FFIErrorCode::Success as i32 ); + assert_eq!( + dash_spv_ffi_config_set_restrict_to_configured_peers(config, true), + FFIErrorCode::Success as i32 + ); + dash_spv_ffi_config_destroy(config); } } diff --git a/dash-spv/src/client/config.rs b/dash-spv/src/client/config.rs index 3ce0862bf..a2852fb4d 100644 --- a/dash-spv/src/client/config.rs +++ b/dash-spv/src/client/config.rs @@ -30,6 +30,13 @@ pub struct ClientConfig { /// List of peer addresses to connect to. pub peers: Vec, + /// Restrict connections strictly to the configured peers. + /// + /// When true, the client will not use DNS discovery or peer persistence and + /// will only attempt to connect to addresses provided in `peers`. + /// If no peers are configured, no outbound connections will be made. + pub restrict_to_configured_peers: bool, + /// Optional path for persistent storage. pub storage_path: Option, @@ -183,6 +190,7 @@ impl Default for ClientConfig { Self { network: Network::Dash, peers: vec![], + restrict_to_configured_peers: false, storage_path: None, validation_mode: ValidationMode::Full, filter_checkpoint_interval: 1000, @@ -243,6 +251,7 @@ impl ClientConfig { Self { network, peers: Self::default_peers_for_network(network), + restrict_to_configured_peers: false, ..Self::default() } } @@ -268,6 +277,12 @@ impl ClientConfig { self } + /// Restrict connections to the configured peers only. + pub fn with_restrict_to_configured_peers(mut self, restrict: bool) -> Self { + self.restrict_to_configured_peers = restrict; + self + } + /// Set storage path. pub fn with_storage_path(mut self, path: PathBuf) -> Self { self.storage_path = Some(path); diff --git a/dash-spv/src/network/multi_peer.rs b/dash-spv/src/network/multi_peer.rs index 331e52767..0b6f24eff 100644 --- a/dash-spv/src/network/multi_peer.rs +++ b/dash-spv/src/network/multi_peer.rs @@ -68,6 +68,8 @@ pub struct MultiPeerNetworkManager { peers_sent_headers2: Arc>>, /// Optional user agent to advertise user_agent: Option, + /// Exclusive mode: restrict to configured peers only (no DNS or peer store) + exclusive_mode: bool, } impl MultiPeerNetworkManager { @@ -97,6 +99,9 @@ impl MultiPeerNetworkManager { log::warn!("Failed to load peer reputation data: {}", e); } + // Determine exclusive mode: either explicitly requested or peers were provided + let exclusive_mode = config.restrict_to_configured_peers || !config.peers.is_empty(); + Ok(Self { pool: Arc::new(ConnectionPool::new()), discovery: Arc::new(discovery), @@ -117,6 +122,7 @@ impl MultiPeerNetworkManager { read_timeout: config.read_timeout, peers_sent_headers2: Arc::new(Mutex::new(HashSet::new())), user_agent: config.user_agent.clone(), + exclusive_mode, }) } @@ -126,10 +132,7 @@ impl MultiPeerNetworkManager { let mut peer_addresses = self.initial_peers.clone(); - // If specific peers were configured via -p flag, use ONLY those (exclusive mode) - let exclusive_mode = !self.initial_peers.is_empty(); - - if exclusive_mode { + if self.exclusive_mode { log::info!( "Exclusive peer mode: connecting ONLY to {} specified peer(s)", self.initial_peers.len() @@ -161,7 +164,7 @@ impl MultiPeerNetworkManager { } // Connect to peers (all in exclusive mode, or up to TARGET_PEERS in normal mode) - let max_connections = if exclusive_mode { + let max_connections = if self.exclusive_mode { peer_addresses.len() } else { TARGET_PEERS @@ -574,8 +577,8 @@ impl MultiPeerNetworkManager { let initial_peers = self.initial_peers.clone(); let data_dir = self.data_dir.clone(); - // Check if we're in exclusive mode (specific peers configured via -p) - let exclusive_mode = !initial_peers.is_empty(); + // Check if we're in exclusive mode (explicit flag or peers configured) + let exclusive_mode = self.exclusive_mode; // Clone self for connection callback let connect_fn = { @@ -977,6 +980,7 @@ impl Clone for MultiPeerNetworkManager { read_timeout: self.read_timeout, peers_sent_headers2: self.peers_sent_headers2.clone(), user_agent: self.user_agent.clone(), + exclusive_mode: self.exclusive_mode, } } } diff --git a/dash-spv/src/network/tests.rs b/dash-spv/src/network/tests.rs index 4dffa7a72..d26b7f62d 100644 --- a/dash-spv/src/network/tests.rs +++ b/dash-spv/src/network/tests.rs @@ -129,6 +129,7 @@ mod multi_peer_tests { ClientConfig { network: Network::Regtest, peers: vec!["127.0.0.1:19899".parse().unwrap()], + restrict_to_configured_peers: false, storage_path: Some(temp_dir.path().to_path_buf()), validation_mode: crate::types::ValidationMode::Basic, filter_checkpoint_interval: 1000, diff --git a/dash-spv/src/storage/disk.rs b/dash-spv/src/storage/disk.rs index 92babb0e7..3954b8e7f 100644 --- a/dash-spv/src/storage/disk.rs +++ b/dash-spv/src/storage/disk.rs @@ -132,6 +132,80 @@ fn create_sentinel_header() -> BlockHeader { } impl DiskStorageManager { + /// Start the background worker and notification channel. + async fn start_worker(&mut self) { + let (worker_tx, mut worker_rx) = mpsc::channel::(100); + let (notification_tx, notification_rx) = mpsc::channel::(100); + + let worker_base_path = self.base_path.clone(); + let worker_notification_tx = notification_tx.clone(); + let worker_handle = tokio::spawn(async move { + while let Some(cmd) = worker_rx.recv().await { + match cmd { + WorkerCommand::SaveHeaderSegment { + segment_id, + headers, + } => { + let path = + worker_base_path.join(format!("headers/segment_{:04}.dat", segment_id)); + if let Err(e) = save_segment_to_disk(&path, &headers).await { + eprintln!("Failed to save segment {}: {}", segment_id, e); + } else { + let _ = worker_notification_tx + .send(WorkerNotification::HeaderSegmentSaved { + segment_id, + }) + .await; + } + } + WorkerCommand::SaveFilterSegment { + segment_id, + filter_headers, + } => { + let path = worker_base_path + .join(format!("filters/filter_segment_{:04}.dat", segment_id)); + if let Err(e) = save_filter_segment_to_disk(&path, &filter_headers).await { + eprintln!("Failed to save filter segment {}: {}", segment_id, e); + } else { + let _ = worker_notification_tx + .send(WorkerNotification::FilterSegmentSaved { + segment_id, + }) + .await; + } + } + WorkerCommand::SaveIndex { + index, + } => { + let path = worker_base_path.join("headers/index.dat"); + if let Err(e) = save_index_to_disk(&path, &index).await { + eprintln!("Failed to save index: {}", e); + } else { + let _ = + worker_notification_tx.send(WorkerNotification::IndexSaved).await; + } + } + WorkerCommand::Shutdown => { + break; + } + } + } + }); + + self.worker_tx = Some(worker_tx); + self.worker_handle = Some(worker_handle); + self.notification_rx = Arc::new(RwLock::new(notification_rx)); + } + + /// Stop the background worker without forcing a save. + async fn stop_worker(&mut self) { + if let Some(tx) = self.worker_tx.take() { + let _ = tx.send(WorkerCommand::Shutdown).await; + } + if let Some(handle) = self.worker_handle.take() { + let _ = handle.await; + } + } /// Create a new disk storage manager with segmented storage. pub async fn new(base_path: PathBuf) -> StorageResult { // Create directories if they don't exist @@ -1472,25 +1546,46 @@ impl StorageManager for DiskStorageManager { } async fn clear(&mut self) -> StorageResult<()> { - // Clear in-memory data + // First, stop the background worker to avoid races with file deletion + self.stop_worker().await; + + // Clear in-memory state self.active_segments.write().await.clear(); self.active_filter_segments.write().await.clear(); self.header_hash_index.write().await.clear(); *self.cached_tip_height.write().await = None; *self.cached_filter_tip_height.write().await = None; - - // UTXO cache removed - UTXO management is now handled externally - - // Clear mempool self.mempool_transactions.write().await.clear(); *self.mempool_state.write().await = None; - // Remove all files + // Remove all files and directories under base_path if self.base_path.exists() { - tokio::fs::remove_dir_all(&self.base_path).await?; + // Best-effort removal; if concurrent files appear, retry once + match tokio::fs::remove_dir_all(&self.base_path).await { + Ok(_) => {} + Err(e) => { + // Retry once after a short delay to handle transient races + if e.kind() == std::io::ErrorKind::Other + || e.kind() == std::io::ErrorKind::DirectoryNotEmpty + { + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + tokio::fs::remove_dir_all(&self.base_path).await?; + } else { + return Err(StorageError::Io(e)); + } + } + } tokio::fs::create_dir_all(&self.base_path).await?; } + // Recreate expected subdirectories + tokio::fs::create_dir_all(self.base_path.join("headers")).await?; + tokio::fs::create_dir_all(self.base_path.join("filters")).await?; + tokio::fs::create_dir_all(self.base_path.join("state")).await?; + + // Restart the background worker for future operations + self.start_worker().await; + Ok(()) } diff --git a/swift-dash-core-sdk/Sources/DashSPVFFI/include/dash_spv_ffi.h b/swift-dash-core-sdk/Sources/DashSPVFFI/include/dash_spv_ffi.h index d58a1c940..ae4ca7397 100644 --- a/swift-dash-core-sdk/Sources/DashSPVFFI/include/dash_spv_ffi.h +++ b/swift-dash-core-sdk/Sources/DashSPVFFI/include/dash_spv_ffi.h @@ -3,9 +3,6 @@ #include #include -// Include shared FFI types (FFINetwork, etc.) from key-wallet-ffi -#include "key_wallet_ffi.h" - typedef enum FFIMempoolStrategy { FetchAll = 0, BloomFilter = 1, @@ -28,10 +25,26 @@ typedef enum FFIValidationMode { Full = 2, } FFIValidationMode; +typedef struct FFIDashSpvClient FFIDashSpvClient; + /** - * FFIDashSpvClient structure + * FFI-safe array that transfers ownership of memory to the C caller. + * + * # Safety + * + * This struct represents memory that has been allocated by Rust but ownership + * has been transferred to the C caller. The caller is responsible for: + * - Not accessing the memory after it has been freed + * - Calling `dash_spv_ffi_array_destroy` to properly deallocate the memory + * - Ensuring the data, len, and capacity fields remain consistent */ -typedef struct FFIDashSpvClient FFIDashSpvClient; +typedef struct FFIArray { + void *data; + uintptr_t len; + uintptr_t capacity; + uintptr_t elem_size; + uintptr_t elem_align; +} FFIArray; typedef ClientConfig FFIClientConfig; @@ -147,25 +160,6 @@ typedef struct FFIResult { const char *error_message; } FFIResult; -/** - * FFI-safe array that transfers ownership of memory to the C caller. - * - * # Safety - * - * This struct represents memory that has been allocated by Rust but ownership - * has been transferred to the C caller. The caller is responsible for: - * - Not accessing the memory after it has been freed - * - Calling `dash_spv_ffi_array_destroy` to properly deallocate the memory - * - Ensuring the data, len, and capacity fields remain consistent - */ -typedef struct FFIArray { - void *data; - uintptr_t len; - uintptr_t capacity; - uintptr_t elem_size; - uintptr_t elem_align; -} FFIArray; - /** * FFI-safe representation of an unconfirmed transaction * @@ -199,10 +193,83 @@ typedef struct FFIUnconfirmedTransaction { uintptr_t addresses_len; } FFIUnconfirmedTransaction; +/** + * Get the latest checkpoint for the given network. + * + * # Safety + * - `out_height` must be a valid pointer to a `u32`. + * - `out_hash` must point to at least 32 writable bytes. + */ +int32_t dash_spv_ffi_checkpoint_latest(FFINetwork network, uint32_t *out_height, uint8_t *out_hash); + +/** + * Get the last checkpoint at or before a given height. + * + * # Safety + * - `out_height` must be a valid pointer to a `u32`. + * - `out_hash` must point to at least 32 writable bytes. + */ +int32_t dash_spv_ffi_checkpoint_before_height(FFINetwork network, + uint32_t height, + uint32_t *out_height, + uint8_t *out_hash); + +/** + * Get the last checkpoint at or before a given UNIX timestamp (seconds). + * + * # Safety + * - `out_height` must be a valid pointer to a `u32`. + * - `out_hash` must point to at least 32 writable bytes. + */ +int32_t dash_spv_ffi_checkpoint_before_timestamp(FFINetwork network, + uint32_t timestamp, + uint32_t *out_height, + uint8_t *out_hash); + +/** + * Get all checkpoints between two heights (inclusive). + * + * Returns an `FFIArray` of `FFICheckpoint` items. The caller owns the memory and + * must free the array buffer using `dash_spv_ffi_array_destroy` when done. + */ +struct FFIArray dash_spv_ffi_checkpoints_between_heights(FFINetwork network, + uint32_t start_height, + uint32_t end_height); + +/** + * Create a new SPV client and return an opaque pointer. + * + * # Safety + * - `config` must be a valid, non-null pointer for the duration of the call. + * - The returned pointer must be freed with `dash_spv_ffi_client_destroy`. + */ struct FFIDashSpvClient *dash_spv_ffi_client_new(const FFIClientConfig *config); +/** + * Update the running client's configuration. + * + * # Safety + * - `client` must be a valid pointer to an `FFIDashSpvClient`. + * - `config` must be a valid pointer to an `FFIClientConfig`. + * - The network in `config` must match the client's network; changing networks at runtime is not supported. + */ +int32_t dash_spv_ffi_client_update_config(struct FFIDashSpvClient *client, + const FFIClientConfig *config); + +/** + * Start the SPV client. + * + * # Safety + * - `client` must be a valid, non-null pointer to a created client. + */ int32_t dash_spv_ffi_client_start(struct FFIDashSpvClient *client); +/** + * Stop the SPV client. + * + * # Safety + * - `client` must be a valid, non-null pointer to a created client. + */ int32_t dash_spv_ffi_client_stop(struct FFIDashSpvClient *client); /** @@ -296,27 +363,87 @@ int32_t dash_spv_ffi_client_sync_to_tip_with_progress(struct FFIDashSpvClient *c */ int32_t dash_spv_ffi_client_cancel_sync(struct FFIDashSpvClient *client); +/** + * Get the current sync progress snapshot. + * + * # Safety + * - `client` must be a valid, non-null pointer. + */ struct FFISyncProgress *dash_spv_ffi_client_get_sync_progress(struct FFIDashSpvClient *client); +/** + * Get current runtime statistics for the SPV client. + * + * # Safety + * - `client` must be a valid, non-null pointer. + */ struct FFISpvStats *dash_spv_ffi_client_get_stats(struct FFIDashSpvClient *client); +/** + * Check if compact filter sync is currently available. + * + * # Safety + * - `client` must be a valid, non-null pointer. + */ bool dash_spv_ffi_client_is_filter_sync_available(struct FFIDashSpvClient *client); +/** + * Set event callbacks for the client. + * + * # Safety + * - `client` must be a valid, non-null pointer. + */ int32_t dash_spv_ffi_client_set_event_callbacks(struct FFIDashSpvClient *client, struct FFIEventCallbacks callbacks); +/** + * Destroy the client and free associated resources. + * + * # Safety + * - `client` must be either null or a pointer obtained from `dash_spv_ffi_client_new`. + */ void dash_spv_ffi_client_destroy(struct FFIDashSpvClient *client); +/** + * Destroy a `FFISyncProgress` object returned by this crate. + * + * # Safety + * - `progress` must be a pointer returned from this crate, or null. + */ void dash_spv_ffi_sync_progress_destroy(struct FFISyncProgress *progress); +/** + * Destroy an `FFISpvStats` object returned by this crate. + * + * # Safety + * - `stats` must be a pointer returned from this crate, or null. + */ void dash_spv_ffi_spv_stats_destroy(struct FFISpvStats *stats); +/** + * Request a rescan of the blockchain from a given height (not yet implemented). + * + * # Safety + * - `client` must be a valid, non-null pointer. + */ int32_t dash_spv_ffi_client_rescan_blockchain(struct FFIDashSpvClient *client, uint32_t _from_height); +/** + * Enable mempool tracking with a given strategy. + * + * # Safety + * - `client` must be a valid, non-null pointer. + */ int32_t dash_spv_ffi_client_enable_mempool_tracking(struct FFIDashSpvClient *client, enum FFIMempoolStrategy strategy); +/** + * Record that we attempted to send a transaction by its txid. + * + * # Safety + * - `client` and `txid` must be valid, non-null pointers. + */ int32_t dash_spv_ffi_client_record_send(struct FFIDashSpvClient *client, const char *txid); /** @@ -329,12 +456,16 @@ int32_t dash_spv_ffi_client_record_send(struct FFIDashSpvClient *client, const c * * The caller must ensure that: * - The client pointer is valid - * - The returned pointer is freed using wallet_manager_free() + * - The returned pointer is freed using `wallet_manager_free` from key-wallet-ffi * * # Returns * * An opaque pointer (void*) to the wallet manager, or NULL if the client is not initialized. * Swift should treat this as an OpaquePointer. + * Get a handle to the wallet manager owned by this client. + * + * # Safety + * - `client` must be a valid, non-null pointer. */ void *dash_spv_ffi_client_get_wallet_manager(struct FFIDashSpvClient *client); @@ -378,9 +509,13 @@ int32_t dash_spv_ffi_config_set_max_peers(FFIClientConfig *config, /** * Adds a peer address to the configuration * + * Accepts either a full socket address (e.g., "192.168.1.1:9999" or "[::1]:19999") + * or an IP-only string (e.g., "127.0.0.1" or "2001:db8::1"). When an IP-only + * string is given, the default P2P port for the configured network is used. + * * # Safety * - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - * - `addr` must be a valid null-terminated C string containing a socket address (e.g., "192.168.1.1:9999") + * - `addr` must be a valid null-terminated C string containing a socket address or IP-only string * - The caller must ensure both pointers remain valid for the duration of this call */ int32_t dash_spv_ffi_config_add_peer(FFIClientConfig *config, @@ -417,6 +552,25 @@ int32_t dash_spv_ffi_config_set_relay_transactions(FFIClientConfig *config, int32_t dash_spv_ffi_config_set_filter_load(FFIClientConfig *config, bool load_filters); +/** + * Restrict connections strictly to configured peers (disable DNS discovery and peer store) + * + * # Safety + * - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet + */ +int32_t dash_spv_ffi_config_set_restrict_to_configured_peers(FFIClientConfig *config, + bool restrict); + +/** + * Enables or disables masternode synchronization + * + * # Safety + * - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet + * - The caller must ensure the config pointer remains valid for the duration of this call + */ +int32_t dash_spv_ffi_config_set_masternode_sync_enabled(FFIClientConfig *config, + bool enable); + /** * Gets the network type from the configuration * @@ -432,7 +586,7 @@ FFINetwork dash_spv_ffi_config_get_network(const FFIClientConfig *config); * # Safety * - `config` must be a valid pointer to an FFIClientConfig or null * - If null or no data directory is set, returns an FFIString with null pointer - * - The returned FFIString must be freed by the caller using dash_string_free + * - The returned FFIString must be freed by the caller using `dash_spv_ffi_string_destroy` */ struct FFIString dash_spv_ffi_config_get_data_dir(const FFIClientConfig *config); @@ -600,8 +754,18 @@ struct FFIResult ffi_dash_spv_get_quorum_public_key(struct FFIDashSpvClient *cli struct FFIResult ffi_dash_spv_get_platform_activation_height(struct FFIDashSpvClient *client, uint32_t *out_height); +/** + * # Safety + * - `s.ptr` must be a pointer previously returned by `FFIString::new` or compatible. + * - It must not be used after this call. + */ void dash_spv_ffi_string_destroy(struct FFIString s); +/** + * # Safety + * - `arr` must be either null or a valid pointer to an `FFIArray` previously constructed in Rust. + * - The memory referenced by `arr.data` must not be used after this call. + */ void dash_spv_ffi_array_destroy(struct FFIArray *arr); /** @@ -611,6 +775,10 @@ void dash_spv_ffi_array_destroy(struct FFIArray *arr); * - Iterates the array elements as pointers to FFIString and destroys each via dash_spv_ffi_string_destroy * - Frees the underlying vector buffer stored in FFIArray * - Does not free the FFIArray struct itself (safe for both stack- and heap-allocated structs) + * # Safety + * - `arr` must be either null or a valid pointer to an `FFIArray` whose elements are `*mut FFIString`. + * - Each element pointer must be valid or null; non-null entries are freed. + * - The memory referenced by `arr.data` must not be used after this call. */ void dash_spv_ffi_string_array_destroy(struct FFIArray *arr); @@ -652,6 +820,13 @@ void dash_spv_ffi_unconfirmed_transaction_destroy_addresses(struct FFIString *ad */ void dash_spv_ffi_unconfirmed_transaction_destroy(struct FFIUnconfirmedTransaction *tx); +/** + * Initialize logging for the SPV library. + * + * # Safety + * - `level` may be null or point to a valid, NUL-terminated C string. + * - If non-null, the pointer must remain valid for the duration of this call. + */ int32_t dash_spv_ffi_init_logging(const char *level); const char *dash_spv_ffi_version(void); diff --git a/swift-dash-core-sdk/Sources/KeyWalletFFI/include/key_wallet_ffi.h b/swift-dash-core-sdk/Sources/KeyWalletFFI/include/key_wallet_ffi.h index ceff25e1a..fcec5a290 100644 --- a/swift-dash-core-sdk/Sources/KeyWalletFFI/include/key_wallet_ffi.h +++ b/swift-dash-core-sdk/Sources/KeyWalletFFI/include/key_wallet_ffi.h @@ -126,29 +126,6 @@ typedef enum { SINGLE = 2, } FFIAddressPoolType; -/* - Derivation path type for DIP9 - */ -typedef enum { - PATH_UNKNOWN = 0, - PATH_BIP32 = 1, - PATH_BIP44 = 2, - PATH_BLOCKCHAIN_IDENTITIES = 3, - PATH_PROVIDER_FUNDS = 4, - PATH_PROVIDER_VOTING_KEYS = 5, - PATH_PROVIDER_OPERATOR_KEYS = 6, - PATH_PROVIDER_OWNER_KEYS = 7, - PATH_CONTACT_BASED_FUNDS = 8, - PATH_CONTACT_BASED_FUNDS_ROOT = 9, - PATH_CONTACT_BASED_FUNDS_EXTERNAL = 10, - PATH_BLOCKCHAIN_IDENTITY_CREDIT_REGISTRATION_FUNDING = 11, - PATH_BLOCKCHAIN_IDENTITY_CREDIT_TOPUP_FUNDING = 12, - PATH_BLOCKCHAIN_IDENTITY_CREDIT_INVITATION_FUNDING = 13, - PATH_PROVIDER_PLATFORM_NODE_KEYS = 14, - PATH_COIN_JOIN = 15, - PATH_ROOT = 255, -} FFIDerivationPathType; - /* FFI Error code */ @@ -639,7 +616,6 @@ typedef struct { } FFITxOutput; /* - Transaction context for checking Transaction check result */ typedef struct { @@ -1399,6 +1375,224 @@ FFIAccountCollectionSummary *account_collection_summary_data(const FFIAccountCol void account_collection_summary_free(FFIAccountCollectionSummary *summary) ; +/* + Derive an extended private key from an account at a given index, using the provided master xpriv. + + Returns an opaque FFIExtendedPrivateKey pointer that must be freed with `extended_private_key_free`. + + Notes: + - This is chain-agnostic. For accounts with internal/external chains, this returns an error. + - For hardened-only account types (e.g., EdDSA), a hardened index is used. + + # Safety + - `account` and `master_xpriv` must be valid, non-null pointers allocated by this library. + - `error` must be a valid pointer to an FFIError or null. + - The caller must free the returned pointer with `extended_private_key_free`. + */ + +FFIExtendedPrivateKey *account_derive_extended_private_key_at(const FFIAccount *account, + const FFIExtendedPrivateKey *master_xpriv, + unsigned int index, + FFIError *error) +; + +/* + Derive a BLS private key from a raw seed buffer at the given index. + + Returns a newly allocated hex string of the 32-byte private key. The caller must free + it with `string_free`. + + Notes: + - Uses the account's network for master key creation. + - Chain-agnostic; may return an error for accounts with internal/external chains. + + # Safety + - `account` must be a valid, non-null pointer to an `FFIBLSAccount` (only when `bls` feature is enabled). + - `seed` must point to a readable buffer of length `seed_len` (1..=64 bytes expected). + - `error` must be a valid pointer to an FFIError or null. + - Returned string must be freed with `string_free`. + */ + +char *bls_account_derive_private_key_from_seed(const FFIBLSAccount *account, + const uint8_t *seed, + size_t seed_len, + unsigned int index, + FFIError *error) +; + +/* + Derive a BLS private key from a mnemonic + optional passphrase at the given index. + + Returns a newly allocated hex string of the 32-byte private key. The caller must free + it with `string_free`. + + Notes: + - Uses the English wordlist for parsing the mnemonic. + - Chain-agnostic; may return an error for accounts with internal/external chains. + + # Safety + - `account` must be a valid, non-null pointer to an `FFIBLSAccount` (only when `bls` feature is enabled). + - `mnemonic` must be a valid, null-terminated UTF-8 C string. + - `passphrase` may be null; if not null, must be a valid UTF-8 C string. + - `error` must be a valid pointer to an FFIError or null. + - Returned string must be freed with `string_free`. + */ + +char *bls_account_derive_private_key_from_mnemonic(const FFIBLSAccount *account, + const char *mnemonic, + const char *passphrase, + unsigned int index, + FFIError *error) +; + +/* + Derive an EdDSA (ed25519) private key from a raw seed buffer at the given index. + + Returns a newly allocated hex string of the 32-byte private key. The caller must free + it with `string_free`. + + Notes: + - EdDSA only supports hardened derivation; the index will be used accordingly. + - Chain-agnostic; EdDSA accounts typically do not have internal/external split. + + # Safety + - `account` must be a valid, non-null pointer to an `FFIEdDSAAccount` (only when `eddsa` feature is enabled). + - `seed` must point to a readable buffer of length `seed_len` (1..=64 bytes expected). + - `error` must be a valid pointer to an FFIError or null. + - Returned string must be freed with `string_free`. + */ + +char *eddsa_account_derive_private_key_from_seed(const FFIEdDSAAccount *account, + const uint8_t *seed, + size_t seed_len, + unsigned int index, + FFIError *error) +; + +/* + Derive an EdDSA (ed25519) private key from a mnemonic + optional passphrase at the given index. + + Returns a newly allocated hex string of the 32-byte private key. The caller must free + it with `string_free`. + + Notes: + - Uses the English wordlist for parsing the mnemonic. + + # Safety + - `account` must be a valid, non-null pointer to an `FFIEdDSAAccount` (only when `eddsa` feature is enabled). + - `mnemonic` must be a valid, null-terminated UTF-8 C string. + - `passphrase` may be null; if not null, must be a valid UTF-8 C string. + - `error` must be a valid pointer to an FFIError or null. + - Returned string must be freed with `string_free`. + */ + +char *eddsa_account_derive_private_key_from_mnemonic(const FFIEdDSAAccount *account, + const char *mnemonic, + const char *passphrase, + unsigned int index, + FFIError *error) +; + +/* + Derive a private key (secp256k1) from an account at a given chain/index, using the provided master xpriv. + Returns an opaque FFIPrivateKey pointer that must be freed with `private_key_free`. + + # Safety + - `account` and `master_xpriv` must be valid pointers allocated by this library + - `error` must be a valid pointer to an FFIError or null + */ + +FFIPrivateKey *account_derive_private_key_at(const FFIAccount *account, + const FFIExtendedPrivateKey *master_xpriv, + unsigned int index, + FFIError *error) +; + +/* + Derive a private key from an account at a given chain/index and return as WIF string. + Caller must free the returned string with `string_free`. + + # Safety + - `account` and `master_xpriv` must be valid pointers allocated by this library + - `error` must be a valid pointer to an FFIError or null + */ + +char *account_derive_private_key_as_wif_at(const FFIAccount *account, + const FFIExtendedPrivateKey *master_xpriv, + unsigned int index, + FFIError *error) +; + +/* + Derive an extended private key from a raw seed buffer at the given index. + Returns an opaque FFIExtendedPrivateKey pointer that must be freed with `extended_private_key_free`. + + # Safety + - `account` must be a valid pointer to an FFIAccount + - `seed` must point to a valid buffer of length `seed_len` + - `error` must be a valid pointer to an FFIError or null + */ + +FFIExtendedPrivateKey *account_derive_extended_private_key_from_seed(const FFIAccount *account, + const uint8_t *seed, + size_t seed_len, + unsigned int index, + FFIError *error) +; + +/* + Derive a private key from a raw seed buffer at the given index. + Returns an opaque FFIPrivateKey pointer that must be freed with `private_key_free`. + + # Safety + - `account` must be a valid pointer to an FFIAccount + - `seed` must point to a valid buffer of length `seed_len` + - `error` must be a valid pointer to an FFIError or null + */ + +FFIPrivateKey *account_derive_private_key_from_seed(const FFIAccount *account, + const uint8_t *seed, + size_t seed_len, + unsigned int index, + FFIError *error) +; + +/* + Derive an extended private key from a mnemonic + optional passphrase at the given index. + Returns an opaque FFIExtendedPrivateKey pointer that must be freed with `extended_private_key_free`. + + # Safety + - `account` must be a valid pointer to an FFIAccount + - `mnemonic` must be a valid, null-terminated C string + - `passphrase` may be null; if not null, must be a valid C string + - `error` must be a valid pointer to an FFIError or null + */ + +FFIExtendedPrivateKey *account_derive_extended_private_key_from_mnemonic(const FFIAccount *account, + const char *mnemonic, + const char *passphrase, + unsigned int index, + FFIError *error) +; + +/* + Derive a private key from a mnemonic + optional passphrase at the given index. + Returns an opaque FFIPrivateKey pointer that must be freed with `private_key_free`. + + # Safety + - `account` must be a valid pointer to an FFIAccount + - `mnemonic` must be a valid, null-terminated C string + - `passphrase` may be null; if not null, must be a valid C string + - `error` must be a valid pointer to an FFIError or null + */ + +FFIPrivateKey *account_derive_private_key_from_mnemonic(const FFIAccount *account, + const char *mnemonic, + const char *passphrase, + unsigned int index, + FFIError *error) +; + /* Free address string @@ -1792,25 +1986,6 @@ bool derivation_xpub_fingerprint(const FFIExtendedPubKey *xpub, */ void derivation_string_free(char *s) ; -/* - Derive key using DIP9 path constants for identity - - # Safety - - - `seed` must be a valid pointer to a byte array of `seed_len` length - - `error` must be a valid pointer to an FFIError structure or null - - The caller must ensure the seed pointer remains valid for the duration of this call - */ - -FFIExtendedPrivKey *dip9_derive_identity_key(const uint8_t *seed, - size_t seed_len, - FFINetwork network, - unsigned int identity_index, - unsigned int key_index, - FFIDerivationPathType key_type, - FFIError *error) -; - /* Derive an address from a private key diff --git a/swift-dash-core-sdk/Sources/SwiftDashCoreSDK/Models/Network.swift b/swift-dash-core-sdk/Sources/SwiftDashCoreSDK/Models/Network.swift index 06c547ae8..4a23dfbc9 100644 --- a/swift-dash-core-sdk/Sources/SwiftDashCoreSDK/Models/Network.swift +++ b/swift-dash-core-sdk/Sources/SwiftDashCoreSDK/Models/Network.swift @@ -16,7 +16,7 @@ public enum DashNetwork: String, Codable, CaseIterable, Sendable { case .regtest: return 19899 case .devnet: - return 19799 + return 29999 } } @@ -55,4 +55,4 @@ public enum DashNetwork: String, Codable, CaseIterable, Sendable { return nil } } -} \ No newline at end of file +} diff --git a/swift-dash-core-sdk/Tests/SwiftDashCoreSDKTests/DashSDKTests.swift b/swift-dash-core-sdk/Tests/SwiftDashCoreSDKTests/DashSDKTests.swift index 1b5acb30c..434814e58 100644 --- a/swift-dash-core-sdk/Tests/SwiftDashCoreSDKTests/DashSDKTests.swift +++ b/swift-dash-core-sdk/Tests/SwiftDashCoreSDKTests/DashSDKTests.swift @@ -49,7 +49,7 @@ final class DashSDKTests: XCTestCase { XCTAssertEqual(DashNetwork.mainnet.defaultPort, 9999) XCTAssertEqual(DashNetwork.testnet.defaultPort, 19999) XCTAssertEqual(DashNetwork.regtest.defaultPort, 19899) - XCTAssertEqual(DashNetwork.devnet.defaultPort, 19799) + XCTAssertEqual(DashNetwork.devnet.defaultPort, 29999) } func testBalanceCalculations() { @@ -279,4 +279,4 @@ final class StorageIntegrationTests: XCTestCase { let all = try storage.fetchUTXOs(includeSpent: true) XCTAssertEqual(all.count, 2) } -} \ No newline at end of file +}