Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ cobertura.xml
# Build scripts artifacts
*.log
/dash-spv-ffi/peer_reputation.json
/dash-spv/peer_reputation.json
27 changes: 22 additions & 5 deletions dash-spv-ffi/FFI_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 |
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Clarify address parsing and fix typo (“addres” → “address”); state hostname policy explicitly.

The docs say “full socket addres” and mention “IP-only string” but don’t state whether plain hostnames (e.g., “localhost”) without a port are accepted. Given PR title implies allowing localhost, please make this explicit: either document that hostnames are accepted (with or without port and what default is used) or that only IP literals are accepted unless a port is provided. Also correct “addres” → “address”.

Since this file is auto-generated, adjust the Rust doc comments at the source and re-generate.

Also applies to: 251-255

| `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 |
Expand All @@ -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 |
Expand Down Expand Up @@ -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`

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions dash-spv-ffi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -123,4 +123,4 @@ The FFI bindings are thread-safe. The client uses internal synchronization to en

## License

MIT
MIT
15 changes: 14 additions & 1 deletion dash-spv-ffi/include/dash_spv_ffi.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
*
Expand Down
74 changes: 65 additions & 9 deletions dash-spv-ffi/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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(
Expand All @@ -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::<SocketAddr>() {
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::<IpAddr>() {
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
}
}
Expand Down Expand Up @@ -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
Expand Down
19 changes: 15 additions & 4 deletions dash-spv-ffi/tests/unit/test_configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
Expand Down Expand Up @@ -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);
}
}
Expand Down
15 changes: 15 additions & 0 deletions dash-spv/src/client/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ pub struct ClientConfig {
/// List of peer addresses to connect to.
pub peers: Vec<SocketAddr>,

/// 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<PathBuf>,

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -243,6 +251,7 @@ impl ClientConfig {
Self {
network,
peers: Self::default_peers_for_network(network),
restrict_to_configured_peers: false,
..Self::default()
}
}
Expand All @@ -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);
Expand Down
18 changes: 11 additions & 7 deletions dash-spv/src/network/multi_peer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ pub struct MultiPeerNetworkManager {
peers_sent_headers2: Arc<Mutex<HashSet<SocketAddr>>>,
/// Optional user agent to advertise
user_agent: Option<String>,
/// Exclusive mode: restrict to configured peers only (no DNS or peer store)
exclusive_mode: bool,
}

impl MultiPeerNetworkManager {
Expand Down Expand Up @@ -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),
Expand All @@ -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,
})
}

Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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,
}
}
}
Expand Down
1 change: 1 addition & 0 deletions dash-spv/src/network/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading