Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
Copy Markdown
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