Skip to content

Commit be06e94

Browse files
feat: allow localhost on ffi (#138)
* feat: allow localhost on ffi * updated FFI * fix * fix * fix * fix
1 parent cb46f84 commit be06e94

File tree

14 files changed

+672
-109
lines changed

14 files changed

+672
-109
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,4 @@ cobertura.xml
4646
# Build scripts artifacts
4747
*.log
4848
/dash-spv-ffi/peer_reputation.json
49+
/dash-spv/peer_reputation.json

dash-spv-ffi/FFI_API.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This document provides a comprehensive reference for all FFI (Foreign Function I
44

55
**Auto-generated**: This documentation is automatically generated from the source code. Do not edit manually.
66

7-
**Total Functions**: 63
7+
**Total Functions**: 64
88

99
## Table of Contents
1010

@@ -34,12 +34,12 @@ Functions: 4
3434

3535
### Configuration
3636

37-
Functions: 25
37+
Functions: 26
3838

3939
| Function | Description | Module |
4040
|----------|-------------|--------|
4141
| `dash_spv_ffi_client_update_config` | Update the running client's configuration | client |
42-
| `dash_spv_ffi_config_add_peer` | Adds a peer address to the configuration # Safety - `config` must be a valid... | config |
42+
| `dash_spv_ffi_config_add_peer` | Adds a peer address to the configuration Accepts either a full socket addres... | config |
4343
| `dash_spv_ffi_config_destroy` | Destroys an FFIClientConfig and frees its memory # Safety - `config` must be... | config |
4444
| `dash_spv_ffi_config_get_data_dir` | Gets the data directory path from the configuration # Safety - `config` must... | config |
4545
| `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
5858
| `dash_spv_ffi_config_set_mempool_tracking` | Enables or disables mempool tracking # Safety - `config` must be a valid poi... | config |
5959
| `dash_spv_ffi_config_set_persist_mempool` | Sets whether to persist mempool state to disk # Safety - `config` must be a ... | config |
6060
| `dash_spv_ffi_config_set_relay_transactions` | Sets whether to relay transactions (currently a no-op) # Safety - `config` m... | config |
61+
| `dash_spv_ffi_config_set_restrict_to_configured_peers` | Restrict connections strictly to configured peers (disable DNS discovery and ... | config |
6162
| `dash_spv_ffi_config_set_start_from_height` | Sets the starting block height for synchronization # Safety - `config` must ... | config |
6263
| `dash_spv_ffi_config_set_user_agent` | Sets the user agent string to advertise in the P2P handshake # Safety - `con... | config |
6364
| `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,)
247248
```
248249

249250
**Description:**
250-
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
251+
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
251252

252253
**Safety:**
253-
- `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
254+
- `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
254255

255256
**Module:** `config`
256257

@@ -532,6 +533,22 @@ Sets whether to relay transactions (currently a no-op) # Safety - `config` must
532533

533534
---
534535

536+
#### `dash_spv_ffi_config_set_restrict_to_configured_peers`
537+
538+
```c
539+
dash_spv_ffi_config_set_restrict_to_configured_peers(config: *mut FFIClientConfig, restrict: bool,) -> i32
540+
```
541+
542+
**Description:**
543+
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
544+
545+
**Safety:**
546+
- `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet
547+
548+
**Module:** `config`
549+
550+
---
551+
535552
#### `dash_spv_ffi_config_set_start_from_height`
536553
537554
```c

dash-spv-ffi/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ dash_spv_ffi_config_destroy(config);
8282
- `dash_spv_ffi_config_set_data_dir(config, path)` - Set data directory
8383
- `dash_spv_ffi_config_set_validation_mode(config, mode)` - Set validation mode
8484
- `dash_spv_ffi_config_set_max_peers(config, max)` - Set maximum peers
85-
- `dash_spv_ffi_config_add_peer(config, addr)` - Add a peer address
85+
- `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).
8686
- `dash_spv_ffi_config_destroy(config)` - Free config memory
8787
8888
### Client Operations
@@ -123,4 +123,4 @@ The FFI bindings are thread-safe. The client uses internal synchronization to en
123123
124124
## License
125125
126-
MIT
126+
MIT

dash-spv-ffi/include/dash_spv_ffi.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,9 +509,13 @@ int32_t dash_spv_ffi_config_set_max_peers(FFIClientConfig *config,
509509
/**
510510
* Adds a peer address to the configuration
511511
*
512+
* Accepts either a full socket address (e.g., "192.168.1.1:9999" or "[::1]:19999")
513+
* or an IP-only string (e.g., "127.0.0.1" or "2001:db8::1"). When an IP-only
514+
* string is given, the default P2P port for the configured network is used.
515+
*
512516
* # Safety
513517
* - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet
514-
* - `addr` must be a valid null-terminated C string containing a socket address (e.g., "192.168.1.1:9999")
518+
* - `addr` must be a valid null-terminated C string containing a socket address or IP-only string
515519
* - The caller must ensure both pointers remain valid for the duration of this call
516520
*/
517521
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,
548552
int32_t dash_spv_ffi_config_set_filter_load(FFIClientConfig *config,
549553
bool load_filters);
550554

555+
/**
556+
* Restrict connections strictly to configured peers (disable DNS discovery and peer store)
557+
*
558+
* # Safety
559+
* - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet
560+
*/
561+
int32_t dash_spv_ffi_config_set_restrict_to_configured_peers(FFIClientConfig *config,
562+
bool restrict);
563+
551564
/**
552565
* Enables or disables masternode synchronization
553566
*

dash-spv-ffi/src/config.rs

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::{null_check, set_last_error, FFIErrorCode, FFIMempoolStrategy, FFIStr
22
use dash_spv::{ClientConfig, ValidationMode};
33
use key_wallet_ffi::FFINetwork;
44
use std::ffi::CStr;
5+
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
56
use std::os::raw::c_char;
67

78
#[repr(C)]
@@ -115,9 +116,13 @@ pub unsafe extern "C" fn dash_spv_ffi_config_set_max_peers(
115116

116117
/// Adds a peer address to the configuration
117118
///
119+
/// Accepts either a full socket address (e.g., "192.168.1.1:9999" or "[::1]:19999")
120+
/// or an IP-only string (e.g., "127.0.0.1" or "2001:db8::1"). When an IP-only
121+
/// string is given, the default P2P port for the configured network is used.
122+
///
118123
/// # Safety
119124
/// - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet
120-
/// - `addr` must be a valid null-terminated C string containing a socket address (e.g., "192.168.1.1:9999")
125+
/// - `addr` must be a valid null-terminated C string containing a socket address or IP-only string
121126
/// - The caller must ensure both pointers remain valid for the duration of this call
122127
#[no_mangle]
123128
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(
127132
null_check!(config);
128133
null_check!(addr);
129134

130-
let config = &mut (*config).inner;
131-
match CStr::from_ptr(addr).to_str() {
132-
Ok(addr_str) => match addr_str.parse() {
133-
Ok(socket_addr) => {
134-
config.peers.push(socket_addr);
135+
let cfg = &mut (*config).inner;
136+
let default_port = match cfg.network {
137+
dashcore::Network::Dash => 9999,
138+
dashcore::Network::Testnet => 19999,
139+
dashcore::Network::Regtest => 19899,
140+
dashcore::Network::Devnet => 29999,
141+
_ => 9999,
142+
};
143+
144+
let addr_str = match CStr::from_ptr(addr).to_str() {
145+
Ok(s) => s.trim(),
146+
Err(e) => {
147+
set_last_error(&format!("Invalid UTF-8 in address: {}", e));
148+
return FFIErrorCode::InvalidArgument as i32;
149+
}
150+
};
151+
152+
// 1) Try parsing as full SocketAddr first (handles IPv6 [::1]:port forms)
153+
if let Ok(sock) = addr_str.parse::<SocketAddr>() {
154+
cfg.peers.push(sock);
155+
return FFIErrorCode::Success as i32;
156+
}
157+
158+
// 2) If that fails, try parsing as bare IP address and apply default port
159+
if let Ok(ip) = addr_str.parse::<IpAddr>() {
160+
let sock = SocketAddr::new(ip, default_port);
161+
cfg.peers.push(sock);
162+
return FFIErrorCode::Success as i32;
163+
}
164+
165+
// 3) Optionally attempt DNS name with explicit port only; if no port, reject
166+
if !addr_str.contains(':') {
167+
set_last_error("Missing port for hostname; supply 'host:port' or IP only");
168+
return FFIErrorCode::InvalidArgument as i32;
169+
}
170+
171+
match addr_str.to_socket_addrs() {
172+
Ok(mut iter) => match iter.next() {
173+
Some(sock) => {
174+
cfg.peers.push(sock);
135175
FFIErrorCode::Success as i32
136176
}
137-
Err(e) => {
138-
set_last_error(&format!("Invalid socket address: {}", e));
177+
None => {
178+
set_last_error(&format!("Failed to resolve address: {}", addr_str));
139179
FFIErrorCode::InvalidArgument as i32
140180
}
141181
},
142182
Err(e) => {
143-
set_last_error(&format!("Invalid UTF-8 in address: {}", e));
183+
set_last_error(&format!("Invalid address {} ({})", addr_str, e));
144184
FFIErrorCode::InvalidArgument as i32
145185
}
146186
}
@@ -209,6 +249,22 @@ pub unsafe extern "C" fn dash_spv_ffi_config_set_filter_load(
209249
FFIErrorCode::Success as i32
210250
}
211251

252+
/// Restrict connections strictly to configured peers (disable DNS discovery and peer store)
253+
///
254+
/// # Safety
255+
/// - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet
256+
#[no_mangle]
257+
pub unsafe extern "C" fn dash_spv_ffi_config_set_restrict_to_configured_peers(
258+
config: *mut FFIClientConfig,
259+
restrict: bool,
260+
) -> i32 {
261+
null_check!(config);
262+
263+
let config = &mut (*config).inner;
264+
config.restrict_to_configured_peers = restrict;
265+
FFIErrorCode::Success as i32
266+
}
267+
212268
/// Enables or disables masternode synchronization
213269
///
214270
/// # Safety

dash-spv-ffi/tests/unit/test_configuration.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ mod tests {
5858
"256.256.256.256:9999",
5959
"127.0.0.1:99999", // port too high
6060
"127.0.0.1:-1", // negative port
61-
"127.0.0.1", // missing port
61+
"localhost", // hostname without port should be rejected
6262
":9999", // missing IP
6363
":::", // invalid IPv6
6464
"localhost:abc", // non-numeric port
@@ -74,9 +74,15 @@ mod tests {
7474
assert!(!error_ptr.is_null());
7575
}
7676

77-
// Test valid addresses
78-
let valid_addrs =
79-
["127.0.0.1:9999", "192.168.1.1:8333", "[::1]:9999", "[2001:db8::1]:8333"];
77+
// Test valid addresses including IP-only forms (port inferred from network)
78+
let valid_addrs = [
79+
"127.0.0.1:9999",
80+
"192.168.1.1:8333",
81+
"[::1]:9999",
82+
"[2001:db8::1]:8333",
83+
"127.0.0.1", // IP-only v4
84+
"2001:db8::1", // IP-only v6
85+
];
8086

8187
for addr in &valid_addrs {
8288
let c_addr = CString::new(*addr).unwrap();
@@ -198,6 +204,11 @@ mod tests {
198204
FFIErrorCode::Success as i32
199205
);
200206

207+
assert_eq!(
208+
dash_spv_ffi_config_set_restrict_to_configured_peers(config, true),
209+
FFIErrorCode::Success as i32
210+
);
211+
201212
dash_spv_ffi_config_destroy(config);
202213
}
203214
}

dash-spv/src/client/config.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ pub struct ClientConfig {
3030
/// List of peer addresses to connect to.
3131
pub peers: Vec<SocketAddr>,
3232

33+
/// Restrict connections strictly to the configured peers.
34+
///
35+
/// When true, the client will not use DNS discovery or peer persistence and
36+
/// will only attempt to connect to addresses provided in `peers`.
37+
/// If no peers are configured, no outbound connections will be made.
38+
pub restrict_to_configured_peers: bool,
39+
3340
/// Optional path for persistent storage.
3441
pub storage_path: Option<PathBuf>,
3542

@@ -183,6 +190,7 @@ impl Default for ClientConfig {
183190
Self {
184191
network: Network::Dash,
185192
peers: vec![],
193+
restrict_to_configured_peers: false,
186194
storage_path: None,
187195
validation_mode: ValidationMode::Full,
188196
filter_checkpoint_interval: 1000,
@@ -243,6 +251,7 @@ impl ClientConfig {
243251
Self {
244252
network,
245253
peers: Self::default_peers_for_network(network),
254+
restrict_to_configured_peers: false,
246255
..Self::default()
247256
}
248257
}
@@ -268,6 +277,12 @@ impl ClientConfig {
268277
self
269278
}
270279

280+
/// Restrict connections to the configured peers only.
281+
pub fn with_restrict_to_configured_peers(mut self, restrict: bool) -> Self {
282+
self.restrict_to_configured_peers = restrict;
283+
self
284+
}
285+
271286
/// Set storage path.
272287
pub fn with_storage_path(mut self, path: PathBuf) -> Self {
273288
self.storage_path = Some(path);

dash-spv/src/network/multi_peer.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ pub struct MultiPeerNetworkManager {
6868
peers_sent_headers2: Arc<Mutex<HashSet<SocketAddr>>>,
6969
/// Optional user agent to advertise
7070
user_agent: Option<String>,
71+
/// Exclusive mode: restrict to configured peers only (no DNS or peer store)
72+
exclusive_mode: bool,
7173
}
7274

7375
impl MultiPeerNetworkManager {
@@ -97,6 +99,9 @@ impl MultiPeerNetworkManager {
9799
log::warn!("Failed to load peer reputation data: {}", e);
98100
}
99101

102+
// Determine exclusive mode: either explicitly requested or peers were provided
103+
let exclusive_mode = config.restrict_to_configured_peers || !config.peers.is_empty();
104+
100105
Ok(Self {
101106
pool: Arc::new(ConnectionPool::new()),
102107
discovery: Arc::new(discovery),
@@ -117,6 +122,7 @@ impl MultiPeerNetworkManager {
117122
read_timeout: config.read_timeout,
118123
peers_sent_headers2: Arc::new(Mutex::new(HashSet::new())),
119124
user_agent: config.user_agent.clone(),
125+
exclusive_mode,
120126
})
121127
}
122128

@@ -126,10 +132,7 @@ impl MultiPeerNetworkManager {
126132

127133
let mut peer_addresses = self.initial_peers.clone();
128134

129-
// If specific peers were configured via -p flag, use ONLY those (exclusive mode)
130-
let exclusive_mode = !self.initial_peers.is_empty();
131-
132-
if exclusive_mode {
135+
if self.exclusive_mode {
133136
log::info!(
134137
"Exclusive peer mode: connecting ONLY to {} specified peer(s)",
135138
self.initial_peers.len()
@@ -161,7 +164,7 @@ impl MultiPeerNetworkManager {
161164
}
162165

163166
// Connect to peers (all in exclusive mode, or up to TARGET_PEERS in normal mode)
164-
let max_connections = if exclusive_mode {
167+
let max_connections = if self.exclusive_mode {
165168
peer_addresses.len()
166169
} else {
167170
TARGET_PEERS
@@ -574,8 +577,8 @@ impl MultiPeerNetworkManager {
574577
let initial_peers = self.initial_peers.clone();
575578
let data_dir = self.data_dir.clone();
576579

577-
// Check if we're in exclusive mode (specific peers configured via -p)
578-
let exclusive_mode = !initial_peers.is_empty();
580+
// Check if we're in exclusive mode (explicit flag or peers configured)
581+
let exclusive_mode = self.exclusive_mode;
579582

580583
// Clone self for connection callback
581584
let connect_fn = {
@@ -977,6 +980,7 @@ impl Clone for MultiPeerNetworkManager {
977980
read_timeout: self.read_timeout,
978981
peers_sent_headers2: self.peers_sent_headers2.clone(),
979982
user_agent: self.user_agent.clone(),
983+
exclusive_mode: self.exclusive_mode,
980984
}
981985
}
982986
}

dash-spv/src/network/tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ mod multi_peer_tests {
129129
ClientConfig {
130130
network: Network::Regtest,
131131
peers: vec!["127.0.0.1:19899".parse().unwrap()],
132+
restrict_to_configured_peers: false,
132133
storage_path: Some(temp_dir.path().to_path_buf()),
133134
validation_mode: crate::types::ValidationMode::Basic,
134135
filter_checkpoint_interval: 1000,

0 commit comments

Comments
 (0)