Skip to content

Commit fe99e22

Browse files
feat: validate cfilter hash against cfheader chain (#149)
* fix(ffi-cli): do not pre-maturely break out, continue sync * feat(spv): verify incoming cfilters against expected CFHeaders chain - Compute BlockFilter header from received cfilter and compare to stored filter header - Reject cfilters whose computed header mismatches expected value - Apply verification in both initial sync and post-sync filter handling
1 parent 9b0c65a commit fe99e22

File tree

2 files changed

+88
-1
lines changed

2 files changed

+88
-1
lines changed

dash-spv/src/sync/filters.rs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Filter synchronization functionality.
22
33
use dashcore::{
4-
bip158::{BlockFilterReader, Error as Bip158Error},
4+
bip158::{BlockFilter, BlockFilterReader, Error as Bip158Error},
55
hash_types::FilterHeader,
66
network::message::NetworkMessage,
77
network::message_blockdata::Inventory,
@@ -102,6 +102,53 @@ pub struct FilterSyncManager<S: StorageManager, N: NetworkManager> {
102102
impl<S: StorageManager + Send + Sync + 'static, N: NetworkManager + Send + Sync + 'static>
103103
FilterSyncManager<S, N>
104104
{
105+
/// Verify that the received compact filter hashes to the expected filter header
106+
/// based on previously synchronized CFHeaders.
107+
pub async fn verify_cfilter_against_headers(
108+
&self,
109+
filter_data: &[u8],
110+
height: u32,
111+
storage: &S,
112+
) -> SyncResult<bool> {
113+
// We expect filter headers to be synced before requesting filters.
114+
// If we're at height 0 (genesis), skip verification because there is no previous header.
115+
if height == 0 {
116+
tracing::debug!("Skipping cfilter verification at genesis height 0");
117+
return Ok(true);
118+
}
119+
120+
// Load previous and expected headers
121+
let prev_header = storage.get_filter_header(height - 1).await.map_err(|e| {
122+
SyncError::Storage(format!("Failed to load previous filter header: {}", e))
123+
})?;
124+
let expected_header = storage.get_filter_header(height).await.map_err(|e| {
125+
SyncError::Storage(format!("Failed to load expected filter header: {}", e))
126+
})?;
127+
128+
let (Some(prev_header), Some(expected_header)) = (prev_header, expected_header) else {
129+
tracing::warn!(
130+
"Missing filter headers in storage for height {} (prev and/or expected)",
131+
height
132+
);
133+
return Ok(false);
134+
};
135+
136+
// Compute the header from the received filter bytes and compare
137+
let filter = BlockFilter::new(filter_data);
138+
let computed_header = filter.filter_header(&prev_header);
139+
140+
let matches = computed_header == expected_header;
141+
if !matches {
142+
tracing::error!(
143+
"CFilter header mismatch at height {}: computed={:?}, expected={:?}",
144+
height,
145+
computed_header,
146+
expected_header
147+
);
148+
}
149+
150+
Ok(matches)
151+
}
105152
/// Scan backward from `abs_height` down to `min_abs_height` (inclusive)
106153
/// to find the nearest available block header stored in `storage`.
107154
/// Returns the found `(BlockHash, height)` or `None` if none available.

dash-spv/src/sync/sequential/mod.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,6 +1362,32 @@ impl<
13621362
let mut wallet = self.wallet.write().await;
13631363

13641364
// Check filter against wallet if available
1365+
// First, verify filter data matches expected filter header chain
1366+
let height = storage
1367+
.get_header_height_by_hash(&cfilter.block_hash)
1368+
.await
1369+
.map_err(|e| SyncError::Storage(format!("Failed to get filter block height: {}", e)))?
1370+
.ok_or_else(|| {
1371+
SyncError::Validation(format!(
1372+
"Block height not found for cfilter block {}",
1373+
cfilter.block_hash
1374+
))
1375+
})?;
1376+
1377+
let header_ok = self
1378+
.filter_sync
1379+
.verify_cfilter_against_headers(&cfilter.filter, height, &*storage)
1380+
.await?;
1381+
1382+
if !header_ok {
1383+
tracing::warn!(
1384+
"Rejecting CFilter for block {} at height {} due to header mismatch",
1385+
cfilter.block_hash,
1386+
height
1387+
);
1388+
return Ok(());
1389+
}
1390+
13651391
let matches = self
13661392
.filter_sync
13671393
.check_filter_for_matches(
@@ -1963,6 +1989,20 @@ impl<
19631989
.map_err(|e| SyncError::Storage(format!("Failed to get filter block height: {}", e)))?
19641990
.ok_or(SyncError::InvalidState("Filter block height not found".to_string()))?;
19651991

1992+
// Verify against expected header chain before storing
1993+
let header_ok = self
1994+
.filter_sync
1995+
.verify_cfilter_against_headers(&cfilter.filter, height, &*storage)
1996+
.await?;
1997+
if !header_ok {
1998+
tracing::warn!(
1999+
"Rejecting post-sync CFilter for block {} at height {} due to header mismatch",
2000+
cfilter.block_hash,
2001+
height
2002+
);
2003+
return Ok(());
2004+
}
2005+
19662006
// Store the filter
19672007
storage
19682008
.store_filter(height, &cfilter.filter)

0 commit comments

Comments
 (0)