Skip to content

Commit 5d182ab

Browse files
authored
feat: Validate headers during sync (#242)
We currently don't validate headers at all during sync. This PR fixes it. ### Timing on a Macbook with M2 Pro #### Basic ``` Header chain validation passed for 2000 headers in mode: Basic, duration: 84.291834ms Header chain validation passed for 2000 headers in mode: Basic, duration: 73.196375ms Header chain validation passed for 2000 headers in mode: Basic, duration: 89.125333ms Header chain validation passed for 2000 headers in mode: Basic, duration: 84.616459ms ``` #### Full ``` Header chain validation passed for 2000 headers in mode: Full, duration: 147.289208ms Header chain validation passed for 2000 headers in mode: Full, duration: 161.37225ms Header chain validation passed for 2000 headers in mode: Full, duration: 164.333167ms Header chain validation passed for 2000 headers in mode: Full, duration: 147.604709ms ```
1 parent 437b59c commit 5d182ab

File tree

3 files changed

+33
-24
lines changed

3 files changed

+33
-24
lines changed

dash-spv/src/client/sync_coordinator.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::network::constants::MESSAGE_RECEIVE_TIMEOUT;
1818
use crate::network::NetworkManager;
1919
use crate::storage::StorageManager;
2020
use crate::sync::headers::validate_headers;
21-
use crate::types::{DetailedSyncProgress, SyncProgress};
21+
use crate::types::{CachedHeader, DetailedSyncProgress, SyncProgress};
2222
use crate::ValidationMode;
2323
use key_wallet_manager::wallet_interface::WalletInterface;
2424
use std::time::{Duration, Instant, SystemTime};
@@ -945,7 +945,9 @@ impl<
945945
// Validate the batch of headers
946946
// Use basic validation only, the headers anyway are already validated since they
947947
// come from storage.
948-
if let Err(e) = validate_headers(&headers, ValidationMode::Basic) {
948+
let cached_headers =
949+
headers.iter().map(|h| CachedHeader::new(*h)).collect::<Vec<CachedHeader>>();
950+
if let Err(e) = validate_headers(&cached_headers, ValidationMode::Basic) {
949951
tracing::error!(
950952
"Header validation failed for range {}..{}: {:?}",
951953
current_height,

dash-spv/src/sync/headers/manager.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use crate::client::ClientConfig;
1212
use crate::error::{SyncError, SyncResult};
1313
use crate::network::NetworkManager;
1414
use crate::storage::StorageManager;
15+
use crate::sync::headers::validate_headers;
1516
use crate::sync::headers2::Headers2StateManager;
1617
use crate::types::{CachedHeader, ChainState};
1718
use std::sync::Arc;
@@ -238,7 +239,7 @@ impl<S: StorageManager + Send + Sync + 'static, N: NetworkManager + Send + Sync
238239
let cached_headers: Vec<CachedHeader> =
239240
headers.iter().map(|h| CachedHeader::new(*h)).collect();
240241

241-
// Step 2: Validate Batch Connection Point
242+
// Step 2: Validate Batch
242243
let first_cached = &cached_headers[0];
243244
let first_header = first_cached.header();
244245
let tip =
@@ -281,6 +282,12 @@ impl<S: StorageManager + Send + Sync + 'static, N: NetworkManager + Send + Sync
281282
}
282283
}
283284

285+
validate_headers(&cached_headers, self.config.validation_mode).map_err(|e| {
286+
let error = format!("Header validation failed: {}", e);
287+
tracing::error!(error);
288+
SyncError::Validation(error)
289+
})?;
290+
284291
self.last_sync_progress = std::time::Instant::now();
285292

286293
// Log details about the batch for debugging

dash-spv/src/sync/headers/validation.rs

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
//! Header validation functionality.
22
3-
use dashcore::{block::Header as BlockHeader, error::Error as DashError};
3+
use dashcore::error::Error as DashError;
44
use std::time::Instant;
55

66
use crate::error::{ValidationError, ValidationResult};
7-
use crate::types::ValidationMode;
7+
use crate::types::{CachedHeader, ValidationMode};
88

99
/// Validate a chain of headers considering the validation mode.
10-
pub fn validate_headers(headers: &[BlockHeader], mode: ValidationMode) -> ValidationResult<()> {
10+
pub fn validate_headers(headers: &[CachedHeader], mode: ValidationMode) -> ValidationResult<()> {
1111
if mode == ValidationMode::None {
1212
tracing::debug!("Skipping header validation: disabled");
1313
return Ok(());
@@ -64,7 +64,7 @@ pub fn validate_headers(headers: &[BlockHeader], mode: ValidationMode) -> Valida
6464
mod tests {
6565
use super::validate_headers;
6666
use crate::error::ValidationError;
67-
use crate::types::ValidationMode;
67+
use crate::types::{CachedHeader, ValidationMode};
6868
use dashcore::{
6969
block::{Header as BlockHeader, Version},
7070
blockdata::constants::genesis_block,
@@ -78,15 +78,15 @@ mod tests {
7878
nonce: u32,
7979
bits: u32,
8080
time: u32,
81-
) -> BlockHeader {
82-
BlockHeader {
81+
) -> CachedHeader {
82+
CachedHeader::new(BlockHeader {
8383
version: Version::from_consensus(0x20000000),
8484
prev_blockhash: prev_hash,
8585
merkle_root: dashcore::TxMerkleNode::from_byte_array([0; 32]),
8686
time,
8787
bits: dashcore::CompactTarget::from_consensus(bits),
8888
nonce,
89-
}
89+
})
9090
}
9191

9292
/// Create a test header with specific parameters
@@ -97,15 +97,15 @@ mod tests {
9797
time: u32,
9898
bits: u32,
9999
nonce: u32,
100-
) -> BlockHeader {
101-
BlockHeader {
100+
) -> CachedHeader {
101+
CachedHeader::new(BlockHeader {
102102
version: Version::from_consensus(version as i32),
103103
prev_blockhash: prev_hash,
104104
merkle_root: dashcore::TxMerkleNode::from_byte_array(merkle_root),
105105
time,
106106
bits: CompactTarget::from_consensus(bits),
107107
nonce,
108-
}
108+
})
109109
}
110110

111111
// ==================== Basic Tests ====================
@@ -122,7 +122,7 @@ mod tests {
122122
);
123123

124124
// Should pass with no previous header
125-
assert!(validate_headers(&[header], ValidationMode::None).is_ok());
125+
assert!(validate_headers(std::slice::from_ref(&header), ValidationMode::None).is_ok());
126126

127127
// Should pass even with invalid chain continuity
128128
let prev_header = create_test_header(
@@ -150,7 +150,7 @@ mod tests {
150150
let header2 = create_test_header(header1.block_hash(), 2, 0x1e0fffff, 1234567900);
151151

152152
// Should pass when headers connect
153-
assert!(validate_headers(&[header1, header2], ValidationMode::Basic).is_ok());
153+
assert!(validate_headers(&[header1.clone(), header2], ValidationMode::Basic).is_ok());
154154

155155
// Should fail when headers don't connect
156156
let disconnected_header = create_test_header(
@@ -201,7 +201,7 @@ mod tests {
201201
#[test]
202202
fn test_validate_headers_empty() {
203203
for mode in [ValidationMode::None, ValidationMode::Basic, ValidationMode::Full] {
204-
let headers: Vec<BlockHeader> = vec![];
204+
let headers: Vec<CachedHeader> = vec![];
205205
// Empty chain should pass
206206
assert!(validate_headers(&headers, mode).is_ok());
207207
}
@@ -281,7 +281,7 @@ mod tests {
281281
);
282282

283283
// Should fail when PoW validation is enabled
284-
let result = validate_headers(&[header1], ValidationMode::Full);
284+
let result = validate_headers(std::slice::from_ref(&header1), ValidationMode::Full);
285285
assert!(matches!(result, Err(ValidationError::InvalidProofOfWork)));
286286
}
287287

@@ -290,13 +290,13 @@ mod tests {
290290
#[test]
291291
fn test_genesis_block_validation() {
292292
for network in [Network::Dash, Network::Testnet, Network::Regtest] {
293-
let genesis = genesis_block(network).header;
293+
let genesis = CachedHeader::new(genesis_block(network).header);
294294

295295
// Genesis block should validate with no previous header
296-
assert!(validate_headers(&[genesis], ValidationMode::Full).is_ok());
296+
assert!(validate_headers(std::slice::from_ref(&genesis), ValidationMode::Full).is_ok());
297297

298298
// Genesis block with itself as previous should fail
299-
let result = validate_headers(&[genesis, genesis], ValidationMode::Full);
299+
let result = validate_headers(&[genesis.clone(), genesis], ValidationMode::Full);
300300
assert!(matches!(result, Err(ValidationError::InvalidHeaderChain(_))));
301301
}
302302
}
@@ -365,7 +365,7 @@ mod tests {
365365
);
366366

367367
// Should validate single header
368-
assert!(validate_headers(&[header1], ValidationMode::Basic).is_ok());
368+
assert!(validate_headers(std::slice::from_ref(&header1), ValidationMode::Basic).is_ok());
369369

370370
// Should validate chain continuity
371371
assert!(validate_headers(&[header1, header2], ValidationMode::Basic).is_ok());
@@ -386,7 +386,7 @@ mod tests {
386386
);
387387

388388
// Should validate when single header
389-
assert!(validate_headers(&[header], ValidationMode::Basic).is_ok());
389+
assert!(validate_headers(std::slice::from_ref(&header), ValidationMode::Basic).is_ok());
390390

391391
// Create a previous header that would NOT match
392392
let prev_header = create_test_header_with_params(
@@ -530,7 +530,7 @@ mod tests {
530530
);
531531

532532
// Chain with duplicate headers (same header repeated)
533-
let headers = vec![header, header];
533+
let headers = vec![header.clone(), header];
534534

535535
// Should fail because second header's prev_blockhash won't match first header's hash
536536
let result = validate_headers(&headers, ValidationMode::Basic);
@@ -562,7 +562,7 @@ mod tests {
562562
);
563563

564564
// All merkle roots should be valid for basic validation
565-
assert!(validate_headers(&[header], ValidationMode::Basic).is_ok());
565+
assert!(validate_headers(std::slice::from_ref(&header), ValidationMode::Basic).is_ok());
566566

567567
prev_hash = header.block_hash();
568568
}

0 commit comments

Comments
 (0)