Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
3 changes: 1 addition & 2 deletions dash-spv-ffi/include/dash_spv_ffi.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ namespace dash_spv_ffi {
typedef enum FFIMempoolStrategy {
FetchAll = 0,
BloomFilter = 1,
Selective = 2,
} FFIMempoolStrategy;

typedef enum FFISyncStage {
Expand Down Expand Up @@ -817,7 +816,7 @@ int32_t dash_spv_ffi_config_set_persist_mempool(struct FFIClientConfig *config,
*
* # Safety
* - `config` must be a valid pointer to an FFIClientConfig or null
* - If null, returns FFIMempoolStrategy::Selective as default
* - If null, returns FFIMempoolStrategy::FetchAll as default
*/

enum FFIMempoolStrategy dash_spv_ffi_config_get_mempool_strategy(const struct FFIClientConfig *config)
Expand Down
7 changes: 6 additions & 1 deletion dash-spv-ffi/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,12 @@ impl FFIDashSpvClient {
dash_spv::types::SpvEvent::ChainLockReceived {
..
} => {}
dash_spv::types::SpvEvent::InstantLockReceived {
..
} => {
// InstantLock received and validated
// TODO: Add FFI callback if needed for instant lock notifications
}
dash_spv::types::SpvEvent::MempoolTransactionAdded {
ref txid,
amount,
Expand Down Expand Up @@ -1468,7 +1474,6 @@ pub unsafe extern "C" fn dash_spv_ffi_client_record_send(
}
}
};
spv_client.record_transaction_send(txid).await;
let mut guard = inner.lock().unwrap();
*guard = Some(spv_client);
Ok(())
Expand Down
4 changes: 2 additions & 2 deletions dash-spv-ffi/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -503,13 +503,13 @@ pub unsafe extern "C" fn dash_spv_ffi_config_get_mempool_tracking(
///
/// # Safety
/// - `config` must be a valid pointer to an FFIClientConfig or null
/// - If null, returns FFIMempoolStrategy::Selective as default
/// - If null, returns FFIMempoolStrategy::FetchAll as default
#[no_mangle]
pub unsafe extern "C" fn dash_spv_ffi_config_get_mempool_strategy(
config: *const FFIClientConfig,
) -> FFIMempoolStrategy {
if config.is_null() {
return FFIMempoolStrategy::Selective;
return FFIMempoolStrategy::FetchAll;
}

let config = unsafe { &*((*config).inner as *const ClientConfig) };
Expand Down
3 changes: 0 additions & 3 deletions dash-spv-ffi/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,15 +395,13 @@ pub unsafe extern "C" fn dash_spv_ffi_string_array_destroy(arr: *mut FFIArray) {
pub enum FFIMempoolStrategy {
FetchAll = 0,
BloomFilter = 1,
Selective = 2,
}

impl From<MempoolStrategy> for FFIMempoolStrategy {
fn from(strategy: MempoolStrategy) -> Self {
match strategy {
MempoolStrategy::FetchAll => FFIMempoolStrategy::FetchAll,
MempoolStrategy::BloomFilter => FFIMempoolStrategy::BloomFilter,
MempoolStrategy::Selective => FFIMempoolStrategy::Selective,
}
}
}
Expand All @@ -413,7 +411,6 @@ impl From<FFIMempoolStrategy> for MempoolStrategy {
match strategy {
FFIMempoolStrategy::FetchAll => MempoolStrategy::FetchAll,
FFIMempoolStrategy::BloomFilter => MempoolStrategy::BloomFilter,
FFIMempoolStrategy::Selective => MempoolStrategy::Selective,
}
}
}
Expand Down
47 changes: 36 additions & 11 deletions dash-spv/src/client/chainlock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,16 @@ impl<
let chain_state = self.state.read().await;
{
let mut storage = self.storage.lock().await;
self.chainlock_manager
if let Err(e) = self
.chainlock_manager
.process_chain_lock(chainlock.clone(), &chain_state, &mut *storage)
.await
.map_err(SpvError::Validation)?;
{
// Penalize the peer that relayed the invalid ChainLock
let reason = format!("Invalid ChainLock: {}", e);
let _ = self.network.penalize_last_message_peer_invalid_chainlock(&reason).await;
return Err(SpvError::Validation(e));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
drop(chain_state);

Expand Down Expand Up @@ -89,20 +95,39 @@ impl<
) -> Result<()> {
tracing::info!("Processing InstantSendLock for tx {}", islock.txid);

// TODO: Implement InstantSendLock validation
// - Verify BLS signature against known quorum
// - Check if all inputs are locked
// - Mark transaction as instantly confirmed
// - Store InstantSendLock for future reference
// Get the masternode engine from sync manager for proper quorum verification
let masternode_engine = self.sync_manager.get_masternode_engine().ok_or_else(|| {
SpvError::Validation(crate::error::ValidationError::MasternodeVerification(
"Masternode engine not available for InstantLock verification".to_string(),
))
})?;

// Validate the InstantLock (structure + BLS signature)
// This is REQUIRED for security - never accept InstantLocks without signature verification
let validator = crate::validation::instantlock::InstantLockValidator::new();
if let Err(e) = validator.validate(&islock, masternode_engine) {
// Penalize the peer that relayed the invalid InstantLock
let reason = format!("Invalid InstantLock: {}", e);
tracing::warn!("{}", reason);

// Ban the peer using the reputation system
let _ = self.network.penalize_last_message_peer_invalid_instantlock(&reason).await;

return Err(SpvError::Validation(e));
}

// For now, just log the InstantSendLock details
tracing::info!(
"InstantSendLock validated: txid={}, inputs={}, signature={:?}",
"InstantSendLock validated successfully: txid={}, inputs={}",
islock.txid,
islock.inputs.len(),
islock.signature.to_string().chars().take(20).collect::<String>()
islock.inputs.len()
);

// Emit InstantLock event
self.emit_event(SpvEvent::InstantLockReceived {
txid: islock.txid,
inputs: islock.inputs.clone(),
});

Ok(())
}

Expand Down
27 changes: 4 additions & 23 deletions dash-spv/src/client/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,10 @@ use crate::types::ValidationMode;
/// Strategy for handling mempool (unconfirmed) transactions.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MempoolStrategy {
/// Fetch all announced transactions (poor privacy, high bandwidth).
/// Fetch all announced transactions (high bandwidth, sees all transactions).
FetchAll,
/// Use BIP37 bloom filters (moderate privacy, good efficiency).
BloomFilter,
/// Only fetch when recently sent or from known addresses (good privacy, default).
Selective,
}

/// Configuration for the Dash SPV client.
Expand Down Expand Up @@ -132,9 +130,6 @@ pub struct ClientConfig {
/// Time after which unconfirmed transactions are pruned (seconds).
pub mempool_timeout_secs: u64,

/// Time window for recent sends in selective mode (seconds).
pub recent_send_window_secs: u64,

/// Whether to fetch transactions from INV messages immediately.
pub fetch_mempool_transactions: bool,

Expand Down Expand Up @@ -232,11 +227,10 @@ impl Default for ClientConfig {
max_filter_gap_restart_attempts: 5,
max_filter_gap_sync_size: 50000,
// Mempool defaults
enable_mempool_tracking: false,
mempool_strategy: MempoolStrategy::Selective,
enable_mempool_tracking: true,
mempool_strategy: MempoolStrategy::FetchAll,
max_mempool_transactions: 1000,
mempool_timeout_secs: 3600, // 1 hour
recent_send_window_secs: 300, // 5 minutes
mempool_timeout_secs: 3600, // 1 hour
fetch_mempool_transactions: true,
persist_mempool: false,
// Request control defaults
Expand Down Expand Up @@ -388,12 +382,6 @@ impl ClientConfig {
self
}

/// Set recent send window for selective strategy.
pub fn with_recent_send_window(mut self, window_secs: u64) -> Self {
self.recent_send_window_secs = window_secs;
self
}

/// Enable or disable mempool persistence.
pub fn with_mempool_persistence(mut self, enabled: bool) -> Self {
self.persist_mempool = enabled;
Expand Down Expand Up @@ -449,13 +437,6 @@ impl ClientConfig {
if self.mempool_timeout_secs == 0 {
return Err("mempool_timeout_secs must be > 0".to_string());
}
if self.mempool_strategy == MempoolStrategy::Selective
&& self.recent_send_window_secs == 0
{
return Err(
"recent_send_window_secs must be > 0 for Selective strategy".to_string()
);
}
}

Ok(())
Expand Down
1 change: 0 additions & 1 deletion dash-spv/src/client/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@ impl<
// TODO: Get monitored addresses from wallet
self.mempool_filter = Some(Arc::new(MempoolFilter::new(
self.config.mempool_strategy,
Duration::from_secs(self.config.recent_send_window_secs),
self.config.max_mempool_transactions,
self.mempool_state.clone(),
HashSet::new(), // Will be populated from wallet's monitored addresses
Expand Down
9 changes: 0 additions & 9 deletions dash-spv/src/client/mempool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ impl<
// TODO: Get monitored addresses from wallet
self.mempool_filter = Some(Arc::new(MempoolFilter::new(
self.config.mempool_strategy,
Duration::from_secs(self.config.recent_send_window_secs),
self.config.max_mempool_transactions,
self.mempool_state.clone(),
HashSet::new(), // Will be populated from wallet's monitored addresses
Expand Down Expand Up @@ -147,19 +146,11 @@ impl<
// For now, create empty filter until wallet integration is complete
self.mempool_filter = Some(Arc::new(MempoolFilter::new(
self.config.mempool_strategy,
Duration::from_secs(self.config.recent_send_window_secs),
self.config.max_mempool_transactions,
self.mempool_state.clone(),
HashSet::new(), // Will be populated from wallet's monitored addresses
self.config.network,
)));
tracing::info!("Updated mempool filter (wallet integration pending)");
}

/// Record a transaction send for mempool filtering.
pub async fn record_transaction_send(&self, txid: dashcore::Txid) {
if let Some(ref mempool_filter) = self.mempool_filter {
mempool_filter.record_send(txid).await;
}
}
}
16 changes: 13 additions & 3 deletions dash-spv/src/client/message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,8 +403,18 @@ impl<
chainlocks_to_request.push(item);
}
Inventory::InstantSendLock(islock_hash) => {
tracing::info!("⚡ Inventory: New InstantSendLock {}", islock_hash);
islocks_to_request.push(item);
// Only fetch InstantSendLocks when we're fully synced and have masternode data
if self.sync_manager.is_synced()
&& self.sync_manager.get_masternode_engine().is_some()
{
tracing::info!("⚡ Inventory: New InstantSendLock {}", islock_hash);
islocks_to_request.push(item);
} else {
tracing::debug!(
"Skipping InstantSendLock {} fetch - not fully synced or masternode engine unavailable",
islock_hash
);
}
}
Inventory::Transaction(txid) => {
tracing::debug!("💸 Inventory: New transaction {}", txid);
Expand Down Expand Up @@ -444,7 +454,7 @@ impl<
self.network.send_message(getdata).await.map_err(SpvError::Network)?;
}

// Auto-request InstantLocks
// Auto-request InstantLocks (only when synced and masternodes available; gated above)
if !islocks_to_request.is_empty() {
tracing::info!("Requesting {} InstantLocks", islocks_to_request.len());
let getdata = NetworkMessage::GetData(islocks_to_request);
Expand Down
16 changes: 12 additions & 4 deletions dash-spv/src/client/sync_coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ impl<
self.update_status_display().await;

tracing::info!(
"✅ Initial sync requests sent! Current state - Headers: {}, Filter headers: {}",
"✅ Prepared initial sync state - Headers: {}, Filter headers: {}",
result.header_height,
result.filter_header_height
);
tracing::info!("📊 Actual sync will complete asynchronously through monitoring loop");
tracing::info!("📊 Sync requests will be sent by the monitoring loop");

Ok(result)
}
Expand Down Expand Up @@ -612,8 +612,16 @@ impl<
self.process_chainlock(clsig.clone()).await?;
}
NetworkMessage::ISLock(islock_msg) => {
// Additional client-level InstantLock processing
self.process_instantsendlock(islock_msg.clone()).await?;
// Only process InstantLocks when fully synced and masternode engine is available
if self.sync_manager.is_synced()
&& self.sync_manager.get_masternode_engine().is_some()
{
self.process_instantsendlock(islock_msg.clone()).await?;
} else {
tracing::debug!(
"Skipping InstantLock processing - not fully synced or masternode engine unavailable"
);
}
}
_ => {}
}
Expand Down
3 changes: 3 additions & 0 deletions dash-spv/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ pub enum ValidationError {
#[error("Invalid InstantLock: {0}")]
InvalidInstantLock(String),

#[error("Invalid signature: {0}")]
InvalidSignature(String),

#[error("Invalid filter header chain: {0}")]
InvalidFilterHeaderChain(String),

Expand Down
9 changes: 9 additions & 0 deletions dash-spv/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
.help("Disable masternode list synchronization")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("no-mempool")
.long("no-mempool")
.help("Disable mempool transaction tracking")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("validation-mode")
.long("validation-mode")
Expand Down Expand Up @@ -183,6 +189,9 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
if matches.get_flag("no-masternodes") {
config = config.without_masternodes();
}
if matches.get_flag("no-mempool") {
config.enable_mempool_tracking = false;
}

// Set start height if specified
if let Some(start_height_str) = matches.get_one::<String>("start-height") {
Expand Down
Loading
Loading