Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c9f048e
feat(spv): enhance wallet transaction logging and UTXO management
PastaPastaPasta Sep 16, 2025
0949d16
feat(ffi-cli): add FFI-only CLI with logging and runtime/
PastaPastaPasta Sep 25, 2025
90ba566
fix(bip158): match_any returns false for empty queries; check emptine…
PastaPastaPasta Sep 25, 2025
cd61634
feat(ffi): configurable Tokio workers; pull-based event draining via …
PastaPastaPasta Sep 25, 2025
1e5263f
fix(wallet-manager): avoid false positives when no monitored scripts …
PastaPastaPasta Sep 25, 2025
c9009df
feat(sync): derive filter request window from wallet birth height and…
PastaPastaPasta Sep 25, 2025
178935f
chore(cli): include wallet-affecting tx count in periodic snapshot logs
PastaPastaPasta Sep 25, 2025
cb1a32a
fix(wallet): detect spend-only transactions via UTXO spend tracking; …
PastaPastaPasta Sep 25, 2025
9d8d5c0
docs: update FFI API documentation
PastaPastaPasta Sep 25, 2025
21d6435
fix(key-wallet): resolve clippy warning for useless format!
PastaPastaPasta Sep 25, 2025
6ef136f
fix(hashes): resolve clippy manual_is_multiple_of warnings
PastaPastaPasta Sep 25, 2025
d607521
fix(dash): resolve clippy manual_is_multiple_of warnings in taproot
PastaPastaPasta Sep 25, 2025
5d7f410
fix(key-wallet): resolve clippy useless_conversion and unused import …
PastaPastaPasta Sep 25, 2025
4d1e3f5
fix(dash): resolve clippy manual_is_multiple_of warnings in special t…
PastaPastaPasta Sep 25, 2025
628e939
fix(dash-spv): resolve clippy manual_is_multiple_of warnings
PastaPastaPasta Sep 25, 2025
901e3df
fix(dash-spv-ffi): remove unused start_event_listener method
PastaPastaPasta Sep 25, 2025
c79298e
test(ffi): add comprehensive test coverage for new FFI functions
PastaPastaPasta Sep 25, 2025
a915f78
fix(dash-spv): improve wallet birth height hint handling
PastaPastaPasta Sep 25, 2025
2eaea02
fix: allow optional filter sync in ffi cli
PastaPastaPasta Sep 25, 2025
4b2f06b
fix: preserve event receiver reuse after start
PastaPastaPasta Sep 25, 2025
fec61d5
test: flatten event callback tests
PastaPastaPasta Sep 25, 2025
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
5 changes: 5 additions & 0 deletions dash-spv-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ key-wallet-ffi = { path = "../key-wallet-ffi" }
key-wallet = { path = "../key-wallet" }
key-wallet-manager = { path = "../key-wallet-manager" }
rand = "0.8"
clap = { version = "4.5", features = ["derive"] }

[dev-dependencies]
tempfile = "3.8"
Expand All @@ -39,3 +40,7 @@ dashcore-test-utils = { path = "../test-utils" }

[build-dependencies]
cbindgen = "0.29"

[[bin]]
name = "dash-spv-ffi"
path = "src/bin/ffi_cli.rs"
40 changes: 37 additions & 3 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**: 68
**Total Functions**: 70

## Table of Contents

Expand Down Expand Up @@ -34,7 +34,7 @@ Functions: 4

### Configuration

Functions: 26
Functions: 27

| Function | Description | Module |
|----------|-------------|--------|
Expand Down Expand Up @@ -63,6 +63,7 @@ Functions: 26
| `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_set_worker_threads` | Sets the number of Tokio worker threads for the FFI runtime (0 = auto) # Saf... | config |
| `dash_spv_ffi_config_testnet` | No description | config |

### Synchronization
Expand Down Expand Up @@ -119,10 +120,11 @@ Functions: 4

### Event Callbacks

Functions: 1
Functions: 2

| Function | Description | Module |
|----------|-------------|--------|
| `dash_spv_ffi_client_drain_events` | Drain pending events and invoke configured callbacks (non-blocking) | client |
| `dash_spv_ffi_client_set_event_callbacks` | Set event callbacks for the client | client |

### Error Handling
Expand Down Expand Up @@ -617,6 +619,22 @@ Sets the wallet creation timestamp for synchronization optimization # Safety -

---

#### `dash_spv_ffi_config_set_worker_threads`

```c
dash_spv_ffi_config_set_worker_threads(config: *mut FFIClientConfig, threads: u32,) -> i32
```

**Description:**
Sets the number of Tokio worker threads for the FFI runtime (0 = auto) # Safety - `config` must be a valid pointer to an FFIClientConfig

**Safety:**
- `config` must be a valid pointer to an FFIClientConfig

**Module:** `config`

---

#### `dash_spv_ffi_config_testnet`

```c
Expand Down Expand Up @@ -905,6 +923,22 @@ This function is unsafe because: - The caller must ensure the handle pointer is

### Event Callbacks - Detailed

#### `dash_spv_ffi_client_drain_events`

```c
dash_spv_ffi_client_drain_events(client: *mut FFIDashSpvClient) -> i32
```

**Description:**
Drain pending events and invoke configured callbacks (non-blocking). # Safety - `client` must be a valid, non-null pointer.

**Safety:**
- `client` must be a valid, non-null pointer.

**Module:** `client`

---

#### `dash_spv_ffi_client_set_event_callbacks`

```c
Expand Down
17 changes: 17 additions & 0 deletions dash-spv-ffi/include/dash_spv_ffi.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ typedef struct FFIArray {

typedef struct FFIClientConfig {
void *inner;
uint32_t worker_threads;

} FFIClientConfig;

Expand Down Expand Up @@ -281,6 +282,14 @@ struct FFIArray dash_spv_ffi_checkpoints_between_heights(FFINetwork network,
*/
struct FFIDashSpvClient *dash_spv_ffi_client_new(const struct FFIClientConfig *config) ;

/**
* Drain pending events and invoke configured callbacks (non-blocking).
*
* # Safety
* - `client` must be a valid, non-null pointer.
*/
int32_t dash_spv_ffi_client_drain_events(struct FFIDashSpvClient *client) ;

/**
* Update the running client's configuration.
*
Expand Down Expand Up @@ -702,6 +711,14 @@ int32_t dash_spv_ffi_config_set_masternode_sync_enabled(struct FFIClientConfig *
void dash_spv_ffi_config_destroy(struct FFIClientConfig *config)
;

/**
* Sets the number of Tokio worker threads for the FFI runtime (0 = auto)
*
* # Safety
* - `config` must be a valid pointer to an FFIClientConfig
*/
int32_t dash_spv_ffi_config_set_worker_threads(struct FFIClientConfig *config, uint32_t threads) ;

/**
* Enables or disables mempool tracking
*
Expand Down
221 changes: 221 additions & 0 deletions dash-spv-ffi/src/bin/ffi_cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_void};
use std::ptr;
use std::thread;
use std::time::Duration;

use clap::{Arg, ArgAction, Command, ValueEnum};

use dash_spv_ffi::*;
use key_wallet_ffi::FFINetwork;

#[derive(Copy, Clone, Debug, ValueEnum)]
enum NetworkOpt {
Mainnet,
Testnet,
Regtest,
}

fn ffi_string_to_rust(s: *const c_char) -> String {
if s.is_null() {
return String::new();
}
unsafe { CStr::from_ptr(s) }.to_str().unwrap_or_default().to_owned()
}

extern "C" fn on_filter_headers_progress(filter: u32, headers: u32, pct: f64, _ud: *mut c_void) {
println!("filters: {} headers: {} progress: {:.2}%", filter, headers, pct * 100.0);
}

extern "C" fn on_detailed_progress(progress: *const FFIDetailedSyncProgress, _ud: *mut c_void) {
if progress.is_null() {
return;
}
unsafe {
let p = &*progress;
println!(
"height {}/{} {:.2}% peers {} hps {:.1}",
p.current_height,
p.total_height,
p.percentage * 100.0,
p.connected_peers,
p.headers_per_second
);
}
}

extern "C" fn on_completion(success: bool, msg: *const c_char, _ud: *mut c_void) {
let m = ffi_string_to_rust(msg);
if success {
println!("Completed: {}", m);
} else {
eprintln!("Failed: {}", m);
}
}

fn main() {
env_logger::init();

let matches = Command::new("dash-spv-ffi")
.about("Run SPV sync via FFI")
.arg(
Arg::new("network")
.long("network")
.short('n')
.value_parser(clap::builder::PossibleValuesParser::new([
"mainnet", "testnet", "regtest",
]))
.default_value("mainnet"),
)
.arg(
Arg::new("peer")
.long("peer")
.short('p')
.action(ArgAction::Append)
.help("Peer address host:port (repeatable)"),
)
.arg(
Arg::new("workers")
.long("workers")
.value_parser(clap::value_parser!(u32))
.help("Tokio worker threads (0=auto)"),
)
.arg(
Arg::new("log-level")
.long("log-level")
.value_parser(["error", "warn", "info", "debug", "trace"])
.default_value("info")
.help("Tracing log level"),
)
.arg(
Arg::new("start-height")
.long("start-height")
.value_parser(clap::value_parser!(u32))
.help("Start syncing from nearest checkpoint at height"),
)
.arg(
Arg::new("no-masternodes")
.long("no-masternodes")
.action(ArgAction::SetTrue)
.help("Disable masternode list synchronization"),
)
.get_matches();

// Map network
let network = match matches.get_one::<String>("network").map(|s| s.as_str()) {
Some("mainnet") => FFINetwork::Dash,
Some("testnet") => FFINetwork::Testnet,
Some("regtest") => FFINetwork::Regtest,
_ => FFINetwork::Dash,
};

unsafe {
// Initialize tracing/logging via FFI so `tracing::info!` emits output
let level = matches.get_one::<String>("log-level").map(String::as_str).unwrap_or("info");
let level_c = CString::new(level).unwrap();
let _ = dash_spv_ffi_init_logging(level_c.as_ptr());

// Build config
let cfg = dash_spv_ffi_config_new(network);
if cfg.is_null() {
eprintln!(
"Failed to allocate config: {}",
ffi_string_to_rust(dash_spv_ffi_get_last_error())
);
std::process::exit(1);
}

if let Some(workers) = matches.get_one::<u32>("workers") {
let _ = dash_spv_ffi_config_set_worker_threads(cfg, *workers);
}

if let Some(height) = matches.get_one::<u32>("start-height") {
let _ = dash_spv_ffi_config_set_start_from_height(cfg, *height);
}

if matches.get_flag("no-masternodes") {
let _ = dash_spv_ffi_config_set_masternode_sync_enabled(cfg, false);
}

if let Some(peers) = matches.get_many::<String>("peer") {
for p in peers {
let c = CString::new(p.as_str()).unwrap();
let rc = dash_spv_ffi_config_add_peer(cfg, c.as_ptr());
if rc != FFIErrorCode::Success as i32 {
eprintln!(
"Invalid peer {}: {}",
p,
ffi_string_to_rust(dash_spv_ffi_get_last_error())
);
}
}
}

// Create client
let client = dash_spv_ffi_client_new(cfg);
if client.is_null() {
eprintln!(
"Client create failed: {}",
ffi_string_to_rust(dash_spv_ffi_get_last_error())
);
std::process::exit(1);
}

// Set minimal event callbacks (progress via filter headers)
let callbacks = FFIEventCallbacks {
on_block: None,
on_transaction: None,
on_balance_update: None,
on_mempool_transaction_added: None,
on_mempool_transaction_confirmed: None,
on_mempool_transaction_removed: None,
on_compact_filter_matched: None,
on_wallet_transaction: None,
on_filter_headers_progress: Some(on_filter_headers_progress),
user_data: ptr::null_mut(),
};
let _ = dash_spv_ffi_client_set_event_callbacks(client, callbacks);

// Start client
let rc = dash_spv_ffi_client_start(client);
if rc != FFIErrorCode::Success as i32 {
eprintln!("Start failed: {}", ffi_string_to_rust(dash_spv_ffi_get_last_error()));
std::process::exit(1);
}

// Run sync on this thread; detailed progress will print via callback
let rc = dash_spv_ffi_client_sync_to_tip_with_progress(
client,
Some(on_detailed_progress),
Some(on_completion),
ptr::null_mut(),
);
if rc != FFIErrorCode::Success as i32 {
eprintln!("Sync failed: {}", ffi_string_to_rust(dash_spv_ffi_get_last_error()));
std::process::exit(1);
}

// Wait for sync completion by polling basic progress flags; drain events meanwhile
loop {
let _ = dash_spv_ffi_client_drain_events(client);
let prog_ptr = dash_spv_ffi_client_get_sync_progress(client);
if !prog_ptr.is_null() {
let prog = &*prog_ptr;
if prog.headers_synced && prog.filter_headers_synced {
dash_spv_ffi_sync_progress_destroy(prog_ptr);
break;
}
dash_spv_ffi_sync_progress_destroy(prog_ptr);
} else {
// If progress is unavailable, assume sync finished or errored
break;
}
thread::sleep(Duration::from_millis(300));
}
Comment on lines +208 to +227

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

Prevent infinite wait when filter sync is gated off

The loop only exits once both headers_synced and filter_headers_synced flip to true. On networks or configurations where filter sync is unavailable (e.g., BIP158 gating or regtest), filter_sync_available stays false and filter_headers_synced never becomes true, so this busy-waits forever even though the sync is effectively done. We should treat filters as optional when the runtime reports them unavailable and only wait for masternodes when we didn’t explicitly disable them.

-    unsafe {
+    let disable_masternodes = matches.get_flag("no-masternodes");
+
+    unsafe {
@@
-        if matches.get_flag("no-masternodes") {
+        if disable_masternodes {
             let _ = dash_spv_ffi_config_set_masternode_sync_enabled(cfg, false);
         }
@@
-        loop {
+        loop {
             let _ = dash_spv_ffi_client_drain_events(client);
             let prog_ptr = dash_spv_ffi_client_get_sync_progress(client);
             if !prog_ptr.is_null() {
                 let prog = &*prog_ptr;
-                if prog.headers_synced && prog.filter_headers_synced {
+                let filters_done = !prog.filter_sync_available || prog.filter_headers_synced;
+                let masternodes_done = disable_masternodes || prog.masternodes_synced;
+                if prog.headers_synced && filters_done && masternodes_done {
                     dash_spv_ffi_sync_progress_destroy(prog_ptr);
                     break;
                 }
                 dash_spv_ffi_sync_progress_destroy(prog_ptr);
             } else {

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In dash-spv-ffi/src/bin/ffi_cli.rs around lines 198 to 214, the loop currently
waits for both headers_synced and filter_headers_synced which can hang when
filter sync is unavailable; change the exit condition to consider filters
optional by treating filter_headers_synced as satisfied if
prog.filter_sync_available is false or if the user explicitly disabled filter
sync (check the CLI/runtime flag you pass into the client), i.e. break when
prog.headers_synced is true AND (prog.filter_headers_synced is true OR
prog.filter_sync_available is false OR disable_filter_sync_flag is true); ensure
you still call dash_spv_ffi_sync_progress_destroy(prog_ptr) before breaking and
preserve the drain/sleep behavior.


// Cleanup
dash_spv_ffi_client_stop(client);
dash_spv_ffi_client_destroy(client);
dash_spv_ffi_config_destroy(cfg);
}
}
Loading
Loading