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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = ["dash", "dash-network", "dash-network-ffi", "hashes", "internals", "f
resolver = "2"

[workspace.package]
version = "0.39.6"
version = "0.40.0"

[patch.crates-io]
dashcore_hashes = { path = "hashes" }
Expand Down
2 changes: 1 addition & 1 deletion dash-network-ffi/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dash-network-ffi"
version.workspace = true
version = { workspace = true }
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Switch to workspace versioning is correct; add MSRV.

Per project guidelines, declare MSRV explicitly.

 [package]
 name = "dash-network-ffi"
 version = { workspace = true }
+rust-version = "1.89"
 edition = "2021"
🤖 Prompt for AI Agents
In dash-network-ffi/Cargo.toml at line 3, the package is using workspace
versioning but does not declare the MSRV; add a rust-version entry in the
package table (rust-version = "<PROJECT_MSRV>") set to the project’s documented
minimum supported Rust version so the crate explicitly declares the MSRV and
stays consistent with workspace/CI.

edition = "2021"
authors = ["Quantum Explorer <[email protected]>"]
license = "CC0-1.0"
Expand Down
2 changes: 1 addition & 1 deletion dash-network/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Add this to your `Cargo.toml`:

```toml
[dependencies]
dash-network = "0.39.6"
dash-network = "0.40.0"
```

### Basic Example
Expand Down
2 changes: 1 addition & 1 deletion dash-spv-ffi/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dash-spv-ffi"
version = "0.1.0"
version = { workspace = true }
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Align with workspace versioning; also set MSRV.

Declare rust-version here to satisfy the repo’s MSRV policy.

 [package]
 name = "dash-spv-ffi"
 version = { workspace = true }
+rust-version = "1.89"
 edition = "2021"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
version = { workspace = true }
[package]
name = "dash-spv-ffi"
version = { workspace = true }
rust-version = "1.89"
edition = "2021"
🤖 Prompt for AI Agents
In dash-spv-ffi/Cargo.toml around line 3, the package lacks a rust-version
declaration required by the repo MSRV policy; add a rust-version entry (e.g.
rust-version = "<MSRV>") alongside the existing version = { workspace = true }
so the crate explicitly declares the minimum supported Rust version used by the
workspace. Replace <MSRV> with the repository's agreed MSRV (for example "1.70"
or the value defined in the repo policy).

edition = "2021"
authors = ["Dash Core Developers"]
license = "MIT"
Expand Down
4 changes: 2 additions & 2 deletions dash-spv-ffi/FFI_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Functions: 25
| `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_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 (currently not supported) # Safety - `config` mus... | 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 |
| `dash_spv_ffi_config_set_wallet_creation_time` | Sets the wallet creation timestamp for synchronization optimization # Safety... | config |
| `dash_spv_ffi_config_testnet` | No description | config |
Expand Down Expand Up @@ -555,7 +555,7 @@ dash_spv_ffi_config_set_user_agent(config: *mut FFIClientConfig, user_agent: *co
```

**Description:**
Sets the user agent string (currently not supported) # Safety - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - `user_agent` must be a valid null-terminated C string - The caller must ensure both pointers remain valid for the duration of this call
Sets the user agent string to advertise in the P2P handshake # Safety - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - `user_agent` must be a valid null-terminated C 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 - `user_agent` must be a valid null-terminated C string - The caller must ensure both pointers remain valid for the duration of this call
Expand Down
2 changes: 1 addition & 1 deletion dash-spv-ffi/include/dash_spv_ffi.h
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ int32_t dash_spv_ffi_config_add_peer(FFIClientConfig *config,
const char *addr);

/**
* Sets the user agent string (currently not supported)
* Sets the user agent string to advertise in the P2P handshake
*
* # Safety
* - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet
Expand Down
11 changes: 6 additions & 5 deletions dash-spv-ffi/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ pub unsafe extern "C" fn dash_spv_ffi_config_add_peer(
}
}

/// Sets the user agent string (currently not supported)
/// Sets the user agent string to advertise in the P2P handshake
///
/// # Safety
/// - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet
Expand All @@ -162,10 +162,11 @@ pub unsafe extern "C" fn dash_spv_ffi_config_set_user_agent(

// Validate the user_agent string
match CStr::from_ptr(user_agent).to_str() {
Ok(_agent_str) => {
// user_agent is not directly settable in current ClientConfig
set_last_error("Setting user agent is not supported in current implementation");
FFIErrorCode::ConfigError as i32
Ok(agent_str) => {
// Store as-is; normalization/length capping is applied at handshake build time
let cfg = &mut (*config).inner;
cfg.user_agent = Some(agent_str.to_string());
FFIErrorCode::Success as i32
}
Err(e) => {
set_last_error(&format!("Invalid UTF-8 in user agent: {}", e));
Expand Down
2 changes: 1 addition & 1 deletion dash-spv-ffi/tests/test_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ mod tests {

let agent = CString::new("TestAgent/1.0").unwrap();
let result = dash_spv_ffi_config_set_user_agent(config, agent.as_ptr());
assert_eq!(result, FFIErrorCode::ConfigError as i32);
assert_eq!(result, FFIErrorCode::Success as i32);

dash_spv_ffi_config_destroy(config);
}
Expand Down
2 changes: 1 addition & 1 deletion dash-spv-ffi/tests/unit/test_configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ mod tests {
let user_agent = CString::new("TestAgent/1.0").unwrap();
assert_eq!(
dash_spv_ffi_config_set_user_agent(config, user_agent.as_ptr()),
FFIErrorCode::ConfigError as i32
FFIErrorCode::Success as i32
);
Comment on lines 185 to 189
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Add edge/negative tests for user_agent (invalid UTF-8, length cap, empty string behavior)

To lock in the new semantics and prevent regressions:

  • Add a unit test that passes invalid UTF-8 bytes (no interior NUL) and asserts InvalidArgument.
  • Add a test with a very long user agent (e.g., >1024 bytes) to ensure the setter succeeds and does not crash; cover normalization/capping in an SPV handshake-level test.
  • Clarify and test the expected behavior for empty user agent (accept and default vs. reject).

Example unit test snippet to add in this file:

#[test]
#[serial]
fn test_user_agent_invalid_utf8_and_long() {
    unsafe {
        let config = dash_spv_ffi_config_testnet();

        // Invalid UTF-8 sequence: 0xF0 0x28 0x8C 0x28
        let invalid = std::ffi::CString::new(vec![0xF0, 0x28, 0x8C, 0x28]).unwrap();
        let rc = dash_spv_ffi_config_set_user_agent(config, invalid.as_ptr());
        assert_eq!(rc, FFIErrorCode::InvalidArgument as i32);

        // Very long but valid ASCII user agent
        let long = "A".repeat(10_000);
        let long_c = std::ffi::CString::new(long).unwrap();
        let rc2 = dash_spv_ffi_config_set_user_agent(config, long_c.as_ptr());
        assert_eq!(rc2, FFIErrorCode::Success as i32);

        dash_spv_ffi_config_destroy(config);
    }
}

For normalization/capping, add an integration test in the SPV layer that inspects the Version message user agent after handshake and asserts it’s normalized and within the cap.

Run this quick scan to confirm presence of related FFI and handshake hooks:


🏁 Script executed:

#!/bin/bash
# Locate set/get user agent FFI functions and handshake usage
rg -nP 'dash_spv_ffi_config_(set|get)_user_agent'
rg -nP --type=c 'dash_spv_ffi_config_(set|get)_user_agent' -g '*.{h,c}'
rg -nP --type=rust -C2 '\bVersion(Message)?\b.*user[_ ]?agent|user_agent' 

Length of output: 22219


Add edge-case unit tests for user_agent setter
In dash-spv-ffi/tests/unit/test_configuration.rs, add tests for:

  • Invalid UTF-8 (e.g. bytes [0xF0, 0x28, 0x8C, 0x28]) returns FFIErrorCode::InvalidArgument.
  • Empty string returns FFIErrorCode::Success and stores "".
  • Very long ASCII string (>10 000 bytes) returns FFIErrorCode::Success without crashing.

Optionally, add an SPV handshake integration test to assert the Version message’s user_agent is normalized (starts/ends with /) and capped to MAX_USER_AGENT_LENGTH.


assert_eq!(
Expand Down
2 changes: 1 addition & 1 deletion dash-spv/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dash-spv"
version = "0.1.0"
version = { workspace = true }
edition = "2021"
authors = ["Dash Core Team"]
description = "Dash SPV (Simplified Payment Verification) client library"
Expand Down
12 changes: 12 additions & 0 deletions dash-spv/src/client/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ pub struct ClientConfig {
/// Log level for tracing.
pub log_level: String,

/// Optional user agent string to advertise in the P2P version message.
/// If not set, a sensible default is used (includes crate version).
pub user_agent: Option<String>,

/// Maximum concurrent filter requests (default: 8).
pub max_concurrent_filter_requests: usize,

Expand Down Expand Up @@ -192,6 +196,7 @@ impl Default for ClientConfig {
max_peers: 8,
enable_persistence: true,
log_level: "info".to_string(),
user_agent: None,
max_concurrent_filter_requests: 16,
enable_filter_flow_control: true,
filter_request_delay_ms: 0,
Expand Down Expand Up @@ -306,6 +311,13 @@ impl ClientConfig {
self
}

/// Set custom user agent string for the P2P handshake.
/// The library will lightly validate and normalize it during handshake.
pub fn with_user_agent(mut self, agent: impl Into<String>) -> Self {
self.user_agent = Some(agent.into());
self
}

/// Set maximum concurrent filter requests.
pub fn with_max_concurrent_filter_requests(mut self, max_requests: usize) -> Self {
self.max_concurrent_filter_requests = max_requests;
Expand Down
25 changes: 23 additions & 2 deletions dash-spv/src/network/handshake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,16 @@ pub struct HandshakeManager {
verack_received: bool,
version_sent: bool,
mempool_strategy: MempoolStrategy,
user_agent: Option<String>,
}

impl HandshakeManager {
/// Create a new handshake manager.
pub fn new(network: Network, mempool_strategy: MempoolStrategy) -> Self {
pub fn new(
network: Network,
mempool_strategy: MempoolStrategy,
user_agent: Option<String>,
) -> Self {
Self {
_network: network,
state: HandshakeState::Init,
Expand All @@ -55,6 +60,7 @@ impl HandshakeManager {
verack_received: false,
version_sent: false,
mempool_strategy,
user_agent,
}
}

Expand Down Expand Up @@ -254,14 +260,29 @@ impl HandshakeManager {
.parse()
.map_err(|_| NetworkError::AddressParse("Failed to parse local address".to_string()))?;

// Determine user agent: prefer configured value, else default to crate/version.
let default_agent = format!("/rust-dash-spv:{}/", env!("CARGO_PKG_VERSION"));
let mut ua = self.user_agent.clone().unwrap_or(default_agent);
// Normalize: ensure it starts and ends with '/'; trim if excessively long.
if !ua.starts_with('/') {
ua.insert(0, '/');
}
if !ua.ends_with('/') {
ua.push('/');
}
// Keep within a reasonable bound (match peer validation bound of 256)
if ua.len() > 256 {
ua.truncate(256);
}
Comment on lines +263 to +276
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

Possible panic on String::truncate(256) with non-ASCII UA; also allow-only printable ASCII.

truncate expects a char boundary; a multi-byte UA from config/FFI can panic. Also, per BIP14 practice, restrict to printable ASCII and cap length deterministically.

Apply:

-        let default_agent = format!("/rust-dash-spv:{}/", env!("CARGO_PKG_VERSION"));
-        let mut ua = self.user_agent.clone().unwrap_or(default_agent);
-        // Normalize: ensure it starts and ends with '/'; trim if excessively long.
-        if !ua.starts_with('/') {
-            ua.insert(0, '/');
-        }
-        if !ua.ends_with('/') {
-            ua.push('/');
-        }
-        // Keep within a reasonable bound (match peer validation bound of 256)
-        if ua.len() > 256 {
-            ua.truncate(256);
-        }
+        let default_agent = format!("/rust-dash-spv:{}/", env!("CARGO_PKG_VERSION"));
+        let raw = self.user_agent.as_deref().unwrap_or(&default_agent);
+        // Strip non-printable / non-ASCII and inner slashes; rebuild as /.../
+        let mut core = raw
+            .trim()
+            .trim_matches('/')
+            .chars()
+            .filter(|c| matches!(*c, ' '..='~')) // printable ASCII only
+            .collect::<String>();
+        // Rewrap with leading/trailing slash
+        let mut ua = String::with_capacity(core.len() + 2);
+        ua.push('/');
+        ua.push_str(&core);
+        ua.push('/');
+        // Cap to 256 bytes; ASCII ensures byte==char, safe truncate
+        if ua.len() > 256 {
+            // Preserve closing '/' by limiting core length
+            let keep = 255; // leave space for trailing '/'
+            ua.truncate(keep);
+            if !ua.ends_with('/') {
+                ua.pop(); // ensure final char slot
+                ua.push('/');
+            }
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Determine user agent: prefer configured value, else default to crate/version.
let default_agent = format!("/rust-dash-spv:{}/", env!("CARGO_PKG_VERSION"));
let mut ua = self.user_agent.clone().unwrap_or(default_agent);
// Normalize: ensure it starts and ends with '/'; trim if excessively long.
if !ua.starts_with('/') {
ua.insert(0, '/');
}
if !ua.ends_with('/') {
ua.push('/');
}
// Keep within a reasonable bound (match peer validation bound of 256)
if ua.len() > 256 {
ua.truncate(256);
}
// Determine user agent: prefer configured value, else default to crate/version.
let default_agent = format!("/rust-dash-spv:{}/", env!("CARGO_PKG_VERSION"));
let raw = self.user_agent.as_deref().unwrap_or(&default_agent);
// Strip non-printable / non-ASCII and inner slashes; rebuild as /.../
let mut core = raw
.trim()
.trim_matches('/')
.chars()
.filter(|c| matches!(*c, ' '..='~')) // printable ASCII only
.collect::<String>();
// Rewrap with leading/trailing slash
let mut ua = String::with_capacity(core.len() + 2);
ua.push('/');
ua.push_str(&core);
ua.push('/');
// Cap to 256 bytes; ASCII ensures byte==char, safe truncate
if ua.len() > 256 {
// Preserve closing '/' by limiting core length
let keep = 255; // leave space for trailing '/'
ua.truncate(keep);
if !ua.ends_with('/') {
ua.pop(); // drop last char if it's not '/', then push '/'
ua.push('/');
}
}


Ok(VersionMessage {
version: self.our_version,
services,
timestamp,
receiver: dashcore::network::address::Address::new(&address, ServiceFlags::NETWORK),
sender: dashcore::network::address::Address::new(&local_addr, services),
nonce: rand::random(),
user_agent: "/rust-dash-spv:0.1.0/".to_string(),
user_agent: ua,
start_height: 0, // SPV client starts at 0
relay: match self.mempool_strategy {
MempoolStrategy::FetchAll => true, // Want all transactions for FetchAll strategy
Expand Down
6 changes: 5 additions & 1 deletion dash-spv/src/network/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,11 @@ impl TcpNetworkManager {
Ok(Self {
config: config.clone(),
connection: None,
handshake: HandshakeManager::new(config.network, config.mempool_strategy),
handshake: HandshakeManager::new(
config.network,
config.mempool_strategy,
config.user_agent.clone(),
),
_message_handler: MessageHandler::new(),
message_sender,
dsq_preference: false,
Expand Down
8 changes: 7 additions & 1 deletion dash-spv/src/network/multi_peer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ pub struct MultiPeerNetworkManager {
read_timeout: Duration,
/// Track which peers have sent us Headers2 messages
peers_sent_headers2: Arc<Mutex<HashSet<SocketAddr>>>,
/// Optional user agent to advertise
user_agent: Option<String>,
}

impl MultiPeerNetworkManager {
Expand Down Expand Up @@ -114,6 +116,7 @@ impl MultiPeerNetworkManager {
last_message_peer: Arc::new(Mutex::new(None)),
read_timeout: config.read_timeout,
peers_sent_headers2: Arc::new(Mutex::new(HashSet::new())),
user_agent: config.user_agent.clone(),
})
}

Expand Down Expand Up @@ -202,6 +205,7 @@ impl MultiPeerNetworkManager {
let reputation_manager = self.reputation_manager.clone();
let mempool_strategy = self.mempool_strategy;
let read_timeout = self.read_timeout;
let user_agent = self.user_agent.clone();

// Spawn connection task
let mut tasks = self.tasks.lock().await;
Expand All @@ -213,7 +217,8 @@ impl MultiPeerNetworkManager {
{
Ok(mut conn) => {
// Perform handshake
let mut handshake_manager = HandshakeManager::new(network, mempool_strategy);
let mut handshake_manager =
HandshakeManager::new(network, mempool_strategy, user_agent);
match handshake_manager.perform_handshake(&mut conn).await {
Ok(_) => {
log::info!("Successfully connected to {}", addr);
Expand Down Expand Up @@ -971,6 +976,7 @@ impl Clone for MultiPeerNetworkManager {
last_message_peer: self.last_message_peer.clone(),
read_timeout: self.read_timeout,
peers_sent_headers2: self.peers_sent_headers2.clone(),
user_agent: self.user_agent.clone(),
}
}
}
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 @@ -178,6 +178,7 @@ mod multi_peer_tests {
// QRInfo fields
qr_info_extra_share: true,
qr_info_timeout: Duration::from_secs(30),
user_agent: None,
}
}

Expand Down
2 changes: 1 addition & 1 deletion dash-spv/tests/test_handshake_logic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use dashcore::Network;

#[test]
fn test_handshake_state_transitions() {
let mut handshake = HandshakeManager::new(Network::Dash, MempoolStrategy::Selective);
let mut handshake = HandshakeManager::new(Network::Dash, MempoolStrategy::Selective, None);

// Initial state should be Init
assert_eq!(*handshake.state(), HandshakeState::Init);
Expand Down
2 changes: 1 addition & 1 deletion key-wallet-ffi/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "key-wallet-ffi"
version = "0.39.6"
version = { workspace = true }
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Set MSRV in manifest (rust-version = "1.89").

Per project guidelines, explicitly declare MSRV in this crate.

 [package]
 name = "key-wallet-ffi"
-version = { workspace = true }
+version = { workspace = true }
+rust-version = "1.89"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
version = { workspace = true }
[package]
name = "key-wallet-ffi"
version = { workspace = true }
rust-version = "1.89"
🤖 Prompt for AI Agents
In key-wallet-ffi/Cargo.toml around lines 3-3, the crate manifest does not
declare the MSRV; add a rust-version = "1.89" entry to the package manifest
(place it alongside the existing version = { workspace = true } line under
[package]) so the crate explicitly declares the minimum supported Rust version.

authors = ["The Dash Core Developers"]
edition = "2021"
description = "FFI bindings for key-wallet library"
Expand Down
Loading
Loading