Skip to content

Commit b864dd4

Browse files
Merge pull request #163 from dashpay/feature/chainlock-peer-reputation
feat: add peer reputation penalization for invalid ChainLocks
2 parents 6a0902b + 8f0cb2d commit b864dd4

34 files changed

+513
-583
lines changed

dash-spv-ffi/FFI_API.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,10 +303,10 @@ dash_spv_ffi_config_get_mempool_strategy(config: *const FFIClientConfig,) -> FFI
303303
```
304304
305305
**Description:**
306-
Gets the mempool synchronization strategy # Safety - `config` must be a valid pointer to an FFIClientConfig or null - If null, returns FFIMempoolStrategy::Selective as default
306+
Gets the mempool synchronization strategy # Safety - `config` must be a valid pointer to an FFIClientConfig or null - If null, returns FFIMempoolStrategy::FetchAll as default
307307
308308
**Safety:**
309-
- `config` must be a valid pointer to an FFIClientConfig or null - If null, returns FFIMempoolStrategy::Selective as default
309+
- `config` must be a valid pointer to an FFIClientConfig or null - If null, returns FFIMempoolStrategy::FetchAll as default
310310
311311
**Module:** `config`
312312

dash-spv-ffi/include/dash_spv_ffi.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ namespace dash_spv_ffi {
1919
typedef enum FFIMempoolStrategy {
2020
FetchAll = 0,
2121
BloomFilter = 1,
22-
Selective = 2,
2322
} FFIMempoolStrategy;
2423

2524
typedef enum FFISyncStage {
@@ -817,7 +816,7 @@ int32_t dash_spv_ffi_config_set_persist_mempool(struct FFIClientConfig *config,
817816
*
818817
* # Safety
819818
* - `config` must be a valid pointer to an FFIClientConfig or null
820-
* - If null, returns FFIMempoolStrategy::Selective as default
819+
* - If null, returns FFIMempoolStrategy::FetchAll as default
821820
*/
822821

823822
enum FFIMempoolStrategy dash_spv_ffi_config_get_mempool_strategy(const struct FFIClientConfig *config)

dash-spv-ffi/src/client.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,12 @@ impl FFIDashSpvClient {
296296
dash_spv::types::SpvEvent::ChainLockReceived {
297297
..
298298
} => {}
299+
dash_spv::types::SpvEvent::InstantLockReceived {
300+
..
301+
} => {
302+
// InstantLock received and validated
303+
// TODO: Add FFI callback if needed for instant lock notifications
304+
}
299305
dash_spv::types::SpvEvent::MempoolTransactionAdded {
300306
ref txid,
301307
amount,
@@ -1445,7 +1451,7 @@ pub unsafe extern "C" fn dash_spv_ffi_client_record_send(
14451451
}
14461452
};
14471453

1448-
let txid = match Txid::from_str(txid_str) {
1454+
let _txid = match Txid::from_str(txid_str) {
14491455
Ok(t) => t,
14501456
Err(e) => {
14511457
set_last_error(&format!("Invalid txid: {}", e));
@@ -1468,7 +1474,6 @@ pub unsafe extern "C" fn dash_spv_ffi_client_record_send(
14681474
}
14691475
}
14701476
};
1471-
spv_client.record_transaction_send(txid).await;
14721477
let mut guard = inner.lock().unwrap();
14731478
*guard = Some(spv_client);
14741479
Ok(())

dash-spv-ffi/src/config.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -503,13 +503,13 @@ pub unsafe extern "C" fn dash_spv_ffi_config_get_mempool_tracking(
503503
///
504504
/// # Safety
505505
/// - `config` must be a valid pointer to an FFIClientConfig or null
506-
/// - If null, returns FFIMempoolStrategy::Selective as default
506+
/// - If null, returns FFIMempoolStrategy::FetchAll as default
507507
#[no_mangle]
508508
pub unsafe extern "C" fn dash_spv_ffi_config_get_mempool_strategy(
509509
config: *const FFIClientConfig,
510510
) -> FFIMempoolStrategy {
511511
if config.is_null() {
512-
return FFIMempoolStrategy::Selective;
512+
return FFIMempoolStrategy::FetchAll;
513513
}
514514

515515
let config = unsafe { &*((*config).inner as *const ClientConfig) };

dash-spv-ffi/src/types.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -395,15 +395,13 @@ pub unsafe extern "C" fn dash_spv_ffi_string_array_destroy(arr: *mut FFIArray) {
395395
pub enum FFIMempoolStrategy {
396396
FetchAll = 0,
397397
BloomFilter = 1,
398-
Selective = 2,
399398
}
400399

401400
impl From<MempoolStrategy> for FFIMempoolStrategy {
402401
fn from(strategy: MempoolStrategy) -> Self {
403402
match strategy {
404403
MempoolStrategy::FetchAll => FFIMempoolStrategy::FetchAll,
405404
MempoolStrategy::BloomFilter => FFIMempoolStrategy::BloomFilter,
406-
MempoolStrategy::Selective => FFIMempoolStrategy::Selective,
407405
}
408406
}
409407
}
@@ -413,7 +411,6 @@ impl From<FFIMempoolStrategy> for MempoolStrategy {
413411
match strategy {
414412
FFIMempoolStrategy::FetchAll => MempoolStrategy::FetchAll,
415413
FFIMempoolStrategy::BloomFilter => MempoolStrategy::BloomFilter,
416-
FFIMempoolStrategy::Selective => MempoolStrategy::Selective,
417414
}
418415
}
419416
}

dash-spv/src/client/chainlock.rs

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,16 @@ impl<
3737
let chain_state = self.state.read().await;
3838
{
3939
let mut storage = self.storage.lock().await;
40-
self.chainlock_manager
40+
if let Err(e) = self
41+
.chainlock_manager
4142
.process_chain_lock(chainlock.clone(), &chain_state, &mut *storage)
4243
.await
43-
.map_err(SpvError::Validation)?;
44+
{
45+
// Penalize the peer that relayed the invalid ChainLock
46+
let reason = format!("Invalid ChainLock: {}", e);
47+
let _ = self.network.penalize_last_message_peer_invalid_chainlock(&reason).await;
48+
return Err(SpvError::Validation(e));
49+
}
4450
}
4551
drop(chain_state);
4652

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

92-
// TODO: Implement InstantSendLock validation
93-
// - Verify BLS signature against known quorum
94-
// - Check if all inputs are locked
95-
// - Mark transaction as instantly confirmed
96-
// - Store InstantSendLock for future reference
98+
// Get the masternode engine from sync manager for proper quorum verification
99+
let masternode_engine = self.sync_manager.get_masternode_engine().ok_or_else(|| {
100+
SpvError::Validation(crate::error::ValidationError::MasternodeVerification(
101+
"Masternode engine not available for InstantLock verification".to_string(),
102+
))
103+
})?;
104+
105+
// Validate the InstantLock (structure + BLS signature)
106+
// This is REQUIRED for security - never accept InstantLocks without signature verification
107+
let validator = crate::validation::instantlock::InstantLockValidator::new();
108+
if let Err(e) = validator.validate(&islock, masternode_engine) {
109+
// Penalize the peer that relayed the invalid InstantLock
110+
let reason = format!("Invalid InstantLock: {}", e);
111+
tracing::warn!("{}", reason);
112+
113+
// Ban the peer using the reputation system
114+
let _ = self.network.penalize_last_message_peer_invalid_instantlock(&reason).await;
115+
116+
return Err(SpvError::Validation(e));
117+
}
97118

98-
// For now, just log the InstantSendLock details
99119
tracing::info!(
100-
"InstantSendLock validated: txid={}, inputs={}, signature={:?}",
120+
"InstantSendLock validated successfully: txid={}, inputs={}",
101121
islock.txid,
102-
islock.inputs.len(),
103-
islock.signature.to_string().chars().take(20).collect::<String>()
122+
islock.inputs.len()
104123
);
105124

125+
// Emit InstantLock event
126+
self.emit_event(SpvEvent::InstantLockReceived {
127+
txid: islock.txid,
128+
inputs: islock.inputs.clone(),
129+
});
130+
106131
Ok(())
107132
}
108133

dash-spv/src/client/config.rs

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,10 @@ use crate::types::ValidationMode;
1212
/// Strategy for handling mempool (unconfirmed) transactions.
1313
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1414
pub enum MempoolStrategy {
15-
/// Fetch all announced transactions (poor privacy, high bandwidth).
15+
/// Fetch all announced transactions (high bandwidth, sees all transactions).
1616
FetchAll,
1717
/// Use BIP37 bloom filters (moderate privacy, good efficiency).
1818
BloomFilter,
19-
/// Only fetch when recently sent or from known addresses (good privacy, default).
20-
Selective,
2119
}
2220

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

135-
/// Time window for recent sends in selective mode (seconds).
136-
pub recent_send_window_secs: u64,
137-
138133
/// Whether to fetch transactions from INV messages immediately.
139134
pub fetch_mempool_transactions: bool,
140135

@@ -232,11 +227,10 @@ impl Default for ClientConfig {
232227
max_filter_gap_restart_attempts: 5,
233228
max_filter_gap_sync_size: 50000,
234229
// Mempool defaults
235-
enable_mempool_tracking: false,
236-
mempool_strategy: MempoolStrategy::Selective,
230+
enable_mempool_tracking: true,
231+
mempool_strategy: MempoolStrategy::FetchAll,
237232
max_mempool_transactions: 1000,
238-
mempool_timeout_secs: 3600, // 1 hour
239-
recent_send_window_secs: 300, // 5 minutes
233+
mempool_timeout_secs: 3600, // 1 hour
240234
fetch_mempool_transactions: true,
241235
persist_mempool: false,
242236
// Request control defaults
@@ -388,12 +382,6 @@ impl ClientConfig {
388382
self
389383
}
390384

391-
/// Set recent send window for selective strategy.
392-
pub fn with_recent_send_window(mut self, window_secs: u64) -> Self {
393-
self.recent_send_window_secs = window_secs;
394-
self
395-
}
396-
397385
/// Enable or disable mempool persistence.
398386
pub fn with_mempool_persistence(mut self, enabled: bool) -> Self {
399387
self.persist_mempool = enabled;
@@ -449,13 +437,6 @@ impl ClientConfig {
449437
if self.mempool_timeout_secs == 0 {
450438
return Err("mempool_timeout_secs must be > 0".to_string());
451439
}
452-
if self.mempool_strategy == MempoolStrategy::Selective
453-
&& self.recent_send_window_secs == 0
454-
{
455-
return Err(
456-
"recent_send_window_secs must be > 0 for Selective strategy".to_string()
457-
);
458-
}
459440
}
460441

461442
Ok(())

dash-spv/src/client/config_test.rs

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,10 @@ mod tests {
3333
assert_eq!(config.filter_request_delay_ms, 0);
3434

3535
// Mempool defaults
36-
assert!(!config.enable_mempool_tracking);
37-
assert_eq!(config.mempool_strategy, MempoolStrategy::Selective);
36+
assert!(config.enable_mempool_tracking);
37+
assert_eq!(config.mempool_strategy, MempoolStrategy::FetchAll);
3838
assert_eq!(config.max_mempool_transactions, 1000);
3939
assert_eq!(config.mempool_timeout_secs, 3600);
40-
assert_eq!(config.recent_send_window_secs, 300);
4140
assert!(config.fetch_mempool_transactions);
4241
assert!(!config.persist_mempool);
4342
}
@@ -74,7 +73,6 @@ mod tests {
7473
.with_mempool_tracking(MempoolStrategy::BloomFilter)
7574
.with_max_mempool_transactions(500)
7675
.with_mempool_timeout(7200)
77-
.with_recent_send_window(600)
7876
.with_mempool_persistence(true)
7977
.with_start_height(100000);
8078

@@ -93,7 +91,6 @@ mod tests {
9391
assert_eq!(config.mempool_strategy, MempoolStrategy::BloomFilter);
9492
assert_eq!(config.max_mempool_transactions, 500);
9593
assert_eq!(config.mempool_timeout_secs, 7200);
96-
assert_eq!(config.recent_send_window_secs, 600);
9794
assert!(config.persist_mempool);
9895
assert_eq!(config.start_from_height, Some(100000));
9996
}
@@ -200,22 +197,7 @@ mod tests {
200197
assert_eq!(result.unwrap_err(), "mempool_timeout_secs must be > 0");
201198
}
202199

203-
#[test]
204-
fn test_validation_invalid_selective_strategy() {
205-
let config = ClientConfig {
206-
enable_mempool_tracking: true,
207-
mempool_strategy: MempoolStrategy::Selective,
208-
recent_send_window_secs: 0,
209-
..Default::default()
210-
};
211-
212-
let result = config.validate();
213-
assert!(result.is_err());
214-
assert_eq!(
215-
result.unwrap_err(),
216-
"recent_send_window_secs must be > 0 for Selective strategy"
217-
);
218-
}
200+
// Removed selective strategy validation test; Selective variant no longer exists
219201

220202
#[test]
221203
fn test_cfheader_gap_settings() {

dash-spv/src/client/lifecycle.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
1111
use std::collections::HashSet;
1212
use std::sync::Arc;
13-
use std::time::Duration;
1413
use tokio::sync::{mpsc, Mutex, RwLock};
1514

1615
use crate::chain::ChainLockManager;
@@ -121,7 +120,6 @@ impl<
121120
// TODO: Get monitored addresses from wallet
122121
self.mempool_filter = Some(Arc::new(MempoolFilter::new(
123122
self.config.mempool_strategy,
124-
Duration::from_secs(self.config.recent_send_window_secs),
125123
self.config.max_mempool_transactions,
126124
self.mempool_state.clone(),
127125
HashSet::new(), // Will be populated from wallet's monitored addresses

dash-spv/src/client/mempool.rs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
99
use std::collections::HashSet;
1010
use std::sync::Arc;
11-
use std::time::Duration;
1211

1312
use crate::error::Result;
1413
use crate::mempool_filter::MempoolFilter;
@@ -38,7 +37,6 @@ impl<
3837
// TODO: Get monitored addresses from wallet
3938
self.mempool_filter = Some(Arc::new(MempoolFilter::new(
4039
self.config.mempool_strategy,
41-
Duration::from_secs(self.config.recent_send_window_secs),
4240
self.config.max_mempool_transactions,
4341
self.mempool_state.clone(),
4442
HashSet::new(), // Will be populated from wallet's monitored addresses
@@ -147,19 +145,11 @@ impl<
147145
// For now, create empty filter until wallet integration is complete
148146
self.mempool_filter = Some(Arc::new(MempoolFilter::new(
149147
self.config.mempool_strategy,
150-
Duration::from_secs(self.config.recent_send_window_secs),
151148
self.config.max_mempool_transactions,
152149
self.mempool_state.clone(),
153150
HashSet::new(), // Will be populated from wallet's monitored addresses
154151
self.config.network,
155152
)));
156153
tracing::info!("Updated mempool filter (wallet integration pending)");
157154
}
158-
159-
/// Record a transaction send for mempool filtering.
160-
pub async fn record_transaction_send(&self, txid: dashcore::Txid) {
161-
if let Some(ref mempool_filter) = self.mempool_filter {
162-
mempool_filter.record_send(txid).await;
163-
}
164-
}
165155
}

0 commit comments

Comments
 (0)