Repository navigation
feat: spv ffi sync improvements related to bip158 #147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
QuantumExplorer
merged 21 commits into
v0.40-dev
from
feat/spv-ffi-sync-improvements-bip158
Sep 25, 2025
Merged
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 0949d16
feat(ffi-cli): add FFI-only CLI with logging and runtime/
PastaPastaPasta 90ba566
fix(bip158): match_any returns false for empty queries; check emptine…
PastaPastaPasta cd61634
feat(ffi): configurable Tokio workers; pull-based event draining via …
PastaPastaPasta 1e5263f
fix(wallet-manager): avoid false positives when no monitored scripts …
PastaPastaPasta c9009df
feat(sync): derive filter request window from wallet birth height and…
PastaPastaPasta 178935f
chore(cli): include wallet-affecting tx count in periodic snapshot logs
PastaPastaPasta cb1a32a
fix(wallet): detect spend-only transactions via UTXO spend tracking; …
PastaPastaPasta 9d8d5c0
docs: update FFI API documentation
PastaPastaPasta 21d6435
fix(key-wallet): resolve clippy warning for useless format!
PastaPastaPasta 6ef136f
fix(hashes): resolve clippy manual_is_multiple_of warnings
PastaPastaPasta d607521
fix(dash): resolve clippy manual_is_multiple_of warnings in taproot
PastaPastaPasta 5d7f410
fix(key-wallet): resolve clippy useless_conversion and unused import …
PastaPastaPasta 4d1e3f5
fix(dash): resolve clippy manual_is_multiple_of warnings in special t…
PastaPastaPasta 628e939
fix(dash-spv): resolve clippy manual_is_multiple_of warnings
PastaPastaPasta 901e3df
fix(dash-spv-ffi): remove unused start_event_listener method
PastaPastaPasta c79298e
test(ffi): add comprehensive test coverage for new FFI functions
PastaPastaPasta a915f78
fix(dash-spv): improve wallet birth height hint handling
PastaPastaPasta 2eaea02
fix: allow optional filter sync in ffi cli
PastaPastaPasta 4b2f06b
fix: preserve event receiver reuse after start
PastaPastaPasta fec61d5
test: flatten event callback tests
PastaPastaPasta File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)); | ||
| } | ||
|
|
||
| // Cleanup | ||
| dash_spv_ffi_client_stop(client); | ||
| dash_spv_ffi_client_destroy(client); | ||
| dash_spv_ffi_config_destroy(cfg); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prevent infinite wait when filter sync is gated off
The loop only exits once both
headers_syncedandfilter_headers_syncedflip to true. On networks or configurations where filter sync is unavailable (e.g., BIP158 gating or regtest),filter_sync_availablestays false andfilter_headers_syncednever 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.🤖 Prompt for AI Agents